Merge branch 'main' of github.com:Infisical/infisical into sid/k8s-operator

This commit is contained in:
sidwebworks
2025-08-07 01:07:52 +05:30
264 changed files with 7349 additions and 4497 deletions
+74 -115
View File
@@ -38,6 +38,7 @@
"@octokit/core": "^5.2.1", "@octokit/core": "^5.2.1",
"@octokit/plugin-paginate-graphql": "^4.0.1", "@octokit/plugin-paginate-graphql": "^4.0.1",
"@octokit/plugin-retry": "^5.0.5", "@octokit/plugin-retry": "^5.0.5",
"@octokit/request": "8.4.1",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@octopusdeploy/api-client": "^3.4.1", "@octopusdeploy/api-client": "^3.4.1",
@@ -9777,18 +9778,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/auth-app/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": { "node_modules/@octokit/auth-app/node_modules/@octokit/openapi-types": {
"version": "22.2.0", "version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -9835,11 +9824,6 @@
"node": "14 || >=16.14" "node": "14 || >=16.14"
} }
}, },
"node_modules/@octokit/auth-app/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/auth-oauth-app": { "node_modules/@octokit/auth-oauth-app": {
"version": "8.1.1", "version": "8.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-app/-/auth-oauth-app-8.1.1.tgz",
@@ -9855,18 +9839,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": { "node_modules/@octokit/auth-oauth-app/node_modules/@octokit/openapi-types": {
"version": "22.2.0", "version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -9905,11 +9877,6 @@
"@octokit/openapi-types": "^22.2.0" "@octokit/openapi-types": "^22.2.0"
} }
}, },
"node_modules/@octokit/auth-oauth-app/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/auth-oauth-device": { "node_modules/@octokit/auth-oauth-device": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-device/-/auth-oauth-device-7.1.1.tgz",
@@ -9924,18 +9891,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": { "node_modules/@octokit/auth-oauth-device/node_modules/@octokit/openapi-types": {
"version": "22.2.0", "version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -9974,11 +9929,6 @@
"@octokit/openapi-types": "^22.2.0" "@octokit/openapi-types": "^22.2.0"
} }
}, },
"node_modules/@octokit/auth-oauth-device/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/auth-oauth-user": { "node_modules/@octokit/auth-oauth-user": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-oauth-user/-/auth-oauth-user-5.1.1.tgz",
@@ -9994,18 +9944,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": { "node_modules/@octokit/auth-oauth-user/node_modules/@octokit/openapi-types": {
"version": "22.2.0", "version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -10044,11 +9982,6 @@
"@octokit/openapi-types": "^22.2.0" "@octokit/openapi-types": "^22.2.0"
} }
}, },
"node_modules/@octokit/auth-oauth-user/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/auth-token": { "node_modules/@octokit/auth-token": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz",
@@ -10102,32 +10035,38 @@
"@octokit/openapi-types": "^24.2.0" "@octokit/openapi-types": "^24.2.0"
} }
}, },
"node_modules/@octokit/core/node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"node_modules/@octokit/endpoint": { "node_modules/@octokit/endpoint": {
"version": "9.0.6", "version": "10.1.4",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==", "integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.1.0", "@octokit/types": "^14.0.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^7.0.2"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": { "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
"version": "24.2.0", "version": "25.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.1.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==", "integrity": "sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@octokit/endpoint/node_modules/@octokit/types": { "node_modules/@octokit/endpoint/node_modules/@octokit/types": {
"version": "13.10.0", "version": "14.1.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.1.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==", "integrity": "sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@octokit/openapi-types": "^24.2.0" "@octokit/openapi-types": "^25.1.0"
} }
}, },
"node_modules/@octokit/graphql": { "node_modules/@octokit/graphql": {
@@ -10159,6 +10098,12 @@
"@octokit/openapi-types": "^24.2.0" "@octokit/openapi-types": "^24.2.0"
} }
}, },
"node_modules/@octokit/graphql/node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"node_modules/@octokit/oauth-authorization-url": { "node_modules/@octokit/oauth-authorization-url": {
"version": "7.1.1", "version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/oauth-authorization-url/-/oauth-authorization-url-7.1.1.tgz",
@@ -10181,18 +10126,6 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/oauth-methods/node_modules/@octokit/endpoint": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz",
"integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==",
"dependencies": {
"@octokit/types": "^13.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": { "node_modules/@octokit/oauth-methods/node_modules/@octokit/openapi-types": {
"version": "22.2.0", "version": "22.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz",
@@ -10231,11 +10164,6 @@
"@octokit/openapi-types": "^22.2.0" "@octokit/openapi-types": "^22.2.0"
} }
}, },
"node_modules/@octokit/oauth-methods/node_modules/universal-user-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz",
"integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q=="
},
"node_modules/@octokit/openapi-types": { "node_modules/@octokit/openapi-types": {
"version": "19.1.0", "version": "19.1.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz",
@@ -10376,31 +10304,54 @@
} }
}, },
"node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": { "node_modules/@octokit/request-error/node_modules/@octokit/openapi-types": {
"version": "22.2.0", "version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
}, },
"node_modules/@octokit/request-error/node_modules/@octokit/types": { "node_modules/@octokit/request-error/node_modules/@octokit/types": {
"version": "13.6.1", "version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/openapi-types": "^22.2.0" "@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/request/node_modules/@octokit/endpoint": {
"version": "9.0.6",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.6.tgz",
"integrity": "sha512-H1fNTMA57HbkFESSt3Y9+FBICv+0jFceJFPWDePYlR/iMGrwM5ph+Dd4XRQs+8X+PUFURLQgX9ChPfhJ/1uNQw==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
} }
}, },
"node_modules/@octokit/request/node_modules/@octokit/openapi-types": { "node_modules/@octokit/request/node_modules/@octokit/openapi-types": {
"version": "22.2.0", "version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==" "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
}, },
"node_modules/@octokit/request/node_modules/@octokit/types": { "node_modules/@octokit/request/node_modules/@octokit/types": {
"version": "13.6.1", "version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", "integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/openapi-types": "^22.2.0" "@octokit/openapi-types": "^24.2.0"
} }
}, },
"node_modules/@octokit/request/node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"node_modules/@octokit/rest": { "node_modules/@octokit/rest": {
"version": "20.0.2", "version": "20.0.2",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz",
@@ -18288,7 +18239,8 @@
"node_modules/fast-content-type-parse": { "node_modules/fast-content-type-parse": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz",
"integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==",
"license": "MIT"
}, },
"node_modules/fast-copy": { "node_modules/fast-copy": {
"version": "3.0.1", "version": "3.0.1",
@@ -24776,6 +24728,12 @@
"jsonwebtoken": "^9.0.2" "jsonwebtoken": "^9.0.2"
} }
}, },
"node_modules/octokit-auth-probot/node_modules/universal-user-agent": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==",
"license": "ISC"
},
"node_modules/odbc": { "node_modules/odbc": {
"version": "2.4.9", "version": "2.4.9",
"resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz", "resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.9.tgz",
@@ -30705,9 +30663,10 @@
"integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ==" "integrity": "sha512-G5o6f95b5BggDGuUfKDApKaCgNYy2x7OdHY0zSMF081O0EJobw+1130VONhrA7ezGSV2FNOGyM+KQpQZAr9bIQ=="
}, },
"node_modules/universal-user-agent": { "node_modules/universal-user-agent": {
"version": "6.0.1", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz",
"integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==",
"license": "ISC"
}, },
"node_modules/universalify": { "node_modules/universalify": {
"version": "2.0.1", "version": "2.0.1",
+1
View File
@@ -158,6 +158,7 @@
"@octokit/core": "^5.2.1", "@octokit/core": "^5.2.1",
"@octokit/plugin-paginate-graphql": "^4.0.1", "@octokit/plugin-paginate-graphql": "^4.0.1",
"@octokit/plugin-retry": "^5.0.5", "@octokit/plugin-retry": "^5.0.5",
"@octokit/request": "8.4.1",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
"@octopusdeploy/api-client": "^3.4.1", "@octopusdeploy/api-client": "^3.4.1",
+1
View File
@@ -99,6 +99,7 @@ const main = async () => {
(el) => (el) =>
!el.tableName.includes("_migrations") && !el.tableName.includes("_migrations") &&
!el.tableName.includes("audit_logs_") && !el.tableName.includes("audit_logs_") &&
!el.tableName.includes("active_locks") &&
el.tableName !== "intermediate_audit_logs" el.tableName !== "intermediate_audit_logs"
); );
+2
View File
@@ -18,6 +18,7 @@ import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/extern
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service"; import { TGithubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
import { TIdentityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template";
import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service"; import { TIdentityProjectAdditionalPrivilegeServiceFactory } from "@app/ee/services/identity-project-additional-privilege/identity-project-additional-privilege-service";
import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service"; import { TIdentityProjectAdditionalPrivilegeV2ServiceFactory } from "@app/ee/services/identity-project-additional-privilege-v2/identity-project-additional-privilege-v2-service";
import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal"; import { TKmipClientDALFactory } from "@app/ee/services/kmip/kmip-client-dal";
@@ -300,6 +301,7 @@ declare module "fastify" {
reminder: TReminderServiceFactory; reminder: TReminderServiceFactory;
bus: TEventBusService; bus: TEventBusService;
sse: TServerSentEventsService; sse: TServerSentEventsService;
identityAuthTemplate: TIdentityAuthTemplateServiceFactory;
}; };
// this is exclusive use for middlewares in which we need to inject data // this is exclusive use for middlewares in which we need to inject data
// everywhere else access using service layer // everywhere else access using service layer
+10
View File
@@ -494,6 +494,11 @@ import {
TAccessApprovalPoliciesEnvironmentsInsert, TAccessApprovalPoliciesEnvironmentsInsert,
TAccessApprovalPoliciesEnvironmentsUpdate TAccessApprovalPoliciesEnvironmentsUpdate
} from "@app/db/schemas/access-approval-policies-environments"; } from "@app/db/schemas/access-approval-policies-environments";
import {
TIdentityAuthTemplates,
TIdentityAuthTemplatesInsert,
TIdentityAuthTemplatesUpdate
} from "@app/db/schemas/identity-auth-templates";
import { import {
TIdentityLdapAuths, TIdentityLdapAuths,
TIdentityLdapAuthsInsert, TIdentityLdapAuthsInsert,
@@ -878,6 +883,11 @@ declare module "knex/types/tables" {
TIdentityProjectAdditionalPrivilegeInsert, TIdentityProjectAdditionalPrivilegeInsert,
TIdentityProjectAdditionalPrivilegeUpdate TIdentityProjectAdditionalPrivilegeUpdate
>; >;
[TableName.IdentityAuthTemplate]: KnexOriginal.CompositeTableType<
TIdentityAuthTemplates,
TIdentityAuthTemplatesInsert,
TIdentityAuthTemplatesUpdate
>;
[TableName.AccessApprovalPolicy]: KnexOriginal.CompositeTableType< [TableName.AccessApprovalPolicy]: KnexOriginal.CompositeTableType<
TAccessApprovalPolicies, TAccessApprovalPolicies,
@@ -0,0 +1,18 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(TableName.UserEncryptionKey, (table) => {
table.text("encryptedPrivateKey").nullable().alter();
table.text("publicKey").nullable().alter();
table.text("iv").nullable().alter();
table.text("tag").nullable().alter();
table.text("salt").nullable().alter();
table.text("verifier").nullable().alter();
});
}
export async function down(): Promise<void> {
// do nothing for now to avoid breaking down migrations
}
@@ -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.Reminder, "fromDate"))) {
await knex.schema.alterTable(TableName.Reminder, (t) => {
t.timestamp("fromDate", { useTz: true }).nullable();
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.Reminder, "fromDate")) {
await knex.schema.alterTable(TableName.Reminder, (t) => {
t.dropColumn("fromDate");
});
}
}
@@ -0,0 +1,36 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
if (!(await knex.schema.hasTable(TableName.IdentityAuthTemplate))) {
await knex.schema.createTable(TableName.IdentityAuthTemplate, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.binary("templateFields").notNullable();
t.uuid("orgId").notNullable();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.string("name", 64).notNullable();
t.string("authMethod").notNullable();
t.timestamps(true, true, true);
});
await createOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
}
if (!(await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId"))) {
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
t.uuid("templateId").nullable();
t.foreign("templateId").references("id").inTable(TableName.IdentityAuthTemplate).onDelete("SET NULL");
});
}
}
export async function down(knex: Knex): Promise<void> {
if (await knex.schema.hasColumn(TableName.IdentityLdapAuth, "templateId")) {
await knex.schema.alterTable(TableName.IdentityLdapAuth, (t) => {
t.dropForeign(["templateId"]);
t.dropColumn("templateId");
});
}
await knex.schema.dropTableIfExists(TableName.IdentityAuthTemplate);
await dropOnUpdateTrigger(knex, TableName.IdentityAuthTemplate);
}
@@ -0,0 +1,24 @@
// Code generated by automation script, DO NOT EDIT.
// Automated by pulling database and generating zod schema
// To update. Just run npm run generate:schema
// Written by akhilmhdh.
import { z } from "zod";
import { zodBuffer } from "@app/lib/zod";
import { TImmutableDBKeys } from "./models";
export const IdentityAuthTemplatesSchema = z.object({
id: z.string().uuid(),
templateFields: zodBuffer,
orgId: z.string().uuid(),
name: z.string(),
authMethod: z.string(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TIdentityAuthTemplates = z.infer<typeof IdentityAuthTemplatesSchema>;
export type TIdentityAuthTemplatesInsert = Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>;
export type TIdentityAuthTemplatesUpdate = Partial<Omit<z.input<typeof IdentityAuthTemplatesSchema>, TImmutableDBKeys>>;
@@ -25,7 +25,8 @@ export const IdentityLdapAuthsSchema = z.object({
allowedFields: z.unknown().nullable().optional(), allowedFields: z.unknown().nullable().optional(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
accessTokenPeriod: z.coerce.number().default(0) accessTokenPeriod: z.coerce.number().default(0),
templateId: z.string().uuid().nullable().optional()
}); });
export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>; export type TIdentityLdapAuths = z.infer<typeof IdentityLdapAuthsSchema>;
+1
View File
@@ -91,6 +91,7 @@ export enum TableName {
IdentityProjectMembership = "identity_project_memberships", IdentityProjectMembership = "identity_project_memberships",
IdentityProjectMembershipRole = "identity_project_membership_role", IdentityProjectMembershipRole = "identity_project_membership_role",
IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege", IdentityProjectAdditionalPrivilege = "identity_project_additional_privilege",
IdentityAuthTemplate = "identity_auth_templates",
// used by both identity and users // used by both identity and users
IdentityMetadata = "identity_metadata", IdentityMetadata = "identity_metadata",
ResourceMetadata = "resource_metadata", ResourceMetadata = "resource_metadata",
+2 -1
View File
@@ -14,7 +14,8 @@ export const RemindersSchema = z.object({
repeatDays: z.number().nullable().optional(), repeatDays: z.number().nullable().optional(),
nextReminderDate: z.date(), nextReminderDate: z.date(),
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date() updatedAt: z.date(),
fromDate: z.date().nullable().optional()
}); });
export type TReminders = z.infer<typeof RemindersSchema>; export type TReminders = z.infer<typeof RemindersSchema>;
@@ -15,12 +15,12 @@ export const UserEncryptionKeysSchema = z.object({
protectedKey: z.string().nullable().optional(), protectedKey: z.string().nullable().optional(),
protectedKeyIV: z.string().nullable().optional(), protectedKeyIV: z.string().nullable().optional(),
protectedKeyTag: z.string().nullable().optional(), protectedKeyTag: z.string().nullable().optional(),
publicKey: z.string(), publicKey: z.string().nullable().optional(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullable().optional(),
iv: z.string(), iv: z.string().nullable().optional(),
tag: z.string(), tag: z.string().nullable().optional(),
salt: z.string(), salt: z.string().nullable().optional(),
verifier: z.string(), verifier: z.string().nullable().optional(),
userId: z.string().uuid(), userId: z.string().uuid(),
hashedPassword: z.string().nullable().optional(), hashedPassword: z.string().nullable().optional(),
serverEncryptedPrivateKey: z.string().nullable().optional(), serverEncryptedPrivateKey: z.string().nullable().optional(),
+4
View File
@@ -115,6 +115,10 @@ export const generateUserSrpKeys = async (password: string) => {
}; };
export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => { export const getUserPrivateKey = async (password: string, user: TUserEncryptionKeys) => {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new Error("User encrypted private key not found");
}
const derivedKey = await argon2.hash(password, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,
+4 -3
View File
@@ -1,7 +1,7 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { crypto } from "@app/lib/crypto"; import { initEnvConfig } from "@app/lib/config/env";
import { initLogger } from "@app/lib/logger"; import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal"; import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { AuthMethod } from "../../services/auth/auth-type"; import { AuthMethod } from "../../services/auth/auth-type";
@@ -17,7 +17,7 @@ export async function seed(knex: Knex): Promise<void> {
initLogger(); initLogger();
const superAdminDAL = superAdminDALFactory(knex); const superAdminDAL = superAdminDALFactory(knex);
await crypto.initialize(superAdminDAL); await initEnvConfig(superAdminDAL, logger);
await knex(TableName.SuperAdmin).insert([ await knex(TableName.SuperAdmin).insert([
// eslint-disable-next-line // eslint-disable-next-line
@@ -25,6 +25,7 @@ export async function seed(knex: Knex): Promise<void> {
{ id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true } { id: "00000000-0000-0000-0000-000000000000", initialized: true, allowSignUp: true }
]); ]);
// Inserts seed entries // Inserts seed entries
const [user] = await knex(TableName.Users) const [user] = await knex(TableName.Users)
.insert([ .insert([
{ {
+180 -19
View File
@@ -1,9 +1,28 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { initEnvConfig } from "@app/lib/config/env";
import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography"; import { crypto, SymmetricKeySize } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { initLogger, logger } from "@app/lib/logger";
import { alphaNumericNanoId } from "@app/lib/nanoid";
import { AuthMethod } from "@app/services/auth/auth-type";
import { assignWorkspaceKeysToMembers, createProjectKey } from "@app/services/project/project-fns";
import { projectKeyDALFactory } from "@app/services/project-key/project-key-dal";
import { projectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
import { projectUserMembershipRoleDALFactory } from "@app/services/project-membership/project-user-membership-role-dal";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { userDALFactory } from "@app/services/user/user-dal";
import { ProjectMembershipRole, ProjectType, SecretEncryptionAlgo, SecretKeyEncoding, TableName } from "../schemas"; import {
import { buildUserProjectKey, getUserPrivateKey, seedData1 } from "../seed-data"; OrgMembershipRole,
OrgMembershipStatus,
ProjectMembershipRole,
ProjectType,
SecretEncryptionAlgo,
SecretKeyEncoding,
TableName
} from "../schemas";
import { seedData1 } from "../seed-data";
export const DEFAULT_PROJECT_ENVS = [ export const DEFAULT_PROJECT_ENVS = [
{ name: "Development", slug: "dev" }, { name: "Development", slug: "dev" },
@@ -11,12 +30,159 @@ export const DEFAULT_PROJECT_ENVS = [
{ name: "Production", slug: "prod" } { name: "Production", slug: "prod" }
]; ];
const createUserWithGhostUser = async (
orgId: string,
projectId: string,
userId: string,
userOrgMembershipId: string,
knex: Knex
) => {
const projectKeyDAL = projectKeyDALFactory(knex);
const userDAL = userDALFactory(knex);
const projectMembershipDAL = projectMembershipDALFactory(knex);
const projectUserMembershipRoleDAL = projectUserMembershipRoleDALFactory(knex);
const email = `sudo-${alphaNumericNanoId(16)}-${orgId}@infisical.com`; // We add a nanoid because the email is unique. And we have to create a new ghost user each time, so we can have access to the private key.
const password = crypto.randomBytes(128).toString("hex");
const [ghostUser] = await knex(TableName.Users)
.insert({
isGhost: true,
authMethods: [AuthMethod.EMAIL],
username: email,
email,
isAccepted: true
})
.returning("*");
const encKeys = await generateUserSrpKeys(email, password);
await knex(TableName.UserEncryptionKey)
.insert({ userId: ghostUser.id, encryptionVersion: 2, publicKey: encKeys.publicKey })
.onConflict("userId")
.merge();
await knex(TableName.OrgMembership)
.insert({
orgId,
userId: ghostUser.id,
role: OrgMembershipRole.Admin,
status: OrgMembershipStatus.Accepted,
isActive: true
})
.returning("*");
const [projectMembership] = await knex(TableName.ProjectMembership)
.insert({
userId: ghostUser.id,
projectId
})
.returning("*");
await knex(TableName.ProjectUserMembershipRole).insert({
projectMembershipId: projectMembership.id,
role: ProjectMembershipRole.Admin
});
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: encKeys.publicKey,
privateKey: encKeys.plainPrivateKey
});
await knex(TableName.ProjectKeys).insert({
projectId,
receiverId: ghostUser.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: ghostUser.id
});
const { iv, tag, ciphertext, encoding, algorithm } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(encKeys.plainPrivateKey);
await knex(TableName.ProjectBot).insert({
name: "Infisical Bot (Ghost)",
projectId,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: encKeys.publicKey,
senderId: ghostUser.id,
algorithm,
keyEncoding: encoding
});
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.id, projectId, knex);
if (!latestKey) {
throw new Error("Latest key not found for user");
}
const user = await userDAL.findUserEncKeyByUserId(userId, knex);
if (!user || !user.publicKey) {
throw new Error("User not found");
}
const [projectAdmin] = assignWorkspaceKeysToMembers({
decryptKey: latestKey,
userPrivateKey: encKeys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: userOrgMembershipId
}
]
});
// Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create(
{
projectId,
userId: user.id
},
knex
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
knex
);
// Create a project key for the user
await projectKeyDAL.create(
{
encryptedKey: projectAdmin.workspaceEncryptedKey,
nonce: projectAdmin.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: user.id,
projectId
},
knex
);
return {
user: ghostUser,
keys: encKeys
};
};
export async function seed(knex: Knex): Promise<void> { export async function seed(knex: Knex): Promise<void> {
// Deletes ALL existing entries // Deletes ALL existing entries
await knex(TableName.Project).del(); await knex(TableName.Project).del();
await knex(TableName.Environment).del(); await knex(TableName.Environment).del();
await knex(TableName.SecretFolder).del(); await knex(TableName.SecretFolder).del();
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);
const [project] = await knex(TableName.Project) const [project] = await knex(TableName.Project)
.insert({ .insert({
name: seedData1.project.name, name: seedData1.project.name,
@@ -29,29 +195,24 @@ export async function seed(knex: Knex): Promise<void> {
}) })
.returning("*"); .returning("*");
const projectMembership = await knex(TableName.ProjectMembership) const userOrgMembership = await knex(TableName.OrgMembership)
.insert({ .where({
projectId: project.id, orgId: seedData1.organization.id,
userId: seedData1.id userId: seedData1.id
}) })
.returning("*"); .first();
await knex(TableName.ProjectUserMembershipRole).insert({
role: ProjectMembershipRole.Admin,
projectMembershipId: projectMembership[0].id
});
if (!userOrgMembership) {
throw new Error("User org membership not found");
}
const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first(); const user = await knex(TableName.UserEncryptionKey).where({ userId: seedData1.id }).first();
if (!user) throw new Error("User not found"); if (!user) throw new Error("User not found");
const userPrivateKey = await getUserPrivateKey(seedData1.password, user); if (!user.publicKey) {
const projectKey = buildUserProjectKey(userPrivateKey, user.publicKey); throw new Error("User public key not found");
await knex(TableName.ProjectKeys).insert({ }
projectId: project.id,
nonce: projectKey.nonce, await createUserWithGhostUser(seedData1.organization.id, project.id, seedData1.id, userOrgMembership.id, knex);
encryptedKey: projectKey.ciphertext,
receiverId: seedData1.id,
senderId: seedData1.id
});
// create default environments and default folders // create default environments and default folders
const envs = await knex(TableName.Environment) const envs = await knex(TableName.Environment)
@@ -1,6 +1,9 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { initEnvConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { initLogger, logger } from "@app/lib/logger";
import { superAdminDALFactory } from "@app/services/super-admin/super-admin-dal";
import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas"; import { IdentityAuthMethod, OrgMembershipRole, ProjectMembershipRole, TableName } from "../schemas";
import { seedData1 } from "../seed-data"; import { seedData1 } from "../seed-data";
@@ -10,6 +13,11 @@ export async function seed(knex: Knex): Promise<void> {
await knex(TableName.Identity).del(); await knex(TableName.Identity).del();
await knex(TableName.IdentityOrgMembership).del(); await knex(TableName.IdentityOrgMembership).del();
initLogger();
const superAdminDAL = superAdminDALFactory(knex);
await initEnvConfig(superAdminDAL, logger);
// Inserts seed entries // Inserts seed entries
await knex(TableName.Identity).insert([ await knex(TableName.Identity).insert([
{ {
@@ -0,0 +1,391 @@
import { z } from "zod";
import { IdentityAuthTemplatesSchema } from "@app/db/schemas/identity-auth-templates";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
IdentityAuthTemplateMethod,
TEMPLATE_SUCCESS_MESSAGES,
TEMPLATE_VALIDATION_MESSAGES
} from "@app/ee/services/identity-auth-template/identity-auth-template-enums";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
const ldapTemplateFieldsSchema = z.object({
url: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.URL_REQUIRED),
bindDN: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_DN_REQUIRED),
bindPass: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.BIND_PASSWORD_REQUIRED),
searchBase: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.LDAP.SEARCH_BASE_REQUIRED),
ldapCaCertificate: z.string().trim().optional()
});
export const registerIdentityTemplateRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Create identity auth template",
security: [
{
bearerAuth: []
}
],
body: z.object({
name: z
.string()
.trim()
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH),
authMethod: z.nativeEnum(IdentityAuthTemplateMethod),
templateFields: ldapTemplateFieldsSchema
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: z.record(z.string(), z.unknown())
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.createTemplate({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
name: req.body.name,
authMethod: req.body.authMethod,
templateFields: req.body.templateFields
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return template;
}
});
server.route({
method: "PATCH",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Update identity auth template",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
body: z.object({
name: z
.string()
.trim()
.min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_REQUIRED)
.max(64, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_NAME_MAX_LENGTH)
.optional(),
templateFields: ldapTemplateFieldsSchema.partial().optional()
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: z.record(z.string(), z.unknown())
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.updateTemplate({
templateId: req.params.templateId,
name: req.body.name,
templateFields: req.body.templateFields,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return template;
}
});
server.route({
method: "DELETE",
url: "/:templateId",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Delete identity auth template",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
response: {
200: z.object({
message: z.string()
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.deleteTemplate({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
orgId: req.permission.orgId,
event: {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE,
metadata: {
templateId: template.id,
name: template.name
}
}
});
return { message: TEMPLATE_SUCCESS_MESSAGES.DELETED };
}
});
server.route({
method: "GET",
url: "/:templateId",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get identity auth template by ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string().min(1, TEMPLATE_VALIDATION_MESSAGES.TEMPLATE_ID_REQUIRED)
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
})
}
},
handler: async (req) => {
const template = await server.services.identityAuthTemplate.getTemplate({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return template;
}
});
server.route({
method: "GET",
url: "/search",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "List identity auth templates",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
limit: z.coerce.number().positive().max(100).default(5).optional(),
offset: z.coerce.number().min(0).default(0).optional(),
search: z.string().optional()
}),
response: {
200: z.object({
templates: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
}).array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { templates, totalCount } = await server.services.identityAuthTemplate.listTemplates({
limit: req.query.limit,
offset: req.query.offset,
search: req.query.search,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return { templates, totalCount };
}
});
server.route({
method: "GET",
url: "/",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get identity auth templates by authentication method",
security: [
{
bearerAuth: []
}
],
querystring: z.object({
authMethod: z.nativeEnum(IdentityAuthTemplateMethod)
}),
response: {
200: IdentityAuthTemplatesSchema.extend({
templateFields: ldapTemplateFieldsSchema
}).array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.getTemplatesByAuthMethod({
authMethod: req.query.authMethod,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
server.route({
method: "GET",
url: "/:templateId/usage",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Get template usage by template ID",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string()
}),
response: {
200: z
.object({
identityId: z.string(),
identityName: z.string()
})
.array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.findTemplateUsages({
templateId: req.params.templateId,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
server.route({
method: "POST",
url: "/:templateId/delete-usage",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
hide: false,
description: "Unlink identity auth template usage",
security: [
{
bearerAuth: []
}
],
params: z.object({
templateId: z.string()
}),
body: z.object({
identityIds: z.string().array()
}),
response: {
200: z
.object({
authId: z.string(),
identityId: z.string(),
identityName: z.string()
})
.array()
}
},
handler: async (req) => {
const templates = await server.services.identityAuthTemplate.unlinkTemplateUsage({
templateId: req.params.templateId,
identityIds: req.body.identityIds,
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId
});
return templates;
}
});
};
+2
View File
@@ -13,6 +13,7 @@ import { registerGatewayRouter } from "./gateway-router";
import { registerGithubOrgSyncRouter } from "./github-org-sync-router"; import { registerGithubOrgSyncRouter } from "./github-org-sync-router";
import { registerGroupRouter } from "./group-router"; import { registerGroupRouter } from "./group-router";
import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router"; import { registerIdentityProjectAdditionalPrivilegeRouter } from "./identity-project-additional-privilege-router";
import { registerIdentityTemplateRouter } from "./identity-template-router";
import { registerKmipRouter } from "./kmip-router"; import { registerKmipRouter } from "./kmip-router";
import { registerKmipSpecRouter } from "./kmip-spec-router"; import { registerKmipSpecRouter } from "./kmip-spec-router";
import { registerLdapRouter } from "./ldap-router"; import { registerLdapRouter } from "./ldap-router";
@@ -125,6 +126,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
await server.register(registerExternalKmsRouter, { await server.register(registerExternalKmsRouter, {
prefix: "/external-kms" prefix: "/external-kms"
}); });
await server.register(registerIdentityTemplateRouter, { prefix: "/identity-templates" });
await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" }); await server.register(registerProjectTemplateRouter, { prefix: "/project-templates" });
@@ -1,8 +1,10 @@
// weird commonjs-related error in the CI requires us to do the import like this // weird commonjs-related error in the CI requires us to do the import like this
import knex from "knex"; import knex from "knex";
import { v4 as uuidv4 } from "uuid";
import { TDbClient } from "@app/db"; import { TDbClient } from "@app/db";
import { TableName, TAuditLogs } from "@app/db/schemas"; import { TableName, TAuditLogs } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env";
import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors"; import { DatabaseError, GatewayTimeoutError } from "@app/lib/errors";
import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex"; import { ormify, selectAllTableCols, TOrmify } from "@app/lib/knex";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -150,43 +152,70 @@ export const auditLogDALFactory = (db: TDbClient) => {
// delete all audit log that have expired // delete all audit log that have expired
const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => { const pruneAuditLog: TAuditLogDALFactory["pruneAuditLog"] = async (tx) => {
const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000; const runPrune = async (dbClient: knex.Knex) => {
const MAX_RETRY_ON_FAILURE = 3; const AUDIT_LOG_PRUNE_BATCH_SIZE = 10000;
const MAX_RETRY_ON_FAILURE = 3;
const today = new Date(); const today = new Date();
let deletedAuditLogIds: { id: string }[] = []; let deletedAuditLogIds: { id: string }[] = [];
let numberOfRetryOnFailure = 0; let numberOfRetryOnFailure = 0;
let isRetrying = false; let isRetrying = false;
logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`); logger.info(`${QueueName.DailyResourceCleanUp}: audit log started`);
do { do {
try { try {
const findExpiredLogSubQuery = (tx || db)(TableName.AuditLog) const findExpiredLogSubQuery = dbClient(TableName.AuditLog)
.where("expiresAt", "<", today) .where("expiresAt", "<", today)
.where("createdAt", "<", today) // to use audit log partition .where("createdAt", "<", today) // to use audit log partition
.orderBy(`${TableName.AuditLog}.createdAt`, "desc") .orderBy(`${TableName.AuditLog}.createdAt`, "desc")
.select("id") .select("id")
.limit(AUDIT_LOG_PRUNE_BATCH_SIZE); .limit(AUDIT_LOG_PRUNE_BATCH_SIZE);
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
deletedAuditLogIds = await (tx || db)(TableName.AuditLog) deletedAuditLogIds = await dbClient(TableName.AuditLog)
.whereIn("id", findExpiredLogSubQuery) .whereIn("id", findExpiredLogSubQuery)
.del() .del()
.returning("id"); .returning("id");
numberOfRetryOnFailure = 0; // reset numberOfRetryOnFailure = 0; // reset
} catch (error) { } catch (error) {
numberOfRetryOnFailure += 1; numberOfRetryOnFailure += 1;
logger.error(error, "Failed to delete audit log on pruning"); logger.error(error, "Failed to delete audit log on pruning");
} finally { } finally {
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
await new Promise((resolve) => { await new Promise((resolve) => {
setTimeout(resolve, 10); // time to breathe for db setTimeout(resolve, 10); // time to breathe for db
}); });
} }
isRetrying = numberOfRetryOnFailure > 0; isRetrying = numberOfRetryOnFailure > 0;
} while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE)); } while (deletedAuditLogIds.length > 0 || (isRetrying && numberOfRetryOnFailure < MAX_RETRY_ON_FAILURE));
logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`); logger.info(`${QueueName.DailyResourceCleanUp}: audit log completed`);
};
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 { ...auditLogOrm, pruneAuditLog, find }; const create: TAuditLogDALFactory["create"] = async (tx) => {
const config = getConfig();
if (config.DISABLE_AUDIT_LOG_STORAGE) {
return {
...tx,
id: uuidv4(),
createdAt: new Date(),
updatedAt: new Date()
};
}
return auditLogOrm.create(tx);
};
return { ...auditLogOrm, create, pruneAuditLog, find };
}; };
@@ -161,6 +161,9 @@ export enum EventType {
CREATE_IDENTITY = "create-identity", CREATE_IDENTITY = "create-identity",
UPDATE_IDENTITY = "update-identity", UPDATE_IDENTITY = "update-identity",
DELETE_IDENTITY = "delete-identity", DELETE_IDENTITY = "delete-identity",
MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE = "machine-identity-auth-template-create",
MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE = "machine-identity-auth-template-update",
MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE = "machine-identity-auth-template-delete",
LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth", LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth", ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth", UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
@@ -830,6 +833,30 @@ interface LoginIdentityUniversalAuthEvent {
}; };
} }
interface MachineIdentityAuthTemplateCreateEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_CREATE;
metadata: {
templateId: string;
name: string;
};
}
interface MachineIdentityAuthTemplateUpdateEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_UPDATE;
metadata: {
templateId: string;
name: string;
};
}
interface MachineIdentityAuthTemplateDeleteEvent {
type: EventType.MACHINE_IDENTITY_AUTH_TEMPLATE_DELETE;
metadata: {
templateId: string;
name: string;
};
}
interface AddIdentityUniversalAuthEvent { interface AddIdentityUniversalAuthEvent {
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH; type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
metadata: { metadata: {
@@ -1325,6 +1352,7 @@ interface AddIdentityLdapAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>; accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[]; allowedFields?: TAllowedFields[];
url: string; url: string;
templateId?: string | null;
}; };
} }
@@ -1338,6 +1366,7 @@ interface UpdateIdentityLdapAuthEvent {
accessTokenTrustedIps?: Array<TIdentityTrustedIp>; accessTokenTrustedIps?: Array<TIdentityTrustedIp>;
allowedFields?: TAllowedFields[]; allowedFields?: TAllowedFields[];
url?: string; url?: string;
templateId?: string | null;
}; };
} }
@@ -3439,6 +3468,9 @@ export type Event =
| UpdateIdentityEvent | UpdateIdentityEvent
| DeleteIdentityEvent | DeleteIdentityEvent
| LoginIdentityUniversalAuthEvent | LoginIdentityUniversalAuthEvent
| MachineIdentityAuthTemplateCreateEvent
| MachineIdentityAuthTemplateUpdateEvent
| MachineIdentityAuthTemplateDeleteEvent
| AddIdentityUniversalAuthEvent | AddIdentityUniversalAuthEvent
| UpdateIdentityUniversalAuthEvent | UpdateIdentityUniversalAuthEvent
| DeleteIdentityUniversalAuthEvent | DeleteIdentityUniversalAuthEvent
+25 -1
View File
@@ -1,6 +1,6 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas"; import { ProjectVersion, SecretKeyEncoding, TableName, TUsers } from "@app/db/schemas";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError, ScimRequestError } from "@app/lib/errors";
@@ -65,6 +65,18 @@ const addAcceptedUsersToGroup = async ({
const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`)); const userKeysSet = new Set(keys.map((k) => `${k.projectId}-${k.receiverId}`));
for await (const projectId of projectIds) { for await (const projectId of projectIds) {
const project = await projectDAL.findById(projectId, tx);
if (!project) {
throw new NotFoundError({
message: `Failed to find project with ID '${projectId}'`
});
}
if (project.version !== ProjectVersion.V1 && project.version !== ProjectVersion.V2) {
// eslint-disable-next-line no-continue
continue;
}
const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`)); const usersToAddProjectKeyFor = users.filter((u) => !userKeysSet.has(`${projectId}-${u.userId}`));
if (usersToAddProjectKeyFor.length) { if (usersToAddProjectKeyFor.length) {
@@ -86,6 +98,12 @@ const addAcceptedUsersToGroup = async ({
}); });
} }
if (!ghostUserLatestKey.sender.publicKey) {
throw new NotFoundError({
message: `Failed to find project owner's public key in project with ID '${projectId}'`
});
}
const bot = await projectBotDAL.findOne({ projectId }, tx); const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) { if (!bot) {
@@ -112,6 +130,12 @@ const addAcceptedUsersToGroup = async ({
}); });
const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => { const projectKeysToAdd = usersToAddProjectKeyFor.map((user) => {
if (!user.publicKey) {
throw new NotFoundError({
message: `Failed to find user's public key in project with ID '${projectId}'`
});
}
const { ciphertext: encryptedKey, nonce } = crypto const { ciphertext: encryptedKey, nonce } = crypto
.encryption() .encryption()
.asymmetric() .asymmetric()
@@ -41,7 +41,7 @@ type TGroupServiceFactoryDep = {
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
"findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find" "findOne" | "delete" | "filterProjectsByUserMembership" | "transaction" | "insertMany" | "find"
>; >;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "delete" | "findLatestProjectKey" | "insertMany">;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission" | "getOrgPermissionByRole">;
+3 -3
View File
@@ -65,7 +65,7 @@ export type TAddUsersToGroup = {
userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">; userGroupMembershipDAL: Pick<TUserGroupMembershipDALFactory, "find" | "transaction" | "insertMany">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx: Knex; tx: Knex;
}; };
@@ -78,7 +78,7 @@ export type TAddUsersToGroupByUserIds = {
orgDAL: Pick<TOrgDALFactory, "findMembership">; orgDAL: Pick<TOrgDALFactory, "findMembership">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex; tx?: Knex;
}; };
@@ -102,7 +102,7 @@ export type TConvertPendingGroupAdditionsToGroupMemberships = {
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
tx?: Knex; tx?: Knex;
}; };
@@ -0,0 +1,83 @@
/* eslint-disable no-case-declarations */
import { Knex } from "knex";
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { buildFindFilter, ormify } from "@app/lib/knex";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
export type TIdentityAuthTemplateDALFactory = ReturnType<typeof identityAuthTemplateDALFactory>;
export const identityAuthTemplateDALFactory = (db: TDbClient) => {
const identityAuthTemplateOrm = ormify(db, TableName.IdentityAuthTemplate);
const findByOrgId = async (
orgId: string,
{ limit, offset, search, tx }: { limit?: number; offset?: number; search?: string; tx?: Knex } = {}
) => {
let query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
let countQuery = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ orgId });
if (search) {
const searchFilter = `%${search.toLowerCase()}%`;
query = query.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
countQuery = countQuery.whereRaw("LOWER(name) LIKE ?", [searchFilter]);
}
query = query.orderBy("createdAt", "desc");
if (limit !== undefined) {
query = query.limit(limit);
}
if (offset !== undefined) {
query = query.offset(offset);
}
const docs = await query;
const [{ count }] = (await countQuery.count("* as count")) as [{ count: string | number }];
return { docs, totalCount: Number(count) };
};
const findByAuthMethod = async (authMethod: string, orgId: string, tx?: Knex) => {
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate)
.where({ authMethod, orgId })
.orderBy("createdAt", "desc");
const docs = await query;
return docs;
};
const findTemplateUsages = async (templateId: string, authMethod: string, tx?: Knex) => {
switch (authMethod) {
case IdentityAuthTemplateMethod.LDAP:
const query = (tx || db.replicaNode())(TableName.IdentityLdapAuth)
.join(TableName.Identity, `${TableName.IdentityLdapAuth}.identityId`, `${TableName.Identity}.id`)
// eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter({ templateId }, TableName.IdentityLdapAuth))
.select(
db.ref("identityId").withSchema(TableName.IdentityLdapAuth),
db.ref("name").withSchema(TableName.Identity).as("identityName")
);
const docs = await query;
return docs;
default:
return [];
}
};
const findByIdAndOrgId = async (id: string, orgId: string, tx?: Knex) => {
const query = (tx || db.replicaNode())(TableName.IdentityAuthTemplate).where({ id, orgId });
const doc = await query;
return doc?.[0];
};
return {
...identityAuthTemplateOrm,
findByOrgId,
findByAuthMethod,
findTemplateUsages,
findByIdAndOrgId
};
};
@@ -0,0 +1,22 @@
export enum IdentityAuthTemplateMethod {
LDAP = "ldap"
}
export const TEMPLATE_VALIDATION_MESSAGES = {
TEMPLATE_NAME_REQUIRED: "Template name is required",
TEMPLATE_NAME_MAX_LENGTH: "Template name must be at most 64 characters long",
AUTH_METHOD_REQUIRED: "Auth method is required",
TEMPLATE_ID_REQUIRED: "Template ID is required",
LDAP: {
URL_REQUIRED: "LDAP URL is required",
BIND_DN_REQUIRED: "Bind DN is required",
BIND_PASSWORD_REQUIRED: "Bind password is required",
SEARCH_BASE_REQUIRED: "Search base is required"
}
} as const;
export const TEMPLATE_SUCCESS_MESSAGES = {
CREATED: "Template created successfully",
UPDATED: "Template updated successfully",
DELETED: "Template deleted successfully"
} as const;
@@ -0,0 +1,454 @@
import { ForbiddenError } from "@casl/ability";
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import {
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TOrgPermission } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TIdentityLdapAuthDALFactory } from "@app/services/identity-ldap-auth/identity-ldap-auth-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
import {
TDeleteIdentityAuthTemplateDTO,
TFindTemplateUsagesDTO,
TGetIdentityAuthTemplateDTO,
TGetTemplatesByAuthMethodDTO,
TLdapTemplateFields,
TListIdentityAuthTemplatesDTO,
TUnlinkTemplateUsageDTO
} from "./identity-auth-template-types";
type TIdentityAuthTemplateServiceFactoryDep = {
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
identityLdapAuthDAL: TIdentityLdapAuthDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey" | "encryptWithInputKey" | "decryptWithInputKey">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
};
export type TIdentityAuthTemplateServiceFactory = ReturnType<typeof identityAuthTemplateServiceFactory>;
export const identityAuthTemplateServiceFactory = ({
identityAuthTemplateDAL,
identityLdapAuthDAL,
permissionService,
kmsService,
licenseService,
auditLogService
}: TIdentityAuthTemplateServiceFactoryDep) => {
// Plan check
const $checkPlan = async (orgId: string) => {
const plan = await licenseService.getPlan(orgId);
if (!plan.machineIdentityAuthTemplates)
throw new BadRequestError({
message:
"Failed to use identity auth template due to plan restriction. Upgrade plan to access machine identity auth templates."
});
};
const createTemplate = async ({
name,
authMethod,
templateFields,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: {
name: string;
authMethod: string;
templateFields: Record<string, unknown>;
} & Omit<TOrgPermission, "orgId">) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
const template = await identityAuthTemplateDAL.create({
name,
authMethod,
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob,
orgId: actorOrgId
});
return { ...template, templateFields };
};
const updateTemplate = async ({
templateId,
name,
templateFields,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: {
templateId: string;
name?: string;
templateFields?: Record<string, unknown>;
} & Omit<TOrgPermission, "orgId">) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
let finalTemplateFields: Record<string, unknown> = {};
const updatedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
const authTemplate = await identityAuthTemplateDAL.updateById(
templateId,
{
name,
...(templateFields && {
templateFields: encryptor({ plainText: Buffer.from(JSON.stringify(templateFields)) }).cipherTextBlob
})
},
tx
);
if (templateFields && template.authMethod === IdentityAuthTemplateMethod.LDAP) {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
const currentTemplateFields = JSON.parse(
decryptor({ cipherTextBlob: template.templateFields }).toString()
) as TLdapTemplateFields;
const mergedTemplateFields: TLdapTemplateFields = { ...currentTemplateFields, ...templateFields };
finalTemplateFields = mergedTemplateFields;
const ldapUpdateData: {
url?: string;
searchBase?: string;
encryptedBindDN?: Buffer;
encryptedBindPass?: Buffer;
encryptedLdapCaCertificate?: Buffer;
} = {};
if ("url" in templateFields) {
ldapUpdateData.url = mergedTemplateFields.url;
}
if ("searchBase" in templateFields) {
ldapUpdateData.searchBase = mergedTemplateFields.searchBase;
}
if ("bindDN" in templateFields) {
ldapUpdateData.encryptedBindDN = encryptor({
plainText: Buffer.from(mergedTemplateFields.bindDN)
}).cipherTextBlob;
}
if ("bindPass" in templateFields) {
ldapUpdateData.encryptedBindPass = encryptor({
plainText: Buffer.from(mergedTemplateFields.bindPass)
}).cipherTextBlob;
}
if ("ldapCaCertificate" in templateFields) {
ldapUpdateData.encryptedLdapCaCertificate = encryptor({
plainText: Buffer.from(mergedTemplateFields.ldapCaCertificate || "")
}).cipherTextBlob;
}
if (Object.keys(ldapUpdateData).length > 0) {
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, ldapUpdateData, tx);
await Promise.all(
updatedLdapAuths.map(async (updatedLdapAuth) => {
await auditLogService.createAuditLog({
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
orgId: actorOrgId,
event: {
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
metadata: {
identityId: updatedLdapAuth.identityId,
templateId: template.id
}
}
});
})
);
}
}
return authTemplate;
});
return { ...updatedTemplate, templateFields: finalTemplateFields };
};
const deleteTemplate = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TDeleteIdentityAuthTemplateDTO) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const deletedTemplate = await identityAuthTemplateDAL.transaction(async (tx) => {
// Remove template reference from identityLdapAuth records
const updatedLdapAuths = await identityLdapAuthDAL.update({ templateId }, { templateId: null }, tx);
await Promise.all(
updatedLdapAuths.map(async (updatedLdapAuth) => {
await auditLogService.createAuditLog({
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
orgId: actorOrgId,
event: {
type: EventType.UPDATE_IDENTITY_LDAP_AUTH,
metadata: {
identityId: updatedLdapAuth.identityId,
templateId: template.id
}
}
});
})
);
// Delete the template
const [deletedTpl] = await identityAuthTemplateDAL.delete({ id: templateId }, tx);
return deletedTpl;
});
return deletedTemplate;
};
const getTemplate = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetIdentityAuthTemplateDTO) => {
await $checkPlan(actorOrgId);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
template.orgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: template.orgId
});
const decryptedTemplateFields = decryptor({ cipherTextBlob: template.templateFields }).toString();
return {
...template,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptedTemplateFields)
};
};
const listTemplates = async ({
limit,
offset,
search,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TListIdentityAuthTemplatesDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const { docs, totalCount } = await identityAuthTemplateDAL.findByOrgId(actorOrgId, { limit, offset, search });
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
return {
totalCount,
templates: docs.map((doc) => ({
...doc,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
}))
};
};
const getTemplatesByAuthMethod = async ({
authMethod,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TGetTemplatesByAuthMethodDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const docs = await identityAuthTemplateDAL.findByAuthMethod(authMethod, actorOrgId);
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: actorOrgId
});
return docs.map((doc) => ({
...doc,
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
templateFields: JSON.parse(decryptor({ cipherTextBlob: doc.templateFields }).toString())
}));
};
const findTemplateUsages = async ({
templateId,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TFindTemplateUsagesDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
const docs = await identityAuthTemplateDAL.findTemplateUsages(templateId, template.authMethod);
return docs;
};
const unlinkTemplateUsage = async ({
templateId,
identityIds,
actorId,
actorAuthMethod,
actor,
actorOrgId
}: TUnlinkTemplateUsageDTO) => {
await $checkPlan(actorOrgId);
const { permission } = await permissionService.getOrgPermission(
actor,
actorId,
actorOrgId,
actorAuthMethod,
actorOrgId
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
const template = await identityAuthTemplateDAL.findByIdAndOrgId(templateId, actorOrgId);
if (!template) {
throw new NotFoundError({ message: "Template not found" });
}
switch (template.authMethod) {
case IdentityAuthTemplateMethod.LDAP:
await identityLdapAuthDAL.update({ $in: { identityId: identityIds }, templateId }, { templateId: null });
break;
default:
break;
}
};
return {
createTemplate,
updateTemplate,
deleteTemplate,
getTemplate,
listTemplates,
getTemplatesByAuthMethod,
findTemplateUsages,
unlinkTemplateUsage
};
};
@@ -0,0 +1,61 @@
import { TProjectPermission } from "@app/lib/types";
import { IdentityAuthTemplateMethod } from "./identity-auth-template-enums";
// Method-specific template field types
export type TLdapTemplateFields = {
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
ldapCaCertificate?: string;
};
// Union type for all template field types
export type TTemplateFieldsByMethod = {
[IdentityAuthTemplateMethod.LDAP]: TLdapTemplateFields;
};
// Generic base types that use conditional types for type safety
export type TCreateIdentityAuthTemplateDTO = {
name: string;
authMethod: IdentityAuthTemplateMethod;
templateFields: TTemplateFieldsByMethod[IdentityAuthTemplateMethod];
} & Omit<TProjectPermission, "projectId">;
export type TUpdateIdentityAuthTemplateDTO = {
templateId: string;
name?: string;
templateFields?: Partial<TTemplateFieldsByMethod[IdentityAuthTemplateMethod]>;
} & Omit<TProjectPermission, "projectId">;
export type TDeleteIdentityAuthTemplateDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetIdentityAuthTemplateDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TListIdentityAuthTemplatesDTO = {
limit?: number;
offset?: number;
search?: string;
} & Omit<TProjectPermission, "projectId">;
export type TGetTemplatesByAuthMethodDTO = {
authMethod: string;
} & Omit<TProjectPermission, "projectId">;
export type TFindTemplateUsagesDTO = {
templateId: string;
} & Omit<TProjectPermission, "projectId">;
export type TUnlinkTemplateUsageDTO = {
templateId: string;
identityIds: string[];
} & Omit<TProjectPermission, "projectId">;
// Specific LDAP types for convenience
export type TCreateLdapTemplateDTO = TCreateIdentityAuthTemplateDTO;
export type TUpdateLdapTemplateDTO = TUpdateIdentityAuthTemplateDTO;
@@ -0,0 +1,6 @@
export type { TIdentityAuthTemplateDALFactory } from "./identity-auth-template-dal";
export { identityAuthTemplateDALFactory } from "./identity-auth-template-dal";
export * from "./identity-auth-template-enums";
export type { TIdentityAuthTemplateServiceFactory } from "./identity-auth-template-service";
export { identityAuthTemplateServiceFactory } from "./identity-auth-template-service";
export type * from "./identity-auth-template-types";
@@ -55,7 +55,7 @@ type TLdapConfigServiceFactoryDep = {
groupDAL: Pick<TGroupDALFactory, "find" | "findOne">; groupDAL: Pick<TGroupDALFactory, "find" | "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userGroupMembershipDAL: Pick< userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory, TUserGroupMembershipDALFactory,
@@ -31,7 +31,8 @@ export const getDefaultOnPremFeatures = () => {
caCrl: false, caCrl: false,
sshHostGroups: false, sshHostGroups: false,
enterpriseSecretSyncs: false, enterpriseSecretSyncs: false,
enterpriseAppConnections: false enterpriseAppConnections: false,
machineIdentityAuthTemplates: false
}; };
}; };
@@ -60,7 +60,8 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
enterpriseSecretSyncs: false, enterpriseSecretSyncs: false,
enterpriseAppConnections: false, enterpriseAppConnections: false,
fips: false, fips: false,
eventSubscriptions: false eventSubscriptions: false,
machineIdentityAuthTemplates: false
}); });
export const setupLicenseRequestWithStore = ( export const setupLicenseRequestWithStore = (
@@ -75,6 +75,7 @@ export type TFeatureSet = {
secretScanning: false; secretScanning: false;
enterpriseSecretSyncs: false; enterpriseSecretSyncs: false;
enterpriseAppConnections: false; enterpriseAppConnections: false;
machineIdentityAuthTemplates: false;
fips: false; fips: false;
eventSubscriptions: false; eventSubscriptions: false;
}; };
@@ -79,7 +79,7 @@ type TOidcConfigServiceFactoryDep = {
>; >;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany" | "delete">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">; auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">; kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
@@ -28,6 +28,15 @@ export enum OrgPermissionKmipActions {
Setup = "setup" Setup = "setup"
} }
export enum OrgPermissionMachineIdentityAuthTemplateActions {
ListTemplates = "list-templates",
EditTemplates = "edit-templates",
CreateTemplates = "create-templates",
DeleteTemplates = "delete-templates",
UnlinkTemplates = "unlink-templates",
AttachTemplates = "attach-templates"
}
export enum OrgPermissionAdminConsoleAction { export enum OrgPermissionAdminConsoleAction {
AccessAllProjects = "access-all-projects" AccessAllProjects = "access-all-projects"
} }
@@ -88,6 +97,7 @@ export enum OrgPermissionSubjects {
Identity = "identity", Identity = "identity",
Kms = "kms", Kms = "kms",
AdminConsole = "organization-admin-console", AdminConsole = "organization-admin-console",
MachineIdentityAuthTemplate = "machine-identity-auth-template",
AuditLogs = "audit-logs", AuditLogs = "audit-logs",
ProjectTemplates = "project-templates", ProjectTemplates = "project-templates",
AppConnections = "app-connections", AppConnections = "app-connections",
@@ -126,6 +136,7 @@ export type OrgPermissionSet =
) )
] ]
| [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole] | [OrgPermissionAdminConsoleAction, OrgPermissionSubjects.AdminConsole]
| [OrgPermissionMachineIdentityAuthTemplateActions, OrgPermissionSubjects.MachineIdentityAuthTemplate]
| [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip] | [OrgPermissionKmipActions, OrgPermissionSubjects.Kmip]
| [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare]; | [OrgPermissionSecretShareAction, OrgPermissionSubjects.SecretShare];
@@ -237,6 +248,14 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
"Describe what action an entity can take." "Describe what action an entity can take."
) )
}), }),
z.object({
subject: z
.literal(OrgPermissionSubjects.MachineIdentityAuthTemplate)
.describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionMachineIdentityAuthTemplateActions).describe(
"Describe what action an entity can take."
)
}),
z.object({ z.object({
subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."), subject: z.literal(OrgPermissionSubjects.Gateway).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe( action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
@@ -350,6 +369,25 @@ const buildAdminPermission = () => {
// the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI // the proxy assignment is temporary in order to prevent "more privilege" error during role assignment to MI
can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip); can(OrgPermissionKmipActions.Proxy, OrgPermissionSubjects.Kmip);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(OrgPermissionMachineIdentityAuthTemplateActions.EditTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(
OrgPermissionMachineIdentityAuthTemplateActions.CreateTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.DeleteTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare); can(OrgPermissionSecretShareAction.ManageSettings, OrgPermissionSubjects.SecretShare);
return rules; return rules;
@@ -385,6 +423,16 @@ const buildMemberPermission = () => {
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway); can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
can(
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
can(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
return rules; return rules;
}; };
+1 -1
View File
@@ -59,7 +59,7 @@ type TScimServiceFactoryDep = {
TOrgMembershipDALFactory, TOrgMembershipDALFactory,
"find" | "findOne" | "create" | "updateById" | "findById" | "update" "find" | "findOne" | "create" | "updateById" | "findById" | "update"
>; >;
projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser">; projectDAL: Pick<TProjectDALFactory, "find" | "findProjectGhostUser" | "findById">;
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">; projectMembershipDAL: Pick<TProjectMembershipDALFactory, "find" | "delete" | "findProjectMembershipsByUserId">;
groupDAL: Pick< groupDAL: Pick<
TGroupDALFactory, TGroupDALFactory,
@@ -49,6 +49,7 @@ const baseSecretScanningDataSourceQuery = ({
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"), db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db db
@@ -82,6 +83,7 @@ const expandSecretScanningDataSource = <
connectionUpdatedAt, connectionUpdatedAt,
connectionVersion, connectionVersion,
connectionIsPlatformManagedCredentials, connectionIsPlatformManagedCredentials,
connectionGatewayId,
...el ...el
} = dataSource; } = dataSource;
@@ -100,7 +102,8 @@ const expandSecretScanningDataSource = <
createdAt: connectionCreatedAt, createdAt: connectionCreatedAt,
updatedAt: connectionUpdatedAt, updatedAt: connectionUpdatedAt,
version: connectionVersion, version: connectionVersion,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
gatewayId: connectionGatewayId
} }
: undefined : undefined
}; };
+4 -1
View File
@@ -18,6 +18,7 @@ import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "@app/services/
export enum ApiDocsTags { export enum ApiDocsTags {
Identities = "Identities", Identities = "Identities",
IdentityTemplates = "Identity Templates",
TokenAuth = "Token Auth", TokenAuth = "Token Auth",
UniversalAuth = "Universal Auth", UniversalAuth = "Universal Auth",
GcpAuth = "GCP Auth", GcpAuth = "GCP Auth",
@@ -215,6 +216,7 @@ export const LDAP_AUTH = {
password: "The password of the LDAP user to login." password: "The password of the LDAP user to login."
}, },
ATTACH: { ATTACH: {
templateId: "The ID of the identity auth template to attach the configuration onto.",
identityId: "The ID of the identity to attach the configuration onto.", identityId: "The ID of the identity to attach the configuration onto.",
url: "The URL of the LDAP server.", url: "The URL of the LDAP server.",
allowedFields: allowedFields:
@@ -241,7 +243,8 @@ export const LDAP_AUTH = {
accessTokenTTL: "The new lifetime for an access token in seconds.", accessTokenTTL: "The new lifetime for an access token in seconds.",
accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.", accessTokenMaxTTL: "The new maximum lifetime for an access token in seconds.",
accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.", accessTokenNumUsesLimit: "The new maximum number of times that an access token can be used.",
accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from." accessTokenTrustedIps: "The new IPs or CIDR ranges that access tokens can be used from.",
templateId: "The ID of the identity auth template to update the configuration to."
}, },
RETRIEVE: { RETRIEVE: {
identityId: "The ID of the identity to retrieve the configuration for." identityId: "The ID of the identity to retrieve the configuration for."
+10
View File
@@ -59,6 +59,7 @@ const envSchema = z
AUDIT_LOGS_DB_ROOT_CERT: zpStr( AUDIT_LOGS_DB_ROOT_CERT: zpStr(
z.string().describe("Postgres database base64-encoded CA cert for Audit logs").optional() z.string().describe("Postgres database base64-encoded CA cert for Audit logs").optional()
), ),
DISABLE_AUDIT_LOG_STORAGE: zodStrBool.default("false").optional().describe("Disable audit log storage"),
MAX_LEASE_LIMIT: z.coerce.number().default(10000), MAX_LEASE_LIMIT: z.coerce.number().default(10000),
DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()), DB_ROOT_CERT: zpStr(z.string().describe("Postgres database base64-encoded CA cert").optional()),
DB_HOST: zpStr(z.string().describe("Postgres database host").optional()), DB_HOST: zpStr(z.string().describe("Postgres database host").optional()),
@@ -482,6 +483,15 @@ export const overwriteSchema: {
fields: { key: keyof TEnvConfig; description?: string }[]; fields: { key: keyof TEnvConfig; description?: string }[];
}; };
} = { } = {
auditLogs: {
name: "Audit Logs",
fields: [
{
key: "DISABLE_AUDIT_LOG_STORAGE",
description: "Disable audit log storage"
}
]
},
aws: { aws: {
name: "AWS", name: "AWS",
fields: [ fields: [
+17 -1
View File
@@ -53,7 +53,7 @@ type DecryptedIntegrationAuths = z.infer<typeof DecryptedIntegrationAuthsSchema>
type TLatestKey = TProjectKeys & { type TLatestKey = TProjectKeys & {
sender: { sender: {
publicKey: string; publicKey?: string;
}; };
}; };
@@ -91,6 +91,10 @@ const getDecryptedValues = (data: Array<{ ciphertext: string; iv: string; tag: s
return results; return results;
}; };
export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => { export const decryptSecrets = (encryptedSecrets: TSecrets[], privateKey: string, latestKey: TLatestKey) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -143,6 +147,10 @@ export const decryptSecretVersions = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -195,6 +203,10 @@ export const decryptSecretApprovals = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
@@ -247,6 +259,10 @@ export const decryptIntegrationAuths = (
privateKey: string, privateKey: string,
latestKey: TLatestKey latestKey: TLatestKey
) => { ) => {
if (!latestKey.sender.publicKey) {
throw new Error("Latest key sender public key not found");
}
const key = crypto.encryption().asymmetric().decrypt({ const key = crypto.encryption().asymmetric().decrypt({
ciphertext: latestKey.encryptedKey, ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce, nonce: latestKey.nonce,
+18
View File
@@ -4,6 +4,7 @@ import jsrp from "jsrp";
import { TUserEncryptionKeys } from "@app/db/schemas"; import { TUserEncryptionKeys } from "@app/db/schemas";
import { UserEncryption } from "@app/services/user/user-types"; import { UserEncryption } from "@app/services/user/user-types";
import { BadRequestError } from "../errors";
import { crypto, SymmetricKeySize } from "./cryptography"; import { crypto, SymmetricKeySize } from "./cryptography";
export const generateSrpServerKey = async (salt: string, verifier: string) => { export const generateSrpServerKey = async (salt: string, verifier: string) => {
@@ -127,6 +128,10 @@ export const getUserPrivateKey = async (
> >
) => { ) => {
if (user.encryptionVersion === UserEncryption.V1) { if (user.encryptionVersion === UserEncryption.V1) {
if (!user.encryptedPrivateKey || !user.iv || !user.tag || !user.salt) {
throw new BadRequestError({ message: "User encrypted private key not found" });
}
return crypto return crypto
.encryption() .encryption()
.symmetric() .symmetric()
@@ -138,12 +143,25 @@ export const getUserPrivateKey = async (
keySize: SymmetricKeySize.Bits128 keySize: SymmetricKeySize.Bits128
}); });
} }
// still used for legacy things
if ( if (
user.encryptionVersion === UserEncryption.V2 && user.encryptionVersion === UserEncryption.V2 &&
user.protectedKey && user.protectedKey &&
user.protectedKeyIV && user.protectedKeyIV &&
user.protectedKeyTag user.protectedKeyTag
) { ) {
if (
!user.salt ||
!user.protectedKey ||
!user.protectedKeyIV ||
!user.protectedKeyTag ||
!user.encryptedPrivateKey ||
!user.iv ||
!user.tag
) {
throw new BadRequestError({ message: "User encrypted private key not found" });
}
const derivedKey = await argon2.hash(password, { const derivedKey = await argon2.hash(password, {
salt: Buffer.from(user.salt), salt: Buffer.from(user.salt),
memoryCost: 65536, memoryCost: 65536,
+19 -8
View File
@@ -179,6 +179,8 @@ import { identityAccessTokenDALFactory } from "@app/services/identity-access-tok
import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service"; import { identityAccessTokenServiceFactory } from "@app/services/identity-access-token/identity-access-token-service";
import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal"; import { identityAliCloudAuthDALFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-dal";
import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service"; import { identityAliCloudAuthServiceFactory } from "@app/services/identity-alicloud-auth/identity-alicloud-auth-service";
import { identityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-dal";
import { identityAuthTemplateServiceFactory } from "@app/ee/services/identity-auth-template/identity-auth-template-service";
import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal"; import { identityAwsAuthDALFactory } from "@app/services/identity-aws-auth/identity-aws-auth-dal";
import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service"; import { identityAwsAuthServiceFactory } from "@app/services/identity-aws-auth/identity-aws-auth-service";
import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal"; import { identityAzureAuthDALFactory } from "@app/services/identity-azure-auth/identity-azure-auth-dal";
@@ -394,6 +396,7 @@ export const registerRoutes = async (
const identityProjectDAL = identityProjectDALFactory(db); const identityProjectDAL = identityProjectDALFactory(db);
const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db); const identityProjectMembershipRoleDAL = identityProjectMembershipRoleDALFactory(db);
const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db); const identityProjectAdditionalPrivilegeDAL = identityProjectAdditionalPrivilegeDALFactory(db);
const identityAuthTemplateDAL = identityAuthTemplateDALFactory(db);
const identityTokenAuthDAL = identityTokenAuthDALFactory(db); const identityTokenAuthDAL = identityTokenAuthDALFactory(db);
const identityUaDAL = identityUaDALFactory(db); const identityUaDAL = identityUaDALFactory(db);
@@ -772,7 +775,6 @@ export const registerRoutes = async (
orgRoleDAL, orgRoleDAL,
permissionService, permissionService,
orgDAL, orgDAL,
projectBotDAL,
incidentContactDAL, incidentContactDAL,
tokenService, tokenService,
projectUserAdditionalPrivilegeDAL, projectUserAdditionalPrivilegeDAL,
@@ -847,7 +849,6 @@ export const registerRoutes = async (
projectDAL, projectDAL,
permissionService, permissionService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
userDAL,
projectBotDAL, projectBotDAL,
projectKeyDAL, projectKeyDAL,
projectMembershipDAL projectMembershipDAL
@@ -1135,11 +1136,9 @@ export const registerRoutes = async (
projectBotService, projectBotService,
identityProjectDAL, identityProjectDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
projectKeyDAL,
userDAL, userDAL,
projectEnvDAL, projectEnvDAL,
orgDAL, orgDAL,
orgService,
projectMembershipDAL, projectMembershipDAL,
projectRoleDAL, projectRoleDAL,
folderDAL, folderDAL,
@@ -1159,7 +1158,6 @@ export const registerRoutes = async (
identityProjectMembershipRoleDAL, identityProjectMembershipRoleDAL,
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL,
certificateTemplateDAL, certificateTemplateDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
@@ -1461,6 +1459,15 @@ export const registerRoutes = async (
identityMetadataDAL identityMetadataDAL
}); });
const identityAuthTemplateService = identityAuthTemplateServiceFactory({
identityAuthTemplateDAL,
identityLdapAuthDAL,
permissionService,
kmsService,
licenseService,
auditLogService
});
const identityAccessTokenService = identityAccessTokenServiceFactory({ const identityAccessTokenService = identityAccessTokenServiceFactory({
identityAccessTokenDAL, identityAccessTokenDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
@@ -1604,7 +1611,8 @@ export const registerRoutes = async (
identityAccessTokenDAL, identityAccessTokenDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
licenseService, licenseService,
identityDAL identityDAL,
identityAuthTemplateDAL
}); });
const dynamicSecretProviders = buildDynamicSecretProviders({ const dynamicSecretProviders = buildDynamicSecretProviders({
@@ -2008,6 +2016,7 @@ export const registerRoutes = async (
webhook: webhookService, webhook: webhookService,
serviceToken: serviceTokenService, serviceToken: serviceTokenService,
identity: identityService, identity: identityService,
identityAuthTemplate: identityAuthTemplateService,
identityAccessToken: identityAccessTokenService, identityAccessToken: identityAccessTokenService,
identityProject: identityProjectService, identityProject: identityProjectService,
identityTokenAuth: identityTokenAuthService, identityTokenAuth: identityTokenAuthService,
@@ -2144,7 +2153,8 @@ export const registerRoutes = async (
inviteOnlySignup: z.boolean().optional(), inviteOnlySignup: z.boolean().optional(),
redisConfigured: z.boolean().optional(), redisConfigured: z.boolean().optional(),
secretScanningConfigured: z.boolean().optional(), secretScanningConfigured: z.boolean().optional(),
samlDefaultOrgSlug: z.string().optional() samlDefaultOrgSlug: z.string().optional(),
auditLogStorageDisabled: z.boolean().optional()
}) })
} }
}, },
@@ -2171,7 +2181,8 @@ export const registerRoutes = async (
inviteOnlySignup: Boolean(serverCfg.allowSignUp), inviteOnlySignup: Boolean(serverCfg.allowSignUp),
redisConfigured: cfg.isRedisConfigured, redisConfigured: cfg.isRedisConfigured,
secretScanningConfigured: cfg.isSecretScanningConfigured, secretScanningConfigured: cfg.isSecretScanningConfigured,
samlDefaultOrgSlug: cfg.samlDefaultOrgSlug samlDefaultOrgSlug: cfg.samlDefaultOrgSlug,
auditLogStorageDisabled: Boolean(cfg.DISABLE_AUDIT_LOG_STORAGE)
}; };
} }
}); });
@@ -464,6 +464,42 @@ export const registerAdminRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "DELETE",
url: "/user-management/users",
config: {
rateLimit: writeLimit
},
schema: {
body: z.object({
userIds: z.string().array()
}),
response: {
200: z.object({
users: UsersSchema.pick({
username: true,
firstName: true,
lastName: true,
email: true,
id: true
}).array()
})
}
},
onRequest: (req, res, done) => {
verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN])(req, res, () => {
verifySuperAdmin(req, res, done);
});
},
handler: async (req) => {
const users = await server.services.superAdmin.deleteUsers(req.body.userIds);
return {
users
};
}
});
server.route({ server.route({
method: "PATCH", method: "PATCH",
url: "/user-management/users/:userId/admin-access", url: "/user-management/users/:userId/admin-access",
@@ -200,49 +200,104 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
params: z.object({ params: z.object({
identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId) identityId: z.string().trim().describe(LDAP_AUTH.ATTACH.identityId)
}), }),
body: z body: z.union([
.object({ // Template-based configuration
url: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.url), z
bindDN: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindDN), .object({
bindPass: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.bindPass), templateId: z.string().trim().describe(LDAP_AUTH.ATTACH.templateId),
searchBase: z.string().trim().min(1).describe(LDAP_AUTH.ATTACH.searchBase), searchFilter: z
searchFilter: z .string()
.string() .trim()
.trim() .min(1)
.min(1) .default("(uid={{username}})")
.default("(uid={{username}})") .refine(isValidLdapFilter, "Invalid LDAP search filter")
.refine(isValidLdapFilter, "Invalid LDAP search filter") .describe(LDAP_AUTH.ATTACH.searchFilter),
.describe(LDAP_AUTH.ATTACH.searchFilter), allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields), ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate), accessTokenTrustedIps: z
accessTokenTrustedIps: z .object({
.object({ ipAddress: z.string().trim()
ipAddress: z.string().trim() })
}) .array()
.array() .min(1)
.min(1) .default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }]) .describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps), accessTokenTTL: z
accessTokenTTL: z .number()
.number() .int()
.int() .min(0)
.min(0) .max(315360000)
.max(315360000) .default(2592000)
.default(2592000) .describe(LDAP_AUTH.ATTACH.accessTokenTTL),
.describe(LDAP_AUTH.ATTACH.accessTokenTTL), accessTokenMaxTTL: z
accessTokenMaxTTL: z .number()
.number() .int()
.int() .min(1)
.min(1) .max(315360000)
.max(315360000) .default(2592000)
.default(2592000) .describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL), accessTokenNumUsesLimit: z
accessTokenNumUsesLimit: z.number().int().min(0).default(0).describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit) .number()
}) .int()
.refine( .min(0)
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL, .default(0)
"Access Token TTL cannot be greater than Access Token Max TTL." .describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
), })
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
),
// Manual configuration
z
.object({
url: z.string().trim().describe(LDAP_AUTH.ATTACH.url),
bindDN: z.string().trim().describe(LDAP_AUTH.ATTACH.bindDN),
bindPass: z.string().trim().describe(LDAP_AUTH.ATTACH.bindPass),
searchBase: z.string().trim().describe(LDAP_AUTH.ATTACH.searchBase),
searchFilter: z
.string()
.trim()
.min(1)
.default("(uid={{username}})")
.refine(isValidLdapFilter, "Invalid LDAP search filter")
.describe(LDAP_AUTH.ATTACH.searchFilter),
allowedFields: AllowedFieldsSchema.array().optional().describe(LDAP_AUTH.ATTACH.allowedFields),
ldapCaCertificate: z.string().trim().optional().describe(LDAP_AUTH.ATTACH.ldapCaCertificate),
accessTokenTrustedIps: z
.object({
ipAddress: z.string().trim()
})
.array()
.min(1)
.default([{ ipAddress: "0.0.0.0/0" }, { ipAddress: "::/0" }])
.describe(LDAP_AUTH.ATTACH.accessTokenTrustedIps),
accessTokenTTL: z
.number()
.int()
.min(0)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenTTL),
accessTokenMaxTTL: z
.number()
.int()
.min(1)
.max(315360000)
.default(2592000)
.describe(LDAP_AUTH.ATTACH.accessTokenMaxTTL),
accessTokenNumUsesLimit: z
.number()
.int()
.min(0)
.default(0)
.describe(LDAP_AUTH.ATTACH.accessTokenNumUsesLimit)
})
.refine(
(val) => val.accessTokenTTL <= val.accessTokenMaxTTL,
"Access Token TTL cannot be greater than Access Token Max TTL."
)
]),
response: { response: {
200: z.object({ 200: z.object({
identityLdapAuth: IdentityLdapAuthsSchema.omit({ identityLdapAuth: IdentityLdapAuthsSchema.omit({
@@ -275,7 +330,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL, accessTokenMaxTTL: identityLdapAuth.accessTokenMaxTTL,
accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
allowedFields: req.body.allowedFields allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId
} }
} }
}); });
@@ -309,6 +365,7 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
bindDN: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindDN), bindDN: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindDN),
bindPass: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindPass), bindPass: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.bindPass),
searchBase: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.searchBase), searchBase: z.string().trim().min(1).optional().describe(LDAP_AUTH.UPDATE.searchBase),
templateId: z.string().trim().optional().describe(LDAP_AUTH.UPDATE.templateId),
searchFilter: z searchFilter: z
.string() .string()
.trim() .trim()
@@ -376,7 +433,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
accessTokenTTL: identityLdapAuth.accessTokenTTL, accessTokenTTL: identityLdapAuth.accessTokenTTL,
accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit, accessTokenNumUsesLimit: identityLdapAuth.accessTokenNumUsesLimit,
accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[], accessTokenTrustedIps: identityLdapAuth.accessTokenTrustedIps as TIdentityTrustedIp[],
allowedFields: req.body.allowedFields allowedFields: req.body.allowedFields,
templateId: identityLdapAuth.templateId
} }
} }
}); });
@@ -413,7 +471,8 @@ export const registerIdentityLdapAuthRouter = async (server: FastifyZodProvider)
}).extend({ }).extend({
bindDN: z.string(), bindDN: z.string(),
bindPass: z.string(), bindPass: z.string(),
ldapCaCertificate: z.string().optional() ldapCaCertificate: z.string().optional(),
templateId: z.string().optional().nullable()
}) })
}) })
} }
@@ -247,7 +247,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
lastName: true, lastName: true,
id: true, id: true,
superAdmin: true superAdmin: true
}).merge(z.object({ publicKey: z.string().nullable() })) }).merge(z.object({ publicKey: z.string().nullable().optional() }))
}) })
) )
.omit({ createdAt: true, updatedAt: true }) .omit({ createdAt: true, updatedAt: true })
@@ -9,73 +9,6 @@ import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { UserEncryption } from "@app/services/user/user-types"; import { UserEncryption } from "@app/services/user/user-types";
export const registerPasswordRouter = async (server: FastifyZodProvider) => { export const registerPasswordRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
url: "/srp1",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
clientPublicKey: z.string().trim()
}),
response: {
200: z.object({
serverPublicKey: z.string(),
salt: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const { salt, serverPublicKey } = await server.services.password.generateServerPubKey(
req.permission.id,
req.body.clientPublicKey
);
return { salt, serverPublicKey };
}
});
server.route({
method: "POST",
url: "/change-password",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
clientProof: z.string().trim(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req, res) => {
const appCfg = getConfig();
await server.services.password.changePassword({ ...req.body, userId: req.permission.id });
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
return { message: "Successfully changed password" };
}
});
server.route({ server.route({
method: "POST", method: "POST",
url: "/email/password-reset", url: "/email/password-reset",
@@ -131,41 +64,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "POST",
url: "/backup-private-key",
config: {
rateLimit: authRateLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
clientProof: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
iv: z.string().trim(),
tag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim()
}),
response: {
200: z.object({
message: z.string(),
backupPrivateKey: BackupPrivateKeySchema.omit({ verifier: true })
})
}
},
handler: async (req) => {
const token = validateSignUpAuthorization(req.headers.authorization as string, "", false)!;
const backupPrivateKey = await server.services.password.createBackupPrivateKey({
...req.body,
userId: token.userId
});
if (!backupPrivateKey) throw new Error("Failed to create backup key");
return { message: "Successfully updated backup private key", backupPrivateKey };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/backup-private-key", url: "/backup-private-key",
@@ -257,14 +155,6 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
}, },
schema: { schema: {
body: z.object({ body: z.object({
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
password: z.string().trim(), password: z.string().trim(),
token: z.string().trim() token: z.string().trim()
}), }),
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: z.object({ 200: z.object({
publicKeys: z publicKeys: z
.object({ .object({
publicKey: z.string().optional(), publicKey: z.string().nullable().optional(),
userId: z.string() userId: z.string()
}) })
.array() .array()
@@ -22,6 +22,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) =
message: z.string().trim().max(1024).optional(), message: z.string().trim().max(1024).optional(),
repeatDays: z.number().min(1).nullable().optional(), repeatDays: z.number().min(1).nullable().optional(),
nextReminderDate: z.string().datetime().nullable().optional(), nextReminderDate: z.string().datetime().nullable().optional(),
fromDate: z.string().datetime().nullable().optional(),
recipients: z.string().array().optional() recipients: z.string().array().optional()
}) })
.refine((data) => { .refine((data) => {
@@ -45,6 +46,7 @@ export const registerSecretReminderRouter = async (server: FastifyZodProvider) =
message: req.body.message, message: req.body.message,
repeatDays: req.body.repeatDays, repeatDays: req.body.repeatDays,
nextReminderDate: req.body.nextReminderDate, nextReminderDate: req.body.nextReminderDate,
fromDate: req.body.fromDate,
recipients: req.body.recipients recipients: req.body.recipients
} }
}); });
+4 -38
View File
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { UserEncryptionKeysSchema, UsersSchema } from "@app/db/schemas"; import { UsersSchema } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter"; import { authRateLimit, readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -19,23 +19,9 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
schema: { schema: {
response: { response: {
200: z.object({ 200: z.object({
user: UsersSchema.merge( user: UsersSchema.extend({
UserEncryptionKeysSchema.pick({ encryptionVersion: z.number()
clientPublicKey: true, })
serverPrivateKey: true,
encryptionVersion: true,
protectedKey: true,
protectedKeyIV: true,
protectedKeyTag: true,
publicKey: true,
encryptedPrivateKey: true,
iv: true,
tag: true,
salt: true,
verifier: true,
userId: true
})
)
}) })
} }
}, },
@@ -94,26 +80,6 @@ export const registerUserRouter = async (server: FastifyZodProvider) => {
} }
}); });
server.route({
method: "GET",
url: "/private-key",
config: {
rateLimit: readLimit
},
schema: {
response: {
200: z.object({
privateKey: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => {
const privateKey = await server.services.user.getUserPrivateKey(req.permission.id);
return { privateKey };
}
});
server.route({ server.route({
method: "GET", method: "GET",
url: "/:userId/unlock", url: "/:userId/unlock",
+7 -7
View File
@@ -97,13 +97,13 @@ export const registerMfaRouter = async (server: FastifyZodProvider) => {
response: { response: {
200: z.object({ 200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(), encryptionVersion: z.number().default(1).nullable().optional(),
protectedKey: z.string().nullable(), protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullable(), protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullable(), protectedKeyTag: z.string().nullish(),
publicKey: z.string(), publicKey: z.string().nullish(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullish(),
iv: z.string(), iv: z.string().nullish(),
tag: z.string(), tag: z.string().nullish(),
token: z.string() token: z.string()
}) })
} }
@@ -153,7 +153,7 @@ export const registerOrgRouter = async (server: FastifyZodProvider) => {
firstName: true, firstName: true,
lastName: true, lastName: true,
id: true id: true
}).extend({ publicKey: z.string().nullable() }) }).extend({ publicKey: z.string().nullish() })
}).omit({ createdAt: true, updatedAt: true }) }).omit({ createdAt: true, updatedAt: true })
}) })
} }
@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns"; import { validatePasswordResetAuthorization } from "@app/services/auth/auth-fns";
@@ -41,13 +42,38 @@ export const registerPasswordRouter = async (server: FastifyZodProvider) => {
rateLimit: authRateLimit rateLimit: authRateLimit
}, },
onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }), onRequest: verifyAuth([AuthMode.JWT], { requireOrg: false }),
handler: async (req) => { handler: async (req, res) => {
const appCfg = getConfig();
await server.services.password.resetPasswordV2({ await server.services.password.resetPasswordV2({
type: ResetPasswordV2Type.LoggedInReset, type: ResetPasswordV2Type.LoggedInReset,
userId: req.permission.id, userId: req.permission.id,
newPassword: req.body.newPassword, newPassword: req.body.newPassword,
oldPassword: req.body.oldPassword oldPassword: req.body.oldPassword
}); });
void res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
void res.cookie("aod", "", {
httpOnly: false,
path: "/",
sameSite: "lax",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
} }
}); });
}; };
@@ -52,7 +52,7 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
200: ProjectKeysSchema.merge( 200: ProjectKeysSchema.merge(
z.object({ z.object({
sender: z.object({ sender: z.object({
publicKey: z.string() publicKey: z.string().optional()
}) })
}) })
) )
+65 -10
View File
@@ -20,8 +20,8 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
serverPublicKey: z.string(), serverPublicKey: z.string().nullish(),
salt: z.string() salt: z.string().nullish()
}) })
} }
}, },
@@ -124,14 +124,14 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}), }),
response: { response: {
200: z.object({ 200: z.object({
encryptionVersion: z.number().default(1).nullable().optional(), encryptionVersion: z.number().default(1).nullish(),
protectedKey: z.string().nullable(), protectedKey: z.string().nullish(),
protectedKeyIV: z.string().nullable(), protectedKeyIV: z.string().nullish(),
protectedKeyTag: z.string().nullable(), protectedKeyTag: z.string().nullish(),
publicKey: z.string(), publicKey: z.string().nullish(),
encryptedPrivateKey: z.string(), encryptedPrivateKey: z.string().nullish(),
iv: z.string(), iv: z.string().nullish(),
tag: z.string(), tag: z.string().nullish(),
token: z.string() token: z.string()
}) })
} }
@@ -181,4 +181,59 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
} as const; } as const;
} }
}); });
// New login route that doesn't use SRP
server.route({
method: "POST",
url: "/login",
config: {
rateLimit: authRateLimit
},
schema: {
body: z.object({
email: z.string().trim(),
password: z.string().trim(),
providerAuthToken: z.string().trim().optional(),
captchaToken: z.string().trim().optional()
}),
response: {
200: z.object({
accessToken: z.string()
})
}
},
handler: async (req, res) => {
const userAgent = req.headers["user-agent"];
if (!userAgent) throw new Error("user agent header is required");
const { tokens } = await server.services.login.login({
email: req.body.email,
password: req.body.password,
ip: req.realIp,
userAgent,
providerAuthToken: req.body.providerAuthToken,
captchaToken: req.body.captchaToken
});
const appCfg = getConfig();
void res.setCookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED
});
addAuthOriginDomainCookie(res);
void res.cookie("infisical-project-assume-privileges", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
maxAge: 0
});
return { accessToken: tokens.accessToken };
}
});
}; };
@@ -98,15 +98,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
email: z.string().trim(), email: z.string().trim(),
firstName: z.string().trim(), firstName: z.string().trim(),
lastName: z.string().trim().optional(), lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
providerAuthToken: z.string().trim().optional().nullish(), providerAuthToken: z.string().trim().optional().nullish(),
attributionSource: z.string().trim().optional(), attributionSource: z.string().trim().optional(),
password: z.string() password: z.string()
@@ -189,15 +180,6 @@ export const registerSignupRouter = async (server: FastifyZodProvider) => {
password: z.string(), password: z.string(),
firstName: z.string().trim(), firstName: z.string().trim(),
lastName: z.string().trim().optional(), lastName: z.string().trim().optional(),
protectedKey: z.string().trim(),
protectedKeyIV: z.string().trim(),
protectedKeyTag: z.string().trim(),
publicKey: z.string().trim(),
encryptedPrivateKey: z.string().trim(),
encryptedPrivateKeyIV: z.string().trim(),
encryptedPrivateKeyTag: z.string().trim(),
salt: z.string().trim(),
verifier: z.string().trim(),
tokenMetadata: z.string().optional() tokenMetadata: z.string().optional()
}), }),
response: { response: {
@@ -1,4 +1,5 @@
import { createAppAuth } from "@octokit/auth-app"; import { createAppAuth } from "@octokit/auth-app";
import { request } from "@octokit/request";
import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios"; import { AxiosError, AxiosRequestConfig, AxiosResponse } from "axios";
import https from "https"; import https from "https";
import RE2 from "re2"; import RE2 from "re2";
@@ -12,7 +13,6 @@ import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator"; import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns"; import { getAppConnectionMethodName } from "@app/services/app-connection/app-connection-fns";
import { IntegrationUrls } from "@app/services/integration-auth/integration-list";
import { AppConnection } from "../app-connection-enums"; import { AppConnection } from "../app-connection-enums";
import { GitHubConnectionMethod } from "./github-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums";
@@ -30,6 +30,23 @@ export const getGitHubConnectionListItem = () => {
}; };
}; };
export const getGitHubInstanceApiUrl = async (config: {
credentials: Pick<TGitHubConnectionConfig["credentials"], "host" | "instanceType">;
}) => {
const host = config.credentials.host || "github.com";
await blockLocalAndPrivateIpAddresses(`https://${host}`);
let apiBase: string;
if (config.credentials.instanceType === "server") {
apiBase = `${host}/api/v3`;
} else {
apiBase = `api.${host}`;
}
return apiBase;
};
export const requestWithGitHubGateway = async <T>( export const requestWithGitHubGateway = async <T>(
appConnection: { gatewayId?: string | null }, appConnection: { gatewayId?: string | null },
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">, gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
@@ -73,7 +90,10 @@ export const requestWithGitHubGateway = async <T>(
return await httpRequest.request(finalRequestConfig); return await httpRequest.request(finalRequestConfig);
} catch (error) { } catch (error) {
const axiosError = error as AxiosError; const axiosError = error as AxiosError;
logger.error("Error during GitHub gateway request:", axiosError.message, axiosError.response?.data); logger.error(
{ message: axiosError.message, data: axiosError.response?.data },
"Error during GitHub gateway request:"
);
throw error; throw error;
} }
}, },
@@ -112,7 +132,10 @@ export const getGitHubAppAuthToken = async (appConnection: TGitHubConnection) =>
const appAuth = createAppAuth({ const appAuth = createAppAuth({
appId, appId,
privateKey: appPrivateKey, privateKey: appPrivateKey,
installationId: appConnection.credentials.installationId installationId: appConnection.credentials.installationId,
request: request.defaults({
baseUrl: `https://${await getGitHubInstanceApiUrl(appConnection)}`
})
}); });
const { token } = await appAuth({ type: "installation" }); const { token } = await appAuth({ type: "installation" });
@@ -141,7 +164,7 @@ export const makePaginatedGitHubRequest = async <T, R = T[]>(
const token = const token =
method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection); method === GitHubConnectionMethod.OAuth ? credentials.accessToken : await getGitHubAppAuthToken(appConnection);
let url: string | null = `https://api.${credentials.host || "github.com"}${path}`; let url: string | null = `https://${await getGitHubInstanceApiUrl(appConnection)}${path}`;
let results: T[] = []; let results: T[] = [];
let i = 0; let i = 0;
@@ -325,6 +348,8 @@ export const validateGitHubConnectionCredentials = async (
}); });
} }
} catch (e: unknown) { } catch (e: unknown) {
logger.error(e, "Unable to verify GitHub connection");
if (e instanceof BadRequestError) { if (e instanceof BadRequestError) {
throw e; throw e;
} }
@@ -355,7 +380,7 @@ export const validateGitHubConnectionCredentials = async (
}; };
}[]; }[];
}>(config, gatewayService, { }>(config, gatewayService, {
url: IntegrationUrls.GITHUB_USER_INSTALLATIONS.replace("api.github.com", `api.${host}`), url: `https://${await getGitHubInstanceApiUrl(config)}/user/installations`,
headers: { headers: {
Accept: "application/json", Accept: "application/json",
Authorization: `Bearer ${tokenResp.data.access_token}`, Authorization: `Bearer ${tokenResp.data.access_token}`,
@@ -377,11 +402,15 @@ export const validateGitHubConnectionCredentials = async (
switch (method) { switch (method) {
case GitHubConnectionMethod.App: case GitHubConnectionMethod.App:
return { return {
installationId: credentials.installationId installationId: credentials.installationId,
instanceType: credentials.instanceType,
host: credentials.host
}; };
case GitHubConnectionMethod.OAuth: case GitHubConnectionMethod.OAuth:
return { return {
accessToken: tokenResp.data.access_token accessToken: tokenResp.data.access_token,
instanceType: credentials.instanceType,
host: credentials.host
}; };
default: default:
throw new InternalServerError({ throw new InternalServerError({
@@ -10,26 +10,59 @@ import {
import { GitHubConnectionMethod } from "./github-connection-enums"; import { GitHubConnectionMethod } from "./github-connection-enums";
export const GitHubConnectionOAuthInputCredentialsSchema = z.object({ export const GitHubConnectionOAuthInputCredentialsSchema = z.union([
code: z.string().trim().min(1, "OAuth code required"), z.object({
host: z.string().trim().optional() code: z.string().trim().min(1, "OAuth code required"),
}); instanceType: z.literal("server"),
host: z.string().trim().min(1, "Host is required for server instance type")
}),
z.object({
code: z.string().trim().min(1, "OAuth code required"),
instanceType: z.literal("cloud").optional(),
host: z.string().trim().optional()
})
]);
export const GitHubConnectionAppInputCredentialsSchema = z.object({ export const GitHubConnectionAppInputCredentialsSchema = z.union([
code: z.string().trim().min(1, "GitHub App code required"), z.object({
installationId: z.string().min(1, "GitHub App Installation ID required"), code: z.string().trim().min(1, "GitHub App code required"),
host: z.string().trim().optional() installationId: z.string().min(1, "GitHub App Installation ID required"),
}); instanceType: z.literal("server"),
host: z.string().trim().min(1, "Host is required for server instance type")
}),
z.object({
code: z.string().trim().min(1, "GitHub App code required"),
installationId: z.string().min(1, "GitHub App Installation ID required"),
instanceType: z.literal("cloud").optional(),
host: z.string().trim().optional()
})
]);
export const GitHubConnectionOAuthOutputCredentialsSchema = z.object({ export const GitHubConnectionOAuthOutputCredentialsSchema = z.union([
accessToken: z.string(), z.object({
host: z.string().trim().optional() accessToken: z.string(),
}); instanceType: z.literal("server"),
host: z.string().trim().min(1)
}),
z.object({
accessToken: z.string(),
instanceType: z.literal("cloud").optional(),
host: z.string().trim().optional()
})
]);
export const GitHubConnectionAppOutputCredentialsSchema = z.object({ export const GitHubConnectionAppOutputCredentialsSchema = z.union([
installationId: z.string(), z.object({
host: z.string().trim().optional() installationId: z.string(),
}); instanceType: z.literal("server"),
host: z.string().trim().min(1)
}),
z.object({
installationId: z.string(),
instanceType: z.literal("cloud").optional(),
host: z.string().trim().optional()
})
]);
export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [ export const ValidateGitHubConnectionCredentialsSchema = z.discriminatedUnion("method", [
z.object({ z.object({
@@ -84,11 +117,17 @@ export const GitHubConnectionSchema = z.intersection(
export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [ export const SanitizedGitHubConnectionSchema = z.discriminatedUnion("method", [
BaseGitHubConnectionSchema.extend({ BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.App), method: z.literal(GitHubConnectionMethod.App),
credentials: GitHubConnectionAppOutputCredentialsSchema.pick({}) credentials: z.object({
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional()
})
}), }),
BaseGitHubConnectionSchema.extend({ BaseGitHubConnectionSchema.extend({
method: z.literal(GitHubConnectionMethod.OAuth), method: z.literal(GitHubConnectionMethod.OAuth),
credentials: GitHubConnectionOAuthOutputCredentialsSchema.pick({}) credentials: z.object({
instanceType: z.union([z.literal("server"), z.literal("cloud")]).optional(),
host: z.string().optional()
})
}) })
]); ]);
+57 -2
View File
@@ -1,8 +1,16 @@
import { TUsers } from "@app/db/schemas";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto } from "@app/lib/crypto"; import { crypto } from "@app/lib/crypto";
import { ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
import { AuthModeProviderJwtTokenPayload, AuthModeProviderSignUpTokenPayload, AuthTokenType } from "./auth-type"; import {
AuthMethod,
AuthModeProviderJwtTokenPayload,
AuthModeProviderSignUpTokenPayload,
AuthTokenType
} from "./auth-type";
export const validateProviderAuthToken = (providerToken: string, username?: string) => { export const validateProviderAuthToken = (providerToken: string, username?: string) => {
if (!providerToken) throw new UnauthorizedError(); if (!providerToken) throw new UnauthorizedError();
@@ -97,3 +105,50 @@ export const enforceUserLockStatus = (isLocked: boolean, temporaryLockDateEnd?:
} }
} }
}; };
export const verifyCaptcha = async (user: TUsers, captchaToken?: string) => {
const appCfg = getConfig();
if (
user.consecutiveFailedPasswordAttempts &&
user.consecutiveFailedPasswordAttempts >= 10 &&
Boolean(appCfg.CAPTCHA_SECRET)
) {
if (!captchaToken) {
throw new BadRequestError({
name: "Captcha Required",
message: "Accomplish the required captcha by logging in via Web"
});
}
// validate captcha token
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
response: captchaToken,
secret: appCfg.CAPTCHA_SECRET
});
if (!response.data.success) {
throw new BadRequestError({
name: "Invalid Captcha"
});
}
}
};
export const getAuthMethodAndOrgId = (email: string, providerAuthToken?: string) => {
let authMethod = AuthMethod.EMAIL;
let organizationId: string | undefined;
if (providerAuthToken) {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email);
authMethod = decodedProviderToken.authMethod;
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
) {
organizationId = decodedProviderToken.orgId;
}
}
return { authMethod, organizationId };
};
+86 -42
View File
@@ -4,7 +4,6 @@ import { OrgMembershipRole, OrgMembershipStatus, TableName, TUsers, UserDeviceSc
import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types"; import { EventType, TAuditLogServiceFactory } from "@app/ee/services/audit-log/audit-log-types";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { request } from "@app/lib/config/request";
import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto"; import { crypto, generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { getUserPrivateKey } from "@app/lib/crypto/srp"; import { getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors"; import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
@@ -22,7 +21,8 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { LoginMethod } from "../super-admin/super-admin-types"; import { LoginMethod } from "../super-admin/super-admin-types";
import { TTotpServiceFactory } from "../totp/totp-service"; import { TTotpServiceFactory } from "../totp/totp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { enforceUserLockStatus, validateProviderAuthToken } from "./auth-fns"; import { UserEncryption } from "../user/user-types";
import { enforceUserLockStatus, getAuthMethodAndOrgId, validateProviderAuthToken, verifyCaptcha } from "./auth-fns";
import { import {
TLoginClientProofDTO, TLoginClientProofDTO,
TLoginGenServerPublicKeyDTO, TLoginGenServerPublicKeyDTO,
@@ -208,6 +208,10 @@ export const authLoginServiceFactory = ({
throw new Error("Failed to find user"); throw new Error("Failed to find user");
} }
if (!userEnc.salt || !userEnc.verifier) {
throw new BadRequestError({ message: "Salt or verifier not found" });
}
if ( if (
serverCfg.enabledLoginMethods && serverCfg.enabledLoginMethods &&
!serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) && !serverCfg.enabledLoginMethods.includes(LoginMethod.EMAIL) &&
@@ -247,8 +251,6 @@ export const authLoginServiceFactory = ({
captchaToken, captchaToken,
password password
}: TLoginClientProofDTO) => { }: TLoginClientProofDTO) => {
const appCfg = getConfig();
// akhilmhdh: case sensitive email resolution // akhilmhdh: case sensitive email resolution
const usersByUsername = await userDAL.findUserEncKeyByUsername({ const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email username: email
@@ -259,44 +261,11 @@ export const authLoginServiceFactory = ({
const user = await userDAL.findById(userEnc.userId); const user = await userDAL.findById(userEnc.userId);
const cfg = getConfig(); const cfg = getConfig();
let authMethod = AuthMethod.EMAIL; const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
let organizationId: string | undefined; await verifyCaptcha(user, captchaToken);
if (providerAuthToken) { if (!userEnc.salt || !userEnc.verifier) {
const decodedProviderToken = validateProviderAuthToken(providerAuthToken, email); throw new BadRequestError({ message: "Salt or verifier not found" });
authMethod = decodedProviderToken.authMethod;
if (
(isAuthMethodSaml(authMethod) || [AuthMethod.LDAP, AuthMethod.OIDC].includes(authMethod)) &&
decodedProviderToken.orgId
) {
organizationId = decodedProviderToken.orgId;
}
}
if (
user.consecutiveFailedPasswordAttempts &&
user.consecutiveFailedPasswordAttempts >= 10 &&
Boolean(appCfg.CAPTCHA_SECRET)
) {
if (!captchaToken) {
throw new BadRequestError({
name: "Captcha Required",
message: "Accomplish the required captcha by logging in via Web"
});
}
// validate captcha token
const response = await request.postForm<{ success: boolean }>("https://api.hcaptcha.com/siteverify", {
response: captchaToken,
secret: appCfg.CAPTCHA_SECRET
});
if (!response.data.success) {
throw new BadRequestError({
name: "Invalid Captcha"
});
}
} }
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?"); if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
@@ -371,6 +340,80 @@ export const authLoginServiceFactory = ({
return { token, user: userEnc } as const; return { token, user: userEnc } as const;
}; };
const login = async ({
email,
password,
ip,
userAgent,
providerAuthToken,
captchaToken
}: {
email: string;
password: string;
ip: string;
userAgent: string;
providerAuthToken?: string;
captchaToken?: string;
}) => {
const usersByUsername = await userDAL.findUserEncKeyByUsername({
username: email
});
const userEnc =
usersByUsername?.length > 1 ? usersByUsername.find((el) => el.username === email) : usersByUsername?.[0];
if (!userEnc) throw new BadRequestError({ message: "User not found" });
if (userEnc.encryptionVersion !== UserEncryption.V2) {
throw new BadRequestError({ message: "Legacy encryption scheme not supported", name: "LegacyEncryptionScheme" });
}
if (!userEnc.hashedPassword) {
if (userEnc.authMethods?.includes(AuthMethod.EMAIL)) {
throw new BadRequestError({
message: "Legacy encryption scheme not supported",
name: "LegacyEncryptionScheme"
});
}
throw new BadRequestError({ message: "No password found" });
}
const { authMethod, organizationId } = getAuthMethodAndOrgId(email, providerAuthToken);
await verifyCaptcha(userEnc, captchaToken);
if (!(await crypto.hashing().compareHash(password, userEnc.hashedPassword))) {
await userDAL.update(
{ id: userEnc.userId },
{
$incr: {
consecutiveFailedPasswordAttempts: 1
}
}
);
throw new BadRequestError({ message: "Invalid username or email" });
}
const token = await generateUserTokens({
user: {
...userEnc,
id: userEnc.userId
},
ip,
userAgent,
authMethod,
organizationId
});
return {
tokens: {
accessToken: token.access,
refreshToken: token.refresh
},
user: userEnc
} as const;
};
const selectOrganization = async ({ const selectOrganization = async ({
userAgent, userAgent,
authJwtToken, authJwtToken,
@@ -862,6 +905,7 @@ export const authLoginServiceFactory = ({
resendMfaToken, resendMfaToken,
verifyMfaToken, verifyMfaToken,
selectOrganization, selectOrganization,
generateUserTokens generateUserTokens,
login
}; };
}; };
@@ -1,8 +1,5 @@
import { SecretEncryptionAlgo, SecretKeyEncoding } from "@app/db/schemas";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { generateSrpServerKey, srpCheckClientProof } from "@app/lib/crypto";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys } from "@app/lib/crypto/srp";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types"; import { OrgServiceActor } from "@app/lib/types";
@@ -16,8 +13,6 @@ import { UserEncryption } from "../user/user-types";
import { TAuthDALFactory } from "./auth-dal"; import { TAuthDALFactory } from "./auth-dal";
import { import {
ResetPasswordV2Type, ResetPasswordV2Type,
TChangePasswordDTO,
TCreateBackupPrivateKeyDTO,
TResetPasswordV2DTO, TResetPasswordV2DTO,
TResetPasswordViaBackupKeyDTO, TResetPasswordViaBackupKeyDTO,
TSetupPasswordViaBackupKeyDTO TSetupPasswordViaBackupKeyDTO
@@ -40,79 +35,6 @@ export const authPaswordServiceFactory = ({
smtpService, smtpService,
totpConfigDAL totpConfigDAL
}: TAuthPasswordServiceFactoryDep) => { }: TAuthPasswordServiceFactoryDep) => {
/*
* Pre setup for pass change with srp protocol
* Gets srp server user salt and server public key
*/
const generateServerPubKey = async (userId: string, clientPublicKey: string) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
const serverSrpKey = await generateSrpServerKey(userEnc.salt, userEnc.verifier);
const userEncKeys = await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
clientPublicKey,
serverPrivateKey: serverSrpKey.privateKey
});
if (!userEncKeys) throw new Error("Failed to update encryption key");
return { salt: userEncKeys.salt, serverPublicKey: serverSrpKey.pubKey };
};
/*
* Change password to new pass
* */
const changePassword = async ({
userId,
clientProof,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
tokenVersionId,
password
}: TChangePasswordDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc) throw new Error("Failed to find user");
await userDAL.updateUserEncryptionByUserId(userEnc.userId, {
serverPrivateKey: null,
clientPublicKey: null
});
if (!userEnc.serverPrivateKey || !userEnc.clientPublicKey) throw new Error("Failed to authenticate. Try again?");
const isValidClientProof = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProof) throw new Error("Failed to authenticate. Try again?");
const appCfg = getConfig();
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
await userDAL.updateUserEncryptionByUserId(userId, {
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
serverPrivateKey: null,
clientPublicKey: null,
hashedPassword
});
if (tokenVersionId) {
await tokenService.clearTokenSessionById(userEnc.userId, tokenVersionId);
}
};
/* /*
* Email password reset flow via email. Step 1 send email * Email password reset flow via email. Step 1 send email
*/ */
@@ -193,6 +115,10 @@ export const authPaswordServiceFactory = ({
} }
if (!user.authMethods?.includes(AuthMethod.EMAIL)) { if (!user.authMethods?.includes(AuthMethod.EMAIL)) {
logger.error(
{ authMethods: user.authMethods },
"Unable to reset password, no email authentication method is configured"
);
throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" }); throw new BadRequestError({ message: "Unable to reset password, no email authentication method is configured" });
} }
@@ -211,58 +137,17 @@ export const authPaswordServiceFactory = ({
} }
} }
const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS); if (user.encryptionVersion !== UserEncryption.V2) {
// we need to get the original private key first for v2
let privateKey: string;
if (
user.serverEncryptedPrivateKey &&
user.serverEncryptedPrivateKeyTag &&
user.serverEncryptedPrivateKeyIV &&
user.serverEncryptedPrivateKeyEncoding &&
user.encryptionVersion === UserEncryption.V2
) {
privateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
iv: user.serverEncryptedPrivateKeyIV,
tag: user.serverEncryptedPrivateKeyTag,
ciphertext: user.serverEncryptedPrivateKey,
keyEncoding: user.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
} else {
throw new BadRequestError({ throw new BadRequestError({
message: "Cannot reset password without current credentials or recovery method", message: "Cannot reset password without current credentials or recovery method",
name: "Reset password" name: "Reset password"
}); });
} }
const encKeys = await generateUserSrpKeys(user.username, newPassword, { const newHashedPassword = await crypto.hashing().createHash(newPassword, cfg.SALT_ROUNDS);
publicKey: user.publicKey,
privateKey
});
const { tag, iv, ciphertext, encoding } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
await userDAL.updateUserEncryptionByUserId(userId, { await userDAL.updateUserEncryptionByUserId(userId, {
hashedPassword: newHashedPassword, hashedPassword: newHashedPassword
// srp params
salt: encKeys.salt,
verifier: encKeys.verifier,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
serverEncryptedPrivateKey: ciphertext,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyEncoding: encoding
}); });
await tokenService.revokeAllMySessions(userId); await tokenService.revokeAllMySessions(userId);
@@ -313,66 +198,6 @@ export const authPaswordServiceFactory = ({
}); });
}; };
/*
* backup key creation to give user's their access back when lost their password
* this also needs to do the generateServerPubKey function to be executed first
* then only client proof can be verified
* */
const createBackupPrivateKey = async ({
clientProof,
encryptedPrivateKey,
salt,
verifier,
iv,
tag,
userId
}: TCreateBackupPrivateKeyDTO) => {
const userEnc = await userDAL.findUserEncKeyByUserId(userId);
if (!userEnc || (userEnc && !userEnc.isAccepted)) {
throw new Error("Failed to find user");
}
if (!userEnc.clientPublicKey || !userEnc.serverPrivateKey) throw new Error("failed to create backup key");
const isValidClientProff = await srpCheckClientProof(
userEnc.salt,
userEnc.verifier,
userEnc.serverPrivateKey,
userEnc.clientPublicKey,
clientProof
);
if (!isValidClientProff) throw new Error("failed to create backup key");
const backup = await authDAL.transaction(async (tx) => {
const backupKey = await authDAL.upsertBackupKey(
userEnc.userId,
{
encryptedPrivateKey,
iv,
tag,
salt,
verifier,
algorithm: SecretEncryptionAlgo.AES_256_GCM,
keyEncoding: SecretKeyEncoding.UTF8
},
tx
);
await userDAL.updateUserEncryptionByUserId(
userEnc.userId,
{
serverPrivateKey: null,
clientPublicKey: null
},
tx
);
return backupKey;
});
return backup;
};
/*
* Return user back up
* */
const getBackupPrivateKeyOfUser = async (userId: string) => { const getBackupPrivateKeyOfUser = async (userId: string) => {
const user = await userDAL.findUserEncKeyByUserId(userId); const user = await userDAL.findUserEncKeyByUserId(userId);
if (!user || (user && !user.isAccepted)) { if (!user || (user && !user.isAccepted)) {
@@ -416,21 +241,7 @@ export const authPaswordServiceFactory = ({
}); });
}; };
const setupPassword = async ( const setupPassword = async ({ password, token }: TSetupPasswordViaBackupKeyDTO, actor: OrgServiceActor) => {
{
encryptedPrivateKey,
protectedKeyTag,
protectedKey,
protectedKeyIV,
salt,
verifier,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
password,
token
}: TSetupPasswordViaBackupKeyDTO,
actor: OrgServiceActor
) => {
try { try {
await tokenService.validateTokenForUser({ await tokenService.validateTokenForUser({
type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP, type: TokenType.TOKEN_EMAIL_PASSWORD_SETUP,
@@ -466,15 +277,7 @@ export const authPaswordServiceFactory = ({
await userDAL.updateUserEncryptionByUserId( await userDAL.updateUserEncryptionByUserId(
actor.id, actor.id,
{ {
encryptionVersion: 2, encryptionVersion: UserEncryption.V2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
hashedPassword, hashedPassword,
serverPrivateKey: null, serverPrivateKey: null,
clientPublicKey: null clientPublicKey: null
@@ -487,12 +290,9 @@ export const authPaswordServiceFactory = ({
}; };
return { return {
generateServerPubKey,
changePassword,
resetPasswordByBackupKey, resetPasswordByBackupKey,
sendPasswordResetEmail, sendPasswordResetEmail,
verifyPasswordResetEmail, verifyPasswordResetEmail,
createBackupPrivateKey,
getBackupPrivateKeyOfUser, getBackupPrivateKeyOfUser,
sendPasswordSetupEmail, sendPasswordSetupEmail,
setupPassword, setupPassword,
@@ -1,18 +1,3 @@
export type TChangePasswordDTO = {
userId: string;
clientProof: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
tokenVersionId?: string;
password: string;
};
export enum ResetPasswordV2Type { export enum ResetPasswordV2Type {
Recovery = "recovery", Recovery = "recovery",
LoggedInReset = "logged-in-reset" LoggedInReset = "logged-in-reset"
@@ -39,14 +24,6 @@ export type TResetPasswordViaBackupKeyDTO = {
}; };
export type TSetupPasswordViaBackupKeyDTO = { export type TSetupPasswordViaBackupKeyDTO = {
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
password: string; password: string;
token: string; token: string;
}; };
+18 -194
View File
@@ -1,11 +1,10 @@
import { OrgMembershipStatus, SecretKeyEncoding, TableName } from "@app/db/schemas"; import { OrgMembershipStatus, TableName } from "@app/db/schemas";
import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns"; import { convertPendingGroupAdditionsToGroupMemberships } from "@app/ee/services/group/group-fns";
import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { TUserGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns"; import { isAuthMethodSaml } from "@app/ee/services/permission/permission-fns";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { generateUserSrpKeys, getUserPrivateKey } from "@app/lib/crypto/srp";
import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { getMinExpiresIn } from "@app/lib/fn"; import { getMinExpiresIn } from "@app/lib/fn";
import { isDisposableEmail } from "@app/lib/validator"; import { isDisposableEmail } from "@app/lib/validator";
@@ -41,7 +40,7 @@ type TAuthSignupDep = {
| "findUserGroupMembershipsInProject" | "findUserGroupMembershipsInProject"
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">; projectKeyDAL: Pick<TProjectKeyDALFactory, "find" | "findLatestProjectKey" | "insertMany">;
projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById">; projectDAL: Pick<TProjectDALFactory, "findProjectGhostUser" | "findProjectById" | "findById">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
groupProjectDAL: Pick<TGroupProjectDALFactory, "find">; groupProjectDAL: Pick<TGroupProjectDALFactory, "find">;
orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">; orgService: Pick<TOrgServiceFactory, "createOrganization" | "findOrganizationById">;
@@ -147,17 +146,8 @@ export const authSignupServiceFactory = ({
firstName, firstName,
lastName, lastName,
providerAuthToken, providerAuthToken,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName, organizationName,
// attributionSource, // attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
ip, ip,
userAgent, userAgent,
authorization, authorization,
@@ -191,98 +181,18 @@ export const authSignupServiceFactory = ({
} }
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
const privateKey = await getUserPrivateKey(password, {
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
encryptionVersion: UserEncryption.V2
});
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx);
let userEncKey;
// below condition is true means this is system generated credentials const userEncKey = await userDAL.upsertUserEncryptionKey(
// the private key is actually system generated password us.id,
// thus we will re-encrypt the system generated private key with the new password {
// akhilmhdh: you may find this like why? The reason is simple we are moving away from e2ee and these are pieces of it encryptionVersion: UserEncryption.V2,
// without a dummy key in place some things will break and backward compatiability too. 2025 we will be removing all these things hashedPassword
if ( },
systemGeneratedUserEncryptionKey && tx
!systemGeneratedUserEncryptionKey.hashedPassword && );
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
) {
// get server generated password
const serverGeneratedPassword = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(email, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
// now reencrypt server generated key with user provided password
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
} else {
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
}
// If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it // If it's SAML Auth and the organization ID is present, we should check if the user has a pending invite for this org, and accept it
if ( if (
@@ -400,19 +310,10 @@ export const authSignupServiceFactory = ({
const completeAccountInvite = async ({ const completeAccountInvite = async ({
email, email,
ip, ip,
salt,
password, password,
verifier,
firstName, firstName,
publicKey,
userAgent, userAgent,
lastName, lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
authorization authorization
}: TCompleteAccountInviteDTO) => { }: TCompleteAccountInviteDTO) => {
const sanitizedEmail = email.trim().toLowerCase(); const sanitizedEmail = email.trim().toLowerCase();
@@ -437,94 +338,17 @@ export const authSignupServiceFactory = ({
const appCfg = getConfig(); const appCfg = getConfig();
const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS); const hashedPassword = await crypto.hashing().createHash(password, appCfg.SALT_ROUNDS);
const privateKey = await getUserPrivateKey(password, {
salt,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
encryptionVersion: 2
});
const { tag, encoding, ciphertext, iv } = crypto.encryption().symmetric().encryptWithRootEncryptionKey(privateKey);
const updateduser = await authDAL.transaction(async (tx) => { const updateduser = await authDAL.transaction(async (tx) => {
const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx); const us = await userDAL.updateById(user.id, { firstName, lastName, isAccepted: true }, tx);
if (!us) throw new Error("User not found"); if (!us) throw new Error("User not found");
const systemGeneratedUserEncryptionKey = await userDAL.findUserEncKeyByUserId(us.id, tx); const userEncKey = await userDAL.upsertUserEncryptionKey(
let userEncKey; us.id,
// this means this is system generated credentials {
// now replace the private key encryptionVersion: 2,
if ( hashedPassword
systemGeneratedUserEncryptionKey && },
!systemGeneratedUserEncryptionKey.hashedPassword && tx
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey && );
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV &&
systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding
) {
// get server generated password
const serverGeneratedPassword = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
iv: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyIV,
tag: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyTag,
ciphertext: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKey,
keyEncoding: systemGeneratedUserEncryptionKey.serverEncryptedPrivateKeyEncoding as SecretKeyEncoding
});
const serverGeneratedPrivateKey = await getUserPrivateKey(serverGeneratedPassword, {
...systemGeneratedUserEncryptionKey
});
const encKeys = await generateUserSrpKeys(sanitizedEmail, password, {
publicKey: systemGeneratedUserEncryptionKey.publicKey,
privateKey: serverGeneratedPrivateKey
});
// now reencrypt server generated key with user provided password
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: 2,
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
} else {
userEncKey = await userDAL.upsertUserEncryptionKey(
us.id,
{
encryptionVersion: UserEncryption.V2,
salt,
verifier,
publicKey,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
hashedPassword,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
},
tx
);
}
const updatedMembersips = await orgDAL.updateMembership( const updatedMembersips = await orgDAL.updateMembership(
{ inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited }, { inviteEmail: sanitizedEmail, status: OrgMembershipStatus.Invited },
@@ -3,15 +3,6 @@ export type TCompleteAccountSignupDTO = {
password: string; password: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName?: string; organizationName?: string;
providerAuthToken?: string | null; providerAuthToken?: string | null;
attributionSource?: string | undefined; attributionSource?: string | undefined;
@@ -26,15 +17,6 @@ export type TCompleteAccountInviteDTO = {
password: string; password: string;
firstName: string; firstName: string;
lastName?: string; lastName?: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
ip: string; ip: string;
userAgent: string; userAgent: string;
authorization: string; authorization: string;
@@ -2,6 +2,7 @@ import { z } from "zod";
import { isValidIp } from "@app/lib/ip"; import { isValidIp } from "@app/lib/ip";
import { isFQDN } from "@app/lib/validator/validate-url"; import { isFQDN } from "@app/lib/validator/validate-url";
import { TAltNameMapping, TAltNameType } from "@app/services/certificate/certificate-types";
const isValidDate = (dateString: string) => { const isValidDate = (dateString: string) => {
const date = new Date(dateString); const date = new Date(dateString);
@@ -15,10 +16,15 @@ export const validateAltNameField = z
.trim() .trim()
.refine( .refine(
(name) => { (name) => {
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name); return (
isFQDN(name, { allow_wildcard: true, require_tld: false }) ||
z.string().url().safeParse(name).success ||
z.string().email().safeParse(name).success ||
isValidIp(name)
);
}, },
{ {
message: "SAN must be a valid hostname, email address, or IP address" message: "SAN must be a valid hostname, email address, IP address or URL"
} }
); );
@@ -39,10 +45,31 @@ export const validateAltNamesField = z
if (data === "") return true; if (data === "") return true;
// Split and validate each alt name // Split and validate each alt name
return data.split(", ").every((name) => { return data.split(", ").every((name) => {
return isFQDN(name, { allow_wildcard: true }) || z.string().email().safeParse(name).success || isValidIp(name); return (
isFQDN(name, { allow_wildcard: true, require_tld: false }) ||
z.string().url().safeParse(name).success ||
z.string().email().safeParse(name).success ||
isValidIp(name)
);
}); });
}, },
{ {
message: "Each alt name must be a valid hostname or email address" message: "Each alt name must be a valid hostname, email address, IP address or URL"
} }
); );
export const validateAndMapAltNameType = (name: string): TAltNameMapping | null => {
if (isFQDN(name, { allow_wildcard: true, require_tld: false })) {
return { type: TAltNameType.DNS, value: name };
}
if (z.string().url().safeParse(name).success) {
return { type: TAltNameType.URL, value: name };
}
if (z.string().email().safeParse(name).success) {
return { type: TAltNameType.EMAIL, value: name };
}
if (isValidIp(name)) {
return { type: TAltNameType.IP, value: name };
}
return null;
};
@@ -1,7 +1,6 @@
/* eslint-disable no-bitwise */ /* eslint-disable no-bitwise */
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import RE2 from "re2"; import RE2 from "re2";
import { z } from "zod";
import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas"; import { TCertificateTemplates, TPkiSubscribers } from "@app/db/schemas";
import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal"; import { TCertificateAuthorityCrlDALFactory } from "@app/ee/services/certificate-authority-crl/certificate-authority-crl-dal";
@@ -9,7 +8,6 @@ import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto/cryptography"; import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal"; import { TCertificateSecretDALFactory } from "@app/services/certificate/certificate-secret-dal";
@@ -17,7 +15,8 @@ import {
CertExtendedKeyUsage, CertExtendedKeyUsage,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyUsage, CertKeyUsage,
CertStatus CertStatus,
TAltNameMapping
} from "@app/services/certificate/certificate-types"; } from "@app/services/certificate/certificate-types";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { TProjectDALFactory } from "@app/services/project/project-dal"; import { TProjectDALFactory } from "@app/services/project/project-dal";
@@ -34,6 +33,7 @@ import {
} from "../certificate-authority-fns"; } from "../certificate-authority-fns";
import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal"; import { TCertificateAuthoritySecretDALFactory } from "../certificate-authority-secret-dal";
import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types"; import { TIssueCertWithTemplateDTO } from "./internal-certificate-authority-types";
import { validateAndMapAltNameType } from "../certificate-authority-validators";
type TInternalCertificateAuthorityFnsDeps = { type TInternalCertificateAuthorityFnsDeps = {
certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">; certificateAuthorityDAL: Pick<TCertificateAuthorityDALFactory, "findByIdWithAssociatedCa" | "findById">;
@@ -152,19 +152,15 @@ export const InternalCertificateAuthorityFns = ({
extensions.push(extendedKeyUsagesExtension); extensions.push(extendedKeyUsagesExtension);
} }
let altNamesArray: { type: "email" | "dns"; value: string }[] = []; let altNamesArray: TAltNameMapping[] = [];
if (subscriber.subjectAlternativeNames?.length) { if (subscriber.subjectAlternativeNames?.length) {
altNamesArray = subscriber.subjectAlternativeNames.map((altName) => { altNamesArray = subscriber.subjectAlternativeNames.map((altName) => {
if (z.string().email().safeParse(altName).success) { const altNameType = validateAndMapAltNameType(altName);
return { type: "email", value: altName }; if (!altNameType) {
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
} }
return altNameType;
if (isFQDN(altName, { allow_wildcard: true })) {
return { type: "dns", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
}); });
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -418,19 +414,15 @@ export const InternalCertificateAuthorityFns = ({
); );
} }
let altNamesArray: { type: "email" | "dns"; value: string }[] = []; let altNamesArray: TAltNameMapping[] = [];
if (altNames) { if (altNames) {
altNamesArray = altNames.split(",").map((altName) => { altNamesArray = altNames.split(",").map((altName) => {
if (z.string().email().safeParse(altName).success) { const altNameType = validateAndMapAltNameType(altName);
return { type: "email", value: altName }; if (!altNameType) {
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
} }
return altNameType;
if (isFQDN(altName, { allow_wildcard: true })) {
return { type: "dns", value: altName };
}
throw new BadRequestError({ message: `Invalid SAN entry: ${altName}` });
}); });
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -2,7 +2,6 @@
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import * as x509 from "@peculiar/x509"; import * as x509 from "@peculiar/x509";
import slugify from "@sindresorhus/slugify"; import slugify from "@sindresorhus/slugify";
import { z } from "zod";
import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas"; import { ActionProjectType, TableName, TCertificateAuthorities, TCertificateTemplates } from "@app/db/schemas";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
@@ -18,7 +17,6 @@ import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { ms } from "@app/lib/ms"; import { ms } from "@app/lib/ms";
import { alphaNumericNanoId } from "@app/lib/nanoid"; import { alphaNumericNanoId } from "@app/lib/nanoid";
import { isFQDN } from "@app/lib/validator/validate-url";
import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal"; import { TCertificateBodyDALFactory } from "@app/services/certificate/certificate-body-dal";
import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal"; import { TCertificateDALFactory } from "@app/services/certificate/certificate-dal";
import { TKmsServiceFactory } from "@app/services/kms/kms-service"; import { TKmsServiceFactory } from "@app/services/kms/kms-service";
@@ -34,7 +32,8 @@ import {
CertExtendedKeyUsageOIDToName, CertExtendedKeyUsageOIDToName,
CertKeyAlgorithm, CertKeyAlgorithm,
CertKeyUsage, CertKeyUsage,
CertStatus CertStatus,
TAltNameMapping
} from "../../certificate/certificate-types"; } from "../../certificate/certificate-types";
import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal"; import { TCertificateTemplateDALFactory } from "../../certificate-template/certificate-template-dal";
import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns"; import { validateCertificateDetailsAgainstTemplate } from "../../certificate-template/certificate-template-fns";
@@ -69,6 +68,7 @@ import {
TSignIntermediateDTO, TSignIntermediateDTO,
TUpdateCaDTO TUpdateCaDTO
} from "./internal-certificate-authority-types"; } from "./internal-certificate-authority-types";
import { validateAndMapAltNameType } from "../certificate-authority-validators";
type TInternalCertificateAuthorityServiceFactoryDep = { type TInternalCertificateAuthorityServiceFactoryDep = {
certificateAuthorityDAL: Pick< certificateAuthorityDAL: Pick<
@@ -1364,34 +1364,18 @@ export const internalCertificateAuthorityServiceFactory = ({
); );
} }
let altNamesArray: { let altNamesArray: TAltNameMapping[] = [];
type: "email" | "dns";
value: string;
}[] = [];
if (altNames) { if (altNames) {
altNamesArray = altNames altNamesArray = altNames
.split(",") .split(",")
.map((name) => name.trim()) .map((name) => name.trim())
.map((altName) => { .map((altName): TAltNameMapping => {
// check if the altName is a valid email const altNameType = validateAndMapAltNameType(altName);
if (z.string().email().safeParse(altName).success) { if (!altNameType) {
return { throw new Error(`Invalid altName: ${altName}`);
type: "email",
value: altName
};
} }
return altNameType;
// check if the altName is a valid hostname
if (isFQDN(altName, { allow_wildcard: true })) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
}); });
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false); const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
@@ -1766,34 +1750,22 @@ export const internalCertificateAuthorityServiceFactory = ({
} }
let altNamesFromCsr: string = ""; let altNamesFromCsr: string = "";
let altNamesArray: { let altNamesArray: TAltNameMapping[] = [];
type: "email" | "dns";
value: string;
}[] = [];
if (altNames) { if (altNames) {
altNamesArray = altNames altNamesArray = altNames
.split(",") .split(",")
.map((name) => name.trim()) .map((name) => name.trim())
.map((altName) => { .map((altName): TAltNameMapping => {
// check if the altName is a valid email const altNameType = validateAndMapAltNameType(altName);
if (z.string().email().safeParse(altName).success) { if (!altNameType) {
return { throw new Error(`Invalid altName: ${altName}`);
type: "email",
value: altName
};
} }
return altNameType;
// check if the altName is a valid hostname
if (isFQDN(altName, { allow_wildcard: true })) {
return {
type: "dns",
value: altName
};
}
// If altName is neither a valid email nor a valid hostname, throw an error or handle it accordingly
throw new Error(`Invalid altName: ${altName}`);
}); });
const altNamesExtension = new x509.SubjectAlternativeNameExtension(altNamesArray, false);
extensions.push(altNamesExtension);
} else { } else {
// attempt to read from CSR if altNames is not explicitly provided // attempt to read from CSR if altNames is not explicitly provided
const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17"); const sanExtension = csrObj.extensions.find((ext) => ext.type === "2.5.29.17");
@@ -1801,11 +1773,16 @@ export const internalCertificateAuthorityServiceFactory = ({
const sanNames = new x509.GeneralNames(sanExtension.value); const sanNames = new x509.GeneralNames(sanExtension.value);
altNamesArray = sanNames.items altNamesArray = sanNames.items
.filter((value) => value.type === "email" || value.type === "dns") .filter(
.map((name) => ({ (value) => value.type === "email" || value.type === "dns" || value.type === "url" || value.type === "ip"
type: name.type as "email" | "dns", )
value: name.value .map((name): TAltNameMapping => {
})); const altNameType = validateAndMapAltNameType(name.value);
if (!altNameType) {
throw new Error(`Invalid altName from CSR: ${name.value}`);
}
return altNameType;
});
altNamesFromCsr = sanNames.items.map((item) => item.value).join(","); altNamesFromCsr = sanNames.items.map((item) => item.value).join(",");
} }
@@ -104,3 +104,14 @@ export type TGetCertificateCredentialsDTO = {
projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">; projectDAL: Pick<TProjectDALFactory, "findOne" | "updateById" | "transaction">;
kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">; kmsService: Pick<TKmsServiceFactory, "decryptWithKmsKey" | "generateKmsKey">;
}; };
export enum TAltNameType {
EMAIL = "email",
DNS = "dns",
IP = "ip",
URL = "url"
}
export type TAltNameMapping = {
type: TAltNameType;
value: string;
};
@@ -1,6 +1,6 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ActionProjectType, ProjectMembershipRole, SecretKeyEncoding, TGroups } from "@app/db/schemas"; import { ActionProjectType, ProjectMembershipRole, ProjectVersion, SecretKeyEncoding, TGroups } from "@app/db/schemas";
import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types"; import { TListProjectGroupUsersDTO } from "@app/ee/services/group/group-types";
import { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
@@ -188,7 +188,7 @@ export const groupProjectServiceFactory = ({
// other groups that are in the project // other groups that are in the project
const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx); const groupMembers = await userGroupMembershipDAL.findGroupMembersNotInProject(group!.id, project.id, tx);
if (groupMembers.length) { if (groupMembers.length && (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2)) {
const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx); const ghostUser = await projectDAL.findProjectGhostUser(project.id, tx);
if (!ghostUser) { if (!ghostUser) {
@@ -205,6 +205,12 @@ export const groupProjectServiceFactory = ({
}); });
} }
if (!ghostUserLatestKey.sender.publicKey) {
throw new NotFoundError({
message: `Failed to find project owner's latest key in project with name ${project.name}`
});
}
const bot = await projectBotDAL.findOne({ projectId: project.id }, tx); const bot = await projectBotDAL.findOne({ projectId: project.id }, tx);
if (!bot) { if (!bot) {
@@ -231,6 +237,12 @@ export const groupProjectServiceFactory = ({
}); });
const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => { const projectKeyData = groupMembers.map(({ user: { publicKey, id } }) => {
if (!publicKey) {
throw new NotFoundError({
message: `Failed to find user's public key in project with name ${project.name}`
});
}
const { ciphertext: encryptedKey, nonce } = crypto const { ciphertext: encryptedKey, nonce } = crypto
.encryption() .encryption()
.asymmetric() .asymmetric()
@@ -2,9 +2,14 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { IdentityAuthMethod } from "@app/db/schemas"; import { IdentityAuthMethod } from "@app/db/schemas";
import { TIdentityAuthTemplateDALFactory } from "@app/ee/services/identity-auth-template";
import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns"; import { testLDAPConfig } from "@app/ee/services/ldap-config/ldap-fns";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { OrgPermissionIdentityActions, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import {
OrgPermissionIdentityActions,
OrgPermissionMachineIdentityAuthTemplateActions,
OrgPermissionSubjects
} from "@app/ee/services/permission/org-permission";
import { import {
constructPermissionErrorMessage, constructPermissionErrorMessage,
validatePrivilegeChangeOperation validatePrivilegeChangeOperation
@@ -44,6 +49,7 @@ type TIdentityLdapAuthServiceFactoryDep = {
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: TKmsServiceFactory; kmsService: TKmsServiceFactory;
identityDAL: TIdentityDALFactory; identityDAL: TIdentityDALFactory;
identityAuthTemplateDAL: TIdentityAuthTemplateDALFactory;
}; };
export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>; export type TIdentityLdapAuthServiceFactory = ReturnType<typeof identityLdapAuthServiceFactory>;
@@ -55,7 +61,8 @@ export const identityLdapAuthServiceFactory = ({
identityOrgMembershipDAL, identityOrgMembershipDAL,
licenseService, licenseService,
permissionService, permissionService,
kmsService kmsService,
identityAuthTemplateDAL
}: TIdentityLdapAuthServiceFactoryDep) => { }: TIdentityLdapAuthServiceFactoryDep) => {
const getLdapConfig = async (identityId: string) => { const getLdapConfig = async (identityId: string) => {
const identity = await identityDAL.findOne({ id: identityId }); const identity = await identityDAL.findOne({ id: identityId });
@@ -173,6 +180,7 @@ export const identityLdapAuthServiceFactory = ({
const attachLdapAuth = async ({ const attachLdapAuth = async ({
identityId, identityId,
templateId,
url, url,
searchBase, searchBase,
searchFilter, searchFilter,
@@ -213,6 +221,14 @@ export const identityLdapAuthServiceFactory = ({
actorOrgId actorOrgId
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Create, OrgPermissionSubjects.Identity);
if (templateId) {
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
if (!plan.ldap) { if (!plan.ldap) {
@@ -241,33 +257,55 @@ export const identityLdapAuthServiceFactory = ({
if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields); if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => { const identityLdapAuth = await identityLdapAuthDAL.transaction(async (tx) => {
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId orgId: identityMembershipOrg.orgId
}); });
const template = templateId
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
: undefined;
let ldapConfig: { bindDN: string; bindPass: string; searchBase: string; url: string; ldapCaCertificate?: string };
if (template) {
ldapConfig = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
} else {
if (!bindDN || !bindPass || !searchBase || !url) {
throw new BadRequestError({
message: "Invalid request. Missing bind DN, bind pass, search base, or URL."
});
}
ldapConfig = {
bindDN,
bindPass,
searchBase,
url,
ldapCaCertificate
};
}
const { cipherTextBlob: encryptedBindPass } = encryptor({ const { cipherTextBlob: encryptedBindPass } = encryptor({
plainText: Buffer.from(bindPass) plainText: Buffer.from(ldapConfig.bindPass)
});
const { cipherTextBlob: encryptedBindDN } = encryptor({
plainText: Buffer.from(ldapConfig.bindDN)
}); });
let encryptedLdapCaCertificate: Buffer | undefined; let encryptedLdapCaCertificate: Buffer | undefined;
if (ldapCaCertificate) { if (ldapConfig.ldapCaCertificate) {
const { cipherTextBlob: encryptedCertificate } = encryptor({ const { cipherTextBlob: encryptedCertificate } = encryptor({
plainText: Buffer.from(ldapCaCertificate) plainText: Buffer.from(ldapConfig.ldapCaCertificate)
}); });
encryptedLdapCaCertificate = encryptedCertificate; encryptedLdapCaCertificate = encryptedCertificate;
} }
const { cipherTextBlob: encryptedBindDN } = encryptor({
plainText: Buffer.from(bindDN)
});
const isConnected = await testLDAPConfig({ const isConnected = await testLDAPConfig({
bindDN, bindDN: ldapConfig.bindDN,
bindPass, bindPass: ldapConfig.bindPass,
caCert: ldapCaCertificate || "", caCert: ldapConfig.ldapCaCertificate || "",
url url: ldapConfig.url
}); });
if (!isConnected) { if (!isConnected) {
@@ -282,15 +320,16 @@ export const identityLdapAuthServiceFactory = ({
identityId: identityMembershipOrg.identityId, identityId: identityMembershipOrg.identityId,
encryptedBindDN, encryptedBindDN,
encryptedBindPass, encryptedBindPass,
searchBase, searchBase: ldapConfig.searchBase,
searchFilter, searchFilter,
url, url: ldapConfig.url,
encryptedLdapCaCertificate, encryptedLdapCaCertificate,
accessTokenMaxTTL, accessTokenMaxTTL,
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps), accessTokenTrustedIps: JSON.stringify(reformattedAccessTokenTrustedIps),
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
templateId
}, },
tx tx
); );
@@ -301,6 +340,7 @@ export const identityLdapAuthServiceFactory = ({
const updateLdapAuth = async ({ const updateLdapAuth = async ({
identityId, identityId,
templateId,
url, url,
searchBase, searchBase,
searchFilter, searchFilter,
@@ -344,6 +384,13 @@ export const identityLdapAuthServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity); ForbiddenError.from(permission).throwUnlessCan(OrgPermissionIdentityActions.Edit, OrgPermissionSubjects.Identity);
if (templateId) {
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionMachineIdentityAuthTemplateActions.AttachTemplates,
OrgPermissionSubjects.MachineIdentityAuthTemplate
);
}
const plan = await licenseService.getPlan(identityMembershipOrg.orgId); const plan = await licenseService.getPlan(identityMembershipOrg.orgId);
if (!plan.ldap) { if (!plan.ldap) {
@@ -371,33 +418,56 @@ export const identityLdapAuthServiceFactory = ({
if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields); if (allowedFields) AllowedFieldsSchema.array().parse(allowedFields);
const { encryptor } = await kmsService.createCipherPairWithDataKey({ const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization, type: KmsDataKey.Organization,
orgId: identityMembershipOrg.orgId orgId: identityMembershipOrg.orgId
}); });
const template = templateId
? await identityAuthTemplateDAL.findByIdAndOrgId(templateId, identityMembershipOrg.orgId)
: undefined;
let config: {
bindDN?: string;
bindPass?: string;
searchBase?: string;
url?: string;
ldapCaCertificate?: string;
};
if (template) {
config = JSON.parse(decryptor({ cipherTextBlob: template.templateFields }).toString());
} else {
config = {
bindDN,
bindPass,
searchBase,
url,
ldapCaCertificate
};
}
let encryptedBindPass: Buffer | undefined; let encryptedBindPass: Buffer | undefined;
if (bindPass) { if (config.bindPass) {
const { cipherTextBlob: bindPassCiphertext } = encryptor({ const { cipherTextBlob: bindPassCiphertext } = encryptor({
plainText: Buffer.from(bindPass) plainText: Buffer.from(config.bindPass)
}); });
encryptedBindPass = bindPassCiphertext; encryptedBindPass = bindPassCiphertext;
} }
let encryptedLdapCaCertificate: Buffer | undefined; let encryptedLdapCaCertificate: Buffer | undefined;
if (ldapCaCertificate) { if (config.ldapCaCertificate) {
const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({ const { cipherTextBlob: ldapCaCertificateCiphertext } = encryptor({
plainText: Buffer.from(ldapCaCertificate) plainText: Buffer.from(config.ldapCaCertificate)
}); });
encryptedLdapCaCertificate = ldapCaCertificateCiphertext; encryptedLdapCaCertificate = ldapCaCertificateCiphertext;
} }
let encryptedBindDN: Buffer | undefined; let encryptedBindDN: Buffer | undefined;
if (bindDN) { if (config.bindDN) {
const { cipherTextBlob: bindDNCiphertext } = encryptor({ const { cipherTextBlob: bindDNCiphertext } = encryptor({
plainText: Buffer.from(bindDN) plainText: Buffer.from(config.bindDN)
}); });
encryptedBindDN = bindDNCiphertext; encryptedBindDN = bindDNCiphertext;
@@ -406,10 +476,10 @@ export const identityLdapAuthServiceFactory = ({
const { ldapConfig } = await getLdapConfig(identityId); const { ldapConfig } = await getLdapConfig(identityId);
const isConnected = await testLDAPConfig({ const isConnected = await testLDAPConfig({
bindDN: bindDN || ldapConfig.bindDN, bindDN: config.bindDN || ldapConfig.bindDN,
bindPass: bindPass || ldapConfig.bindPass, bindPass: config.bindPass || ldapConfig.bindPass,
caCert: ldapCaCertificate || ldapConfig.caCert, caCert: config.ldapCaCertificate || ldapConfig.caCert,
url: url || ldapConfig.url url: config.url || ldapConfig.url
}); });
if (!isConnected) { if (!isConnected) {
@@ -420,14 +490,15 @@ export const identityLdapAuthServiceFactory = ({
} }
const updatedLdapAuth = await identityLdapAuthDAL.updateById(identityLdapAuth.id, { const updatedLdapAuth = await identityLdapAuthDAL.updateById(identityLdapAuth.id, {
url, url: config.url,
searchBase, searchBase: config.searchBase,
searchFilter, searchFilter,
encryptedBindDN, encryptedBindDN,
encryptedBindPass, encryptedBindPass,
encryptedLdapCaCertificate, encryptedLdapCaCertificate,
allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined, allowedFields: allowedFields ? JSON.stringify(allowedFields) : undefined,
accessTokenMaxTTL, accessTokenMaxTTL,
templateId: template?.id || null,
accessTokenTTL, accessTokenTTL,
accessTokenNumUsesLimit, accessTokenNumUsesLimit,
accessTokenTrustedIps: reformattedAccessTokenTrustedIps accessTokenTrustedIps: reformattedAccessTokenTrustedIps
@@ -14,11 +14,12 @@ export type TAllowedFields = z.infer<typeof AllowedFieldsSchema>;
export type TAttachLdapAuthDTO = { export type TAttachLdapAuthDTO = {
identityId: string; identityId: string;
url: string; templateId?: string;
searchBase: string; url?: string;
searchBase?: string;
searchFilter: string; searchFilter: string;
bindDN: string; bindDN?: string;
bindPass: string; bindPass?: string;
ldapCaCertificate?: string; ldapCaCertificate?: string;
allowedFields?: TAllowedFields[]; allowedFields?: TAllowedFields[];
accessTokenTTL: number; accessTokenTTL: number;
@@ -30,6 +31,7 @@ export type TAttachLdapAuthDTO = {
export type TUpdateLdapAuthDTO = { export type TUpdateLdapAuthDTO = {
identityId: string; identityId: string;
templateId?: string;
url?: string; url?: string;
searchBase?: string; searchBase?: string;
searchFilter?: string; searchFilter?: string;
@@ -1,19 +1,16 @@
import { ForbiddenError } from "@casl/ability"; import { ForbiddenError } from "@casl/ability";
import { ProjectMembershipRole, ProjectVersion, SecretKeyEncoding } from "@app/db/schemas"; import { ProjectMembershipRole, ProjectVersion } from "@app/db/schemas";
import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission"; import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/services/permission/org-permission";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
import { crypto } from "@app/lib/crypto/cryptography";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal"; import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service"; import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal";
import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types"; import { TAccessProjectDTO, TListOrgProjectsDTO } from "./org-admin-types";
type TOrgAdminServiceFactoryDep = { type TOrgAdminServiceFactoryDep = {
@@ -25,7 +22,6 @@ type TOrgAdminServiceFactoryDep = {
>; >;
projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">; projectKeyDAL: Pick<TProjectKeyDALFactory, "findLatestProjectKey" | "create">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne">; projectBotDAL: Pick<TProjectBotDALFactory, "findOne">;
userDAL: Pick<TUserDALFactory, "findUserEncKeyByUserId">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
}; };
@@ -38,7 +34,6 @@ export const orgAdminServiceFactory = ({
projectMembershipDAL, projectMembershipDAL,
projectKeyDAL, projectKeyDAL,
projectBotDAL, projectBotDAL,
userDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
smtpService smtpService
}: TOrgAdminServiceFactoryDep) => { }: TOrgAdminServiceFactoryDep) => {
@@ -83,7 +78,7 @@ export const orgAdminServiceFactory = ({
actorAuthMethod, actorAuthMethod,
projectId projectId
}: TAccessProjectDTO) => { }: TAccessProjectDTO) => {
const { permission, membership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
actorOrgId, actorOrgId,
@@ -98,8 +93,10 @@ export const orgAdminServiceFactory = ({
const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId }); const project = await projectDAL.findOne({ id: projectId, orgId: actorOrgId });
if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` }); if (!project) throw new NotFoundError({ message: `Project with ID '${projectId}' not found` });
if (project.version === ProjectVersion.V1) { if (project.version === ProjectVersion.V1 || project.version === ProjectVersion.V2) {
throw new BadRequestError({ message: "Please upgrade your project on your dashboard" }); throw new BadRequestError({
message: `Project '${project.name}' is a legacy project and must be upgraded before accessing it through the admin console.`
});
} }
// check already there exist a membership if there return it // check already there exist a membership if there return it
@@ -144,30 +141,6 @@ export const orgAdminServiceFactory = ({
}); });
} }
const botPrivateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const userEncryptionKey = await userDAL.findUserEncKeyByUserId(actorId);
if (!userEncryptionKey)
throw new NotFoundError({ message: `User encryption key for user with ID '${actorId}' not found` });
const [newWsMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: [
{
orgMembershipId: membership.id,
userPublicKey: userEncryptionKey.publicKey
}
]
});
const updatedMembership = await projectMembershipDAL.transaction(async (tx) => { const updatedMembership = await projectMembershipDAL.transaction(async (tx) => {
const newProjectMembership = await projectMembershipDAL.create( const newProjectMembership = await projectMembershipDAL.create(
{ {
@@ -181,16 +154,6 @@ export const orgAdminServiceFactory = ({
tx tx
); );
await projectKeyDAL.create(
{
encryptedKey: newWsMember.workspaceEncryptedKey,
nonce: newWsMember.workspaceEncryptedNonce,
senderId: ghostUser.id,
receiverId: actorId,
projectId
},
tx
);
return newProjectMembership; return newProjectMembership;
}); });
+2 -144
View File
@@ -8,7 +8,6 @@ import {
OrgMembershipStatus, OrgMembershipStatus,
ProjectMembershipRole, ProjectMembershipRole,
ProjectVersion, ProjectVersion,
SecretKeyEncoding,
TableName, TableName,
TProjectMemberships, TProjectMemberships,
TProjectUserMembershipRolesInsert, TProjectUserMembershipRolesInsert,
@@ -58,8 +57,6 @@ import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
import { TokenType } from "../auth-token/auth-token-types"; import { TokenType } from "../auth-token/auth-token-types";
import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal"; import { TIdentityMetadataDALFactory } from "../identity/identity-metadata-dal";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
import { assignWorkspaceKeysToMembers, createProjectKey } from "../project/project-fns";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal"; import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
@@ -137,7 +134,6 @@ type TOrgServiceFactoryDep = {
>; >;
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">; projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find">;
projectBotDAL: Pick<TProjectBotDALFactory, "findOne" | "updateById">;
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">; projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "insertMany" | "create">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
loginService: Pick<TAuthLoginFactory, "generateUserTokens">; loginService: Pick<TAuthLoginFactory, "generateUserTokens">;
@@ -169,7 +165,6 @@ export const orgServiceFactory = ({
projectRoleDAL, projectRoleDAL,
samlConfigDAL, samlConfigDAL,
oidcConfigDAL, oidcConfigDAL,
projectBotDAL,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
identityMetadataDAL, identityMetadataDAL,
projectBotService, projectBotService,
@@ -287,15 +282,7 @@ export const orgServiceFactory = ({
user.id, user.id,
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey: encKeys.protectedKey, publicKey: encKeys.publicKey
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier
}, },
tx tx
); );
@@ -885,29 +872,10 @@ export const orgServiceFactory = ({
// So what we do is we generate a random secure password and then encrypt it with a random pub-private key // So what we do is we generate a random secure password and then encrypt it with a random pub-private key
// Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password // Then when user sign in (as login is not possible as isAccepted is false) we rencrypt the private key with the user password
if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) { if (!inviteeUser || (inviteeUser && !inviteeUser?.isAccepted && !existingEncrytionKey)) {
const serverGeneratedPassword = crypto.randomBytes(32).toString("hex");
const { tag, encoding, ciphertext, iv } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(serverGeneratedPassword);
const encKeys = await generateUserSrpKeys(inviteeEmail, serverGeneratedPassword);
await userDAL.createUserEncryption( await userDAL.createUserEncryption(
{ {
userId: inviteeUserId, userId: inviteeUserId,
encryptionVersion: 2, encryptionVersion: 2
protectedKey: encKeys.protectedKey,
protectedKeyIV: encKeys.protectedKeyIV,
protectedKeyTag: encKeys.protectedKeyTag,
publicKey: encKeys.publicKey,
encryptedPrivateKey: encKeys.encryptedPrivateKey,
iv: encKeys.encryptedPrivateKeyIV,
tag: encKeys.encryptedPrivateKeyTag,
salt: encKeys.salt,
verifier: encKeys.verifier,
serverEncryptedPrivateKeyEncoding: encoding,
serverEncryptedPrivateKeyTag: tag,
serverEncryptedPrivateKeyIV: iv,
serverEncryptedPrivateKey: ciphertext
}, },
tx tx
); );
@@ -1069,106 +1037,6 @@ export const orgServiceFactory = ({
const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug); const customRolesGroupBySlug = groupBy(customRoles, ({ slug }) => slug);
// this will auto generate bot
const { botKey, bot: autoGeneratedBot } = await projectBotService.getBotKey(projectId, true);
const ghostUser = await projectDAL.findProjectGhostUser(projectId, tx);
let ghostUserId = ghostUser?.id;
// backfill missing ghost user
if (!ghostUserId) {
const newGhostUser = await addGhostUser(project.orgId, tx);
const projectMembership = await projectMembershipDAL.create(
{
userId: newGhostUser.user.id,
projectId: project.id
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: newGhostUser.keys.publicKey,
privateKey: newGhostUser.keys.plainPrivateKey,
plainProjectKey: botKey
});
// 4. Save the project key for the ghost user.
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: newGhostUser.user.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: newGhostUser.user.id
},
tx
);
const { iv, tag, ciphertext, encoding, algorithm } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(newGhostUser.keys.plainPrivateKey);
if (autoGeneratedBot) {
await projectBotDAL.updateById(
autoGeneratedBot.id,
{
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: newGhostUser.keys.publicKey,
senderId: newGhostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
}
ghostUserId = newGhostUser.user.id;
}
const bot = await projectBotDAL.findOne({ projectId }, tx);
if (!bot) {
throw new NotFoundError({
name: "InviteUser",
message: `Failed to find project bot for project with ID '${projectId}'`
});
}
const ghostUserLatestKey = await projectKeyDAL.findLatestProjectKey(ghostUserId, projectId, tx);
if (!ghostUserLatestKey) {
throw new NotFoundError({
name: "InviteUser",
message: `Failed to find project owner's latest key for project with ID '${projectId}'`
});
}
const botPrivateKey = crypto
.encryption()
.symmetric()
.decryptWithRootEncryptionKey({
keyEncoding: bot.keyEncoding as SecretKeyEncoding,
iv: bot.iv,
tag: bot.tag,
ciphertext: bot.encryptedPrivateKey
});
const newWsMembers = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey,
userPrivateKey: botPrivateKey,
members: userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
orgMembershipId: userEnc.userId,
projectMembershipRole: ProjectMembershipRole.Admin,
userPublicKey: userEnc.publicKey
}))
});
const projectMemberships = await projectMembershipDAL.insertMany( const projectMemberships = await projectMembershipDAL.insertMany(
userWithEncryptionKeyInvitedToProject.map((userEnc) => ({ userWithEncryptionKeyInvitedToProject.map((userEnc) => ({
projectId, projectId,
@@ -1191,16 +1059,6 @@ export const orgServiceFactory = ({
}); });
await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx); await projectUserMembershipRoleDAL.insertMany(sanitizedProjectMembershipRoles, tx);
await projectKeyDAL.insertMany(
newWsMembers.map((el) => ({
encryptedKey: el.workspaceEncryptedKey,
nonce: el.workspaceEncryptedNonce,
senderId: ghostUserId,
receiverId: el.orgMembershipId,
projectId
})),
tx
);
mailsForProjectInvitation.push({ mailsForProjectInvitation.push({
email: userWithEncryptionKeyInvitedToProject email: userWithEncryptionKeyInvitedToProject
.filter((el) => !userIdsWithOrgInvitation.has(el.userId)) .filter((el) => !userIdsWithOrgInvitation.has(el.userId))
@@ -42,6 +42,13 @@ export const getBotKeyFnFactory = (
message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console.` message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console.`
}); });
} }
if (!projectV1Keys.senderPublicKey) {
throw new NotFoundError({
message: `Project bot not found for project with ID '${projectId}'. Please ask an administrator to log-in to the Infisical Console and upgrade the project.`
});
}
let userPrivateKey = ""; let userPrivateKey = "";
if ( if (
projectV1Keys?.serverEncryptedPrivateKey && projectV1Keys?.serverEncryptedPrivateKey &&
@@ -14,7 +14,7 @@ export const projectKeyDALFactory = (db: TDbClient) => {
userId: string, userId: string,
projectId: string, projectId: string,
tx?: Knex tx?: Knex
): Promise<(TProjectKeys & { sender: { publicKey: string } }) | undefined> => { ): Promise<(TProjectKeys & { sender: { publicKey?: string } }) | undefined> => {
try { try {
const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys) const projectKey = await (tx || db.replicaNode())(TableName.ProjectKeys)
.join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`) .join(TableName.Users, `${TableName.ProjectKeys}.senderId`, `${TableName.Users}.id`)
@@ -25,7 +25,7 @@ export const projectKeyDALFactory = (db: TDbClient) => {
.select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey)) .select(db.ref("publicKey").withSchema(TableName.UserEncryptionKey))
.first(); .first();
if (projectKey) { if (projectKey) {
return { ...projectKey, sender: { publicKey: projectKey.publicKey } }; return { ...projectKey, sender: { publicKey: projectKey.publicKey || undefined } };
} }
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Find latest project key" }); throw new DatabaseError({ error, name: "Find latest project key" });
@@ -10,6 +10,10 @@ import { TProjectDALFactory } from "@app/services/project/project-dal";
import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types"; import { AddUserToWsDTO, TBootstrapSshProjectDTO } from "./project-types";
export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => { export const assignWorkspaceKeysToMembers = ({ members, decryptKey, userPrivateKey }: AddUserToWsDTO) => {
if (!decryptKey.sender.publicKey) {
throw new Error("Decrypt key sender public key not found");
}
const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({ const plaintextProjectKey = crypto.encryption().asymmetric().decrypt({
ciphertext: decryptKey.encryptedKey, ciphertext: decryptKey.encryptedKey,
nonce: decryptKey.nonce, nonce: decryptKey.nonce,
@@ -121,6 +121,10 @@ export const projectQueueFactory = ({
tag: data.encryptedPrivateKey.encryptedKeyTag tag: data.encryptedPrivateKey.encryptedKeyTag
}); });
if (!oldProjectKey.sender.publicKey) {
throw new Error("Old project key sender public key not found");
}
const decryptedPlainProjectKey = crypto.encryption().asymmetric().decrypt({ const decryptedPlainProjectKey = crypto.encryption().asymmetric().decrypt({
ciphertext: oldProjectKey.encryptedKey, ciphertext: oldProjectKey.encryptedKey,
nonce: oldProjectKey.nonce, nonce: oldProjectKey.nonce,
@@ -290,6 +294,10 @@ export const projectQueueFactory = ({
continue; continue;
} }
if (!user.publicKey) {
throw new Error(`User with ID ${key.receiverId} has no public key during upgrade.`);
}
const [newMember] = assignWorkspaceKeysToMembers({ const [newMember] = assignWorkspaceKeysToMembers({
decryptKey: ghostUserLatestKey, decryptKey: ghostUserLatestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey, userPrivateKey: ghostUser.keys.plainPrivateKey,
+5 -100
View File
@@ -55,13 +55,10 @@ import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microso
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal"; import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal"; import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
import { TOrgDALFactory } from "../org/org-dal"; import { TOrgDALFactory } from "../org/org-dal";
import { TOrgServiceFactory } from "../org/org-service";
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal"; import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal"; import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
import { TProjectBotServiceFactory } from "../project-bot/project-bot-service"; import { TProjectBotServiceFactory } from "../project-bot/project-bot-service";
import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
import { TProjectKeyDALFactory } from "../project-key/project-key-dal";
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal"; import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal"; import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
import { TProjectRoleDALFactory } from "../project-role/project-role-dal"; import { TProjectRoleDALFactory } from "../project-role/project-role-dal";
@@ -78,7 +75,7 @@ import { SmtpTemplates, TSmtpService } from "../smtp/smtp-service";
import { TUserDALFactory } from "../user/user-dal"; import { TUserDALFactory } from "../user/user-dal";
import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types"; import { WorkflowIntegration, WorkflowIntegrationStatus } from "../workflow-integration/workflow-integration-types";
import { TProjectDALFactory } from "./project-dal"; import { TProjectDALFactory } from "./project-dal";
import { assignWorkspaceKeysToMembers, bootstrapSshProject, createProjectKey } from "./project-fns"; import { bootstrapSshProject } from "./project-fns";
import { TProjectQueueFactory } from "./project-queue"; import { TProjectQueueFactory } from "./project-queue";
import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal"; import { TProjectSshConfigDALFactory } from "./project-ssh-config-dal";
import { import {
@@ -123,6 +120,7 @@ export const DEFAULT_PROJECT_ENVS = [
type TProjectServiceFactoryDep = { type TProjectServiceFactoryDep = {
projectDAL: TProjectDALFactory; projectDAL: TProjectDALFactory;
identityProjectDAL: Pick<TIdentityProjectDALFactory, "create">;
projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">; projectSshConfigDAL: Pick<TProjectSshConfigDALFactory, "transaction" | "create" | "findOne" | "updateById">;
projectQueue: TProjectQueueFactory; projectQueue: TProjectQueueFactory;
userDAL: TUserDALFactory; userDAL: TUserDALFactory;
@@ -132,9 +130,7 @@ type TProjectServiceFactoryDep = {
secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">; secretV2BridgeDAL: Pick<TSecretV2BridgeDALFactory, "find">;
projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">; projectEnvDAL: Pick<TProjectEnvDALFactory, "insertMany" | "find">;
identityOrgMembershipDAL: TIdentityOrgDALFactory; identityOrgMembershipDAL: TIdentityOrgDALFactory;
identityProjectDAL: TIdentityProjectDALFactory;
identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">; identityProjectMembershipRoleDAL: Pick<TIdentityProjectMembershipRoleDALFactory, "create">;
projectKeyDAL: Pick<TProjectKeyDALFactory, "create" | "findLatestProjectKey" | "delete" | "find" | "insertMany">;
projectMembershipDAL: Pick< projectMembershipDAL: Pick<
TProjectMembershipDALFactory, TProjectMembershipDALFactory,
"create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers" "create" | "findProjectGhostUser" | "findOne" | "delete" | "findAllProjectMembers"
@@ -167,12 +163,10 @@ type TProjectServiceFactoryDep = {
sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">; sshHostDAL: Pick<TSshHostDALFactory, "find" | "findSshHostsWithLoginMappings">;
sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">; sshHostGroupDAL: Pick<TSshHostGroupDALFactory, "find" | "findSshHostGroupsWithLoginMappings">;
permissionService: TPermissionServiceFactory; permissionService: TPermissionServiceFactory;
orgService: Pick<TOrgServiceFactory, "addGhostUser">;
licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">; licenseService: Pick<TLicenseServiceFactory, "getPlan" | "invalidateGetPlan">;
smtpService: Pick<TSmtpService, "sendMail">; smtpService: Pick<TSmtpService, "sendMail">;
orgDAL: Pick<TOrgDALFactory, "findOne">; orgDAL: Pick<TOrgDALFactory, "findOne">;
keyStore: Pick<TKeyStoreFactory, "deleteItem">; keyStore: Pick<TKeyStoreFactory, "deleteItem">;
projectBotDAL: Pick<TProjectBotDALFactory, "create">;
projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">; projectRoleDAL: Pick<TProjectRoleDALFactory, "find" | "insertMany" | "delete">;
kmsService: Pick< kmsService: Pick<
TKmsServiceFactory, TKmsServiceFactory,
@@ -196,27 +190,25 @@ export const projectServiceFactory = ({
secretDAL, secretDAL,
secretV2BridgeDAL, secretV2BridgeDAL,
projectQueue, projectQueue,
projectKeyDAL,
permissionService, permissionService,
projectBotService, projectBotService,
orgDAL, orgDAL,
userDAL, userDAL,
folderDAL, folderDAL,
orgService,
identityProjectDAL,
identityOrgMembershipDAL, identityOrgMembershipDAL,
projectMembershipDAL, projectMembershipDAL,
projectEnvDAL, projectEnvDAL,
licenseService, licenseService,
projectUserMembershipRoleDAL, projectUserMembershipRoleDAL,
projectRoleDAL, projectRoleDAL,
identityProjectMembershipRoleDAL,
certificateAuthorityDAL, certificateAuthorityDAL,
certificateDAL, certificateDAL,
certificateTemplateDAL, certificateTemplateDAL,
pkiCollectionDAL, pkiCollectionDAL,
pkiAlertDAL, pkiAlertDAL,
pkiSubscriberDAL, pkiSubscriberDAL,
identityProjectDAL,
identityProjectMembershipRoleDAL,
sshCertificateAuthorityDAL, sshCertificateAuthorityDAL,
sshCertificateAuthoritySecretDAL, sshCertificateAuthoritySecretDAL,
sshCertificateDAL, sshCertificateDAL,
@@ -225,7 +217,6 @@ export const projectServiceFactory = ({
sshHostGroupDAL, sshHostGroupDAL,
keyStore, keyStore,
kmsService, kmsService,
projectBotDAL,
projectSlackConfigDAL, projectSlackConfigDAL,
projectMicrosoftTeamsConfigDAL, projectMicrosoftTeamsConfigDAL,
slackIntegrationDAL, slackIntegrationDAL,
@@ -253,7 +244,7 @@ export const projectServiceFactory = ({
type = ProjectType.SecretManager type = ProjectType.SecretManager
}: TCreateProjectDTO) => { }: TCreateProjectDTO) => {
const organization = await orgDAL.findOne({ id: actorOrgId }); const organization = await orgDAL.findOne({ id: actorOrgId });
const { permission, membership: orgMembership } = await permissionService.getOrgPermission( const { permission } = await permissionService.getOrgPermission(
actor, actor,
actorId, actorId,
organization.id, organization.id,
@@ -277,7 +268,6 @@ export const projectServiceFactory = ({
message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces." message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
}); });
} }
const ghostUser = await orgService.addGhostUser(organization.id, tx);
if (kmsKeyId) { if (kmsKeyId) {
const kms = await kmsService.getKmsById(kmsKeyId, tx); const kms = await kmsService.getKmsById(kmsKeyId, tx);
@@ -329,19 +319,6 @@ export const projectServiceFactory = ({
}); });
} }
// set ghost user as admin of project
const projectMembership = await projectMembershipDAL.create(
{
userId: ghostUser.user.id,
projectId: project.id
},
tx
);
await projectUserMembershipRoleDAL.create(
{ projectMembershipId: projectMembership.id, role: ProjectMembershipRole.Admin },
tx
);
// set default environments and root folder for provided environments // set default environments and root folder for provided environments
let envs: TProjectEnvironments[] = []; let envs: TProjectEnvironments[] = [];
if (projectTemplate) { if (projectTemplate) {
@@ -374,55 +351,6 @@ export const projectServiceFactory = ({
); );
} }
// 3. Create a random key that we'll use as the project key.
const { key: encryptedProjectKey, iv: encryptedProjectKeyIv } = createProjectKey({
publicKey: ghostUser.keys.publicKey,
privateKey: ghostUser.keys.plainPrivateKey
});
// 4. Save the project key for the ghost user.
await projectKeyDAL.create(
{
projectId: project.id,
receiverId: ghostUser.user.id,
encryptedKey: encryptedProjectKey,
nonce: encryptedProjectKeyIv,
senderId: ghostUser.user.id
},
tx
);
const { iv, tag, ciphertext, encoding, algorithm } = crypto
.encryption()
.symmetric()
.encryptWithRootEncryptionKey(ghostUser.keys.plainPrivateKey);
// 5. Create & a bot for the project
await projectBotDAL.create(
{
name: "Infisical Bot (Ghost)",
projectId: project.id,
tag,
iv,
encryptedProjectKey,
encryptedProjectKeyNonce: encryptedProjectKeyIv,
encryptedPrivateKey: ciphertext,
isActive: true,
publicKey: ghostUser.keys.publicKey,
senderId: ghostUser.user.id,
algorithm,
keyEncoding: encoding
},
tx
);
// Find the ghost users latest key
const latestKey = await projectKeyDAL.findLatestProjectKey(ghostUser.user.id, project.id, tx);
if (!latestKey) {
throw new Error("Latest key not found for user");
}
// If the project is being created by a user, add the user to the project as an admin // If the project is being created by a user, add the user to the project as an admin
if (actor === ActorType.USER) { if (actor === ActorType.USER) {
// Find public key of user // Find public key of user
@@ -432,17 +360,6 @@ export const projectServiceFactory = ({
throw new Error("User not found"); throw new Error("User not found");
} }
const [projectAdmin] = assignWorkspaceKeysToMembers({
decryptKey: latestKey,
userPrivateKey: ghostUser.keys.plainPrivateKey,
members: [
{
userPublicKey: user.publicKey,
orgMembershipId: orgMembership.id
}
]
});
// Create a membership for the user // Create a membership for the user
const userProjectMembership = await projectMembershipDAL.create( const userProjectMembership = await projectMembershipDAL.create(
{ {
@@ -455,18 +372,6 @@ export const projectServiceFactory = ({
{ projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin }, { projectMembershipId: userProjectMembership.id, role: ProjectMembershipRole.Admin },
tx tx
); );
// Create a project key for the user
await projectKeyDAL.create(
{
encryptedKey: projectAdmin.workspaceEncryptedKey,
nonce: projectAdmin.workspaceEncryptedNonce,
senderId: ghostUser.user.id,
receiverId: user.id,
projectId: project.id
},
tx
);
} }
// If the project is being created by an identity, add the identity to the project as an admin // If the project is being created by an identity, add the identity to the project as an admin
@@ -117,7 +117,7 @@ export type TUpgradeProjectDTO = {
} & TProjectPermission; } & TProjectPermission;
export type AddUserToWsDTO = { export type AddUserToWsDTO = {
decryptKey: TProjectKeys & { sender: { publicKey: string } }; decryptKey: TProjectKeys & { sender: { publicKey?: string } };
userPrivateKey: string; userPrivateKey: string;
members: { members: {
orgMembershipId: string; orgMembershipId: string;
@@ -42,7 +42,7 @@ export const reminderServiceFactory = ({
const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise<void> => { const $manageReminderRecipients = async (reminderId: string, newRecipients?: string[] | null): Promise<void> => {
if (!newRecipients || newRecipients.length === 0) { if (!newRecipients || newRecipients.length === 0) {
// If no recipients provided, remove all existing recipients // If no recipients provided, remove all existing recipients
await reminderRecipientDAL.deleteById(reminderId); await reminderRecipientDAL.delete({ reminderId });
return; return;
} }
@@ -79,25 +79,33 @@ export const reminderServiceFactory = ({
repeatDays, repeatDays,
nextReminderDate: nextReminderDateInput, nextReminderDate: nextReminderDateInput,
recipients, recipients,
projectId projectId,
fromDate: fromDateInput
}: { }: {
secretId?: string; secretId?: string;
message?: string | null; message?: string | null;
repeatDays?: number | null; repeatDays?: number | null;
nextReminderDate?: string | null; nextReminderDate?: string | null;
recipients?: string[] | null; recipients?: string[] | null;
fromDate?: string | null;
projectId: string; projectId: string;
}) => { }) => {
if (!secretId) { if (!secretId) {
throw new BadRequestError({ message: "secretId is required" }); throw new BadRequestError({ message: "secretId is required" });
} }
let nextReminderDate; let nextReminderDate;
let fromDate;
if (nextReminderDateInput) { if (nextReminderDateInput) {
nextReminderDate = new Date(nextReminderDateInput); nextReminderDate = new Date(nextReminderDateInput);
} }
if (repeatDays && repeatDays > 0) { if (repeatDays) {
nextReminderDate = $addDays(repeatDays); if (fromDateInput) {
fromDate = new Date(fromDateInput);
nextReminderDate = fromDate;
} else {
nextReminderDate = $addDays(repeatDays);
}
} }
if (!nextReminderDate) { if (!nextReminderDate) {
@@ -112,7 +120,8 @@ export const reminderServiceFactory = ({
await reminderDAL.updateById(existingReminder.id, { await reminderDAL.updateById(existingReminder.id, {
message, message,
repeatDays, repeatDays,
nextReminderDate nextReminderDate,
fromDate
}); });
reminderId = existingReminder.id; reminderId = existingReminder.id;
} else { } else {
@@ -121,7 +130,8 @@ export const reminderServiceFactory = ({
secretId, secretId,
message, message,
repeatDays, repeatDays,
nextReminderDate nextReminderDate,
fromDate
}); });
reminderId = newReminder.id; reminderId = newReminder.id;
} }
@@ -280,14 +290,28 @@ export const reminderServiceFactory = ({
} }
const processedReminders = remindersData.map( const processedReminders = remindersData.map(
({ secretId, message, repeatDays, nextReminderDate: nextReminderDateInput, recipients, projectId }) => { ({
secretId,
message,
repeatDays,
nextReminderDate: nextReminderDateInput,
recipients,
projectId,
fromDate: fromDateInput
}) => {
let nextReminderDate; let nextReminderDate;
let fromDate;
if (nextReminderDateInput) { if (nextReminderDateInput) {
nextReminderDate = new Date(nextReminderDateInput); nextReminderDate = new Date(nextReminderDateInput);
} }
if (repeatDays && repeatDays > 0 && !nextReminderDate) { if (repeatDays && !nextReminderDate) {
nextReminderDate = $addDays(repeatDays); if (fromDateInput) {
fromDate = new Date(fromDateInput);
nextReminderDate = fromDate;
} else {
nextReminderDate = $addDays(repeatDays);
}
} }
if (!nextReminderDate) { if (!nextReminderDate) {
@@ -302,17 +326,19 @@ export const reminderServiceFactory = ({
repeatDays, repeatDays,
nextReminderDate, nextReminderDate,
recipients: recipients ? [...new Set(recipients)] : [], recipients: recipients ? [...new Set(recipients)] : [],
projectId projectId,
fromDate
}; };
} }
); );
const newReminders = await reminderDAL.insertMany( const newReminders = await reminderDAL.insertMany(
processedReminders.map(({ secretId, message, repeatDays, nextReminderDate }) => ({ processedReminders.map(({ secretId, message, repeatDays, nextReminderDate, fromDate }) => ({
secretId, secretId,
message, message,
repeatDays, repeatDays,
nextReminderDate nextReminderDate,
fromDate
})), })),
tx tx
); );
@@ -8,6 +8,7 @@ export type TReminder = {
message?: string | null; message?: string | null;
repeatDays?: number | null; repeatDays?: number | null;
nextReminderDate: Date; nextReminderDate: Date;
fromDate?: Date | null;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;
}; };
@@ -21,6 +22,7 @@ export type TCreateReminderDTO = {
secretId?: string; secretId?: string;
message?: string | null; message?: string | null;
repeatDays?: number | null; repeatDays?: number | null;
fromDate?: string | null;
nextReminderDate?: string | null; nextReminderDate?: string | null;
recipients?: string[] | null; recipients?: string[] | null;
}; };
@@ -31,6 +33,7 @@ export type TBatchCreateReminderDTO = {
message?: string | null; message?: string | null;
repeatDays?: number | null; repeatDays?: number | null;
nextReminderDate?: string | Date | null; nextReminderDate?: string | Date | null;
fromDate?: Date | null;
recipients?: string[] | null; recipients?: string[] | null;
projectId?: string; projectId?: string;
}[]; }[];
@@ -95,6 +98,7 @@ export interface TReminderServiceFactory {
nextReminderDate?: string | null; nextReminderDate?: string | null;
recipients?: string[] | null; recipients?: string[] | null;
projectId: string; projectId: string;
fromDate?: string | null;
}) => Promise<{ }) => Promise<{
id: string; id: string;
created: boolean; created: boolean;
@@ -3,6 +3,7 @@ import sodium from "libsodium-wrappers";
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { import {
getGitHubAppAuthToken, getGitHubAppAuthToken,
getGitHubInstanceApiUrl,
GitHubConnectionMethod, GitHubConnectionMethod,
makePaginatedGitHubRequest, makePaginatedGitHubRequest,
requestWithGitHubGateway requestWithGitHubGateway
@@ -73,7 +74,7 @@ const getPublicKey = async (
} }
const response = await requestWithGitHubGateway<TGitHubPublicKey>(connection, gatewayService, { const response = await requestWithGitHubGateway<TGitHubPublicKey>(connection, gatewayService, {
url: `https://api.${connection.credentials.host || "github.com"}${path}`, url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`,
method: "GET", method: "GET",
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
@@ -111,7 +112,7 @@ const deleteSecret = async (
} }
await requestWithGitHubGateway(connection, gatewayService, { await requestWithGitHubGateway(connection, gatewayService, {
url: `https://api.${connection.credentials.host || "github.com"}${path}`, url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`,
method: "DELETE", method: "DELETE",
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
@@ -157,7 +158,7 @@ const putSecret = async (
} }
await requestWithGitHubGateway(connection, gatewayService, { await requestWithGitHubGateway(connection, gatewayService, {
url: `https://api.${connection.credentials.host || "github.com"}${path}`, url: `https://${await getGitHubInstanceApiUrl(connection)}${path}`,
method: "PUT", method: "PUT",
headers: { headers: {
Accept: "application/vnd.github+json", Accept: "application/vnd.github+json",
@@ -30,6 +30,7 @@ const baseSecretSyncQuery = ({ filter, db, tx }: { db: TDbClient; filter?: Secre
db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"), db.ref("encryptedCredentials").withSchema(TableName.AppConnection).as("connectionEncryptedCredentials"),
db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"), db.ref("description").withSchema(TableName.AppConnection).as("connectionDescription"),
db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"), db.ref("version").withSchema(TableName.AppConnection).as("connectionVersion"),
db.ref("gatewayId").withSchema(TableName.AppConnection).as("connectionGatewayId"),
db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"), db.ref("createdAt").withSchema(TableName.AppConnection).as("connectionCreatedAt"),
db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"), db.ref("updatedAt").withSchema(TableName.AppConnection).as("connectionUpdatedAt"),
db db
@@ -65,6 +66,7 @@ const expandSecretSync = (
connectionUpdatedAt, connectionUpdatedAt,
connectionVersion, connectionVersion,
connectionIsPlatformManagedCredentials, connectionIsPlatformManagedCredentials,
connectionGatewayId,
...el ...el
} = secretSync; } = secretSync;
@@ -83,7 +85,8 @@ const expandSecretSync = (
createdAt: connectionCreatedAt, createdAt: connectionCreatedAt,
updatedAt: connectionUpdatedAt, updatedAt: connectionUpdatedAt,
version: connectionVersion, version: connectionVersion,
isPlatformManagedCredentials: connectionIsPlatformManagedCredentials isPlatformManagedCredentials: connectionIsPlatformManagedCredentials,
gatewayId: connectionGatewayId
}, },
folder: folder folder: folder
? { ? {
@@ -1,7 +1,9 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface AccessApprovalRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface AccessApprovalRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
projectName: string; projectName: string;
@@ -38,18 +40,15 @@ export const AccessApprovalRequestTemplate = ({
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0"> <Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
You have a new access approval request pending review for the project <strong>{projectName}</strong> You have a new access approval request pending review for the project <strong>{projectName}</strong>
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]"> <Text className="text-black text-[14px] leading-[24px]">
<strong>{requesterFullName}</strong> ( <strong>{requesterFullName}</strong> (<BaseLink href={`mailto:${requesterEmail}`}>{requesterEmail}</BaseLink>)
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline"> has requested {isTemporary ? "temporary" : "permanent"} access to <strong>{secretPath}</strong> in the{" "}
{requesterEmail}
</Link>
) has requested {isTemporary ? "temporary" : "permanent"} access to <strong>{secretPath}</strong> in the{" "}
<strong>{environment}</strong> environment. <strong>{environment}</strong> environment.
</Text> </Text>
{isTemporary && ( {isTemporary && (
<Text className="text-[14px] text-red-500 leading-[24px]"> <Text className="text-[14px] text-red-600 leading-[24px]">
<strong>This access will expire {expiresIn} after approval.</strong> <strong>This access will expire {expiresIn} after approval.</strong>
</Text> </Text>
)} )}
@@ -67,13 +66,8 @@ export const AccessApprovalRequestTemplate = ({
</Text> </Text>
)} )}
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <BaseButton href={approvalUrl}>Review Request</BaseButton>
href={approvalUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Review Request
</Button>
</Section> </Section>
</BaseEmailWrapper> </BaseEmailWrapper>
); );
@@ -0,0 +1,18 @@
import { Button } from "@react-email/components";
import React from "react";
type Props = {
href: string;
children: string;
};
export const BaseButton = ({ href, children }: Props) => {
return (
<Button
href={href}
className="rounded-[8px] py-[12px] px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
{children}
</Button>
);
};
@@ -16,23 +16,21 @@ export const BaseEmailWrapper = ({ title, preview, children, siteUrl }: BaseEmai
<Body className="bg-gray-300 my-auto mx-auto font-sans px-[8px] py-[4px]"> <Body className="bg-gray-300 my-auto mx-auto font-sans px-[8px] py-[4px]">
<Preview>{preview}</Preview> <Preview>{preview}</Preview>
<Container className="bg-white rounded-xl my-[40px] mx-auto pb-[0px] max-w-[500px]"> <Container className="bg-white rounded-xl my-[40px] mx-auto pb-[0px] max-w-[500px]">
<Section className="border-0 border-b border-[#d1e309] border-solid bg-[#EBF852] mb-[44px] h-[10px] rounded-t-xl" /> <Section className="mb-[24px] px-[24px] mt-[24px]">
<Section className="px-[32px] mb-[18px]"> <Img
<Section className="w-[48px] h-[48px] border border-solid border-gray-300 rounded-full bg-gray-100 mx-auto"> src="https://infisical.com/_next/image?url=%2Fimages%2Flogo-black.png&w=64&q=75"
<Img width="36"
src={`https://infisical.com/_next/image?url=%2Fimages%2Flogo-black.png&w=64&q=75`} alt="Infisical Logo"
width="32" className="mx-auto"
alt="Infisical Logo" />
className="mx-auto"
/>
</Section>
</Section> </Section>
<Hr className=" mb-[32px] mt-[0px] h-[1px]" />
<Section className="px-[28px]">{children}</Section> <Section className="px-[28px]">{children}</Section>
<Hr className=" mt-[32px] mb-[0px] h-[1px]" /> <Hr className=" mt-[32px] mb-[0px] h-[1px]" />
<Section className="px-[24px] text-center"> <Section className="px-[24px] text-center">
<Text className="text-gray-500 text-[12px]"> <Text className="text-gray-500 text-[12px]">
Email sent via{" "} Email sent via{" "}
<Link href={siteUrl} className="text-slate-700 no-underline"> <Link href={siteUrl} className="text-slate-700 underline decoration-slate-700">
Infisical Infisical
</Link> </Link>
</Text> </Text>
@@ -0,0 +1,15 @@
import { Link } from "@react-email/components";
import React from "react";
type Props = {
href: string;
children: string;
};
export const BaseLink = ({ href, children }: Props) => {
return (
<Link href={href} className="text-slate-700 underline decoration-slate-700">
{children}
</Link>
);
};
@@ -1,7 +1,8 @@
import { Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface EmailMfaTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface EmailMfaTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
code: string; code: string;
@@ -25,11 +26,7 @@ export const EmailMfaTemplate = ({ code, siteUrl, isCloud }: EmailMfaTemplatePro
<strong>Not you?</strong>{" "} <strong>Not you?</strong>{" "}
{isCloud ? ( {isCloud ? (
<> <>
Contact us at{" "} Contact us at <BaseLink href="mailto:[email protected]">support@infisical.com</BaseLink> immediately
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
support@infisical.com
</Link>{" "}
immediately
</> </>
) : ( ) : (
"Contact your administrator immediately" "Contact your administrator immediately"
@@ -1,7 +1,8 @@
import { Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface EmailVerificationTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface EmailVerificationTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
code: string; code: string;
@@ -29,10 +30,7 @@ export const EmailVerificationTemplate = ({ code, siteUrl, isCloud }: EmailVerif
<strong>Questions about Infisical?</strong>{" "} <strong>Questions about Infisical?</strong>{" "}
{isCloud ? ( {isCloud ? (
<> <>
Email us at{" "} Email us at <BaseLink href="mailto:[email protected]">support@infisical.com</BaseLink>
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
support@infisical.com
</Link>
</> </>
) : ( ) : (
"Contact your administrator" "Contact your administrator"
@@ -1,7 +1,8 @@
import { Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface ExternalImportFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface ExternalImportFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
error: string; error: string;
@@ -21,12 +22,9 @@ export const ExternalImportFailedTemplate = ({ error, siteUrl, provider }: Exter
</Text> </Text>
<Text className="text-black text-[14px] leading-[24px]"> <Text className="text-black text-[14px] leading-[24px]">
If your issue persists, you can contact the Infisical team at{" "} If your issue persists, you can contact the Infisical team at{" "}
<Link href="mailto:[email protected]" className="text-slate-700 no-underline"> <BaseLink href="mailto:[email protected]">support@infisical.com</BaseLink>.
support@infisical.com
</Link>
.
</Text> </Text>
<Text className="text-[14px] text-red-500 leading-[24px]"> <Text className="text-[14px] text-red-600 leading-[24px]">
<strong>Error:</strong> "{error}" <strong>Error:</strong> "{error}"
</Text> </Text>
</Section> </Section>
@@ -1,6 +1,7 @@
import { Button, Heading, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface IntegrationSyncFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface IntegrationSyncFailedTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
@@ -30,7 +31,7 @@ export const IntegrationSyncFailedTemplate = ({
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0"> <Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>{count}</strong> integration(s) failed to sync <strong>{count}</strong> integration(s) failed to sync
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[26px] pb-[4px] text-[14px] border border-solid border-gray-200 rounded-md bg-gray-50">
<strong>Project</strong> <strong>Project</strong>
<Text className="text-[14px] mt-[4px]">{projectName}</Text> <Text className="text-[14px] mt-[4px]">{projectName}</Text>
<strong>Environment</strong> <strong>Environment</strong>
@@ -38,15 +39,10 @@ export const IntegrationSyncFailedTemplate = ({
<strong>Secret Path</strong> <strong>Secret Path</strong>
<Text className="text-[14px] mt-[4px]">{secretPath}</Text> <Text className="text-[14px] mt-[4px]">{secretPath}</Text>
<strong className="text-black">Failure Reason:</strong> <strong className="text-black">Failure Reason:</strong>
<Text className="text-[14px] mt-[4px] text-red-500 leading-[24px]">"{syncMessage}"</Text> <Text className="text-[14px] mt-[4px] text-red-600 leading-[24px]">"{syncMessage}"</Text>
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <BaseButton href={integrationUrl}>View Integrations</BaseButton>
href={integrationUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
View Integrations
</Button>
</Section> </Section>
</BaseEmailWrapper> </BaseEmailWrapper>
); );
@@ -1,7 +1,8 @@
import { Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface NewDeviceLoginTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface NewDeviceLoginTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
email: string; email: string;
@@ -42,9 +43,7 @@ export const NewDeviceLoginTemplate = ({
<Text className="mb-[0px]"> <Text className="mb-[0px]">
If you believe that this login is suspicious, please contact{" "} If you believe that this login is suspicious, please contact{" "}
{isCloud ? ( {isCloud ? (
<Link href="mailto:[email protected]" className="text-slate-700 no-underline"> <BaseLink href="mailto:[email protected]">support@infisical.com</BaseLink>
support@infisical.com
</Link>
) : ( ) : (
"your administrator" "your administrator"
)}{" "} )}{" "}
@@ -1,7 +1,8 @@
import { Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface OrgAdminBreakglassAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface OrgAdminBreakglassAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
email: string; email: string;
@@ -35,10 +36,7 @@ export const OrgAdminBreakglassAccessTemplate = ({
<Text className="text-[14px] mt-[4px]">{userAgent}</Text> <Text className="text-[14px] mt-[4px]">{userAgent}</Text>
<Text className="text-[14px]"> <Text className="text-[14px]">
If you'd like to disable Admin SSO Bypass, please visit{" "} If you'd like to disable Admin SSO Bypass, please visit{" "}
<Link href={`${siteUrl}/organization/settings`} className="text-slate-700 no-underline"> <BaseLink href={`${siteUrl}/organization/settings`}>Organization Security Settings</BaseLink>.
Organization Security Settings
</Link>
.
</Text> </Text>
</Section> </Section>
</BaseEmailWrapper> </BaseEmailWrapper>
@@ -2,6 +2,7 @@ import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface OrgAdminProjectGrantAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview"> { interface OrgAdminProjectGrantAccessTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview"> {
email: string; email: string;
@@ -24,8 +25,8 @@ export const OrgAdminProjectGrantAccessTemplate = ({
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[24px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mt-[36px] pt-[24px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px] mt-[4px]"> <Text className="text-[14px] mt-[4px]">
The organization admin <strong>{email}</strong> has self-issued direct access to the project{" "} The organization admin <BaseLink href={`mailto:${email}`}>{email}</BaseLink> has self-issued direct access to
<strong>{projectName}</strong>. the project <strong>{projectName}</strong>.
</Text> </Text>
</Section> </Section>
</BaseEmailWrapper> </BaseEmailWrapper>
@@ -1,7 +1,9 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface OrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> { interface OrganizationInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
metadata?: string; metadata?: string;
@@ -36,15 +38,13 @@ export const OrganizationInvitationTemplate = ({
<br /> <br />
<strong>{organizationName}</strong> on <strong>Infisical</strong> <strong>{organizationName}</strong> on <strong>Infisical</strong>
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]"> <Text className="text-black text-[14px] leading-[24px]">
{inviterFirstName && inviterUsername ? ( {inviterFirstName && inviterUsername ? (
<> <>
<strong>{inviterFirstName}</strong> ( <strong>{inviterFirstName}</strong> (
<Link href={`mailto:${inviterUsername}`} className="text-slate-700 no-underline"> <BaseLink href={`mailto:${inviterUsername}`}>{inviterUsername}</BaseLink>) has invited you to collaborate
{inviterUsername} on <strong>{organizationName}</strong>.
</Link>
) has invited you to collaborate on <strong>{organizationName}</strong>.
</> </>
) : ( ) : (
<> <>
@@ -53,13 +53,12 @@ export const OrganizationInvitationTemplate = ({
)} )}
</Text> </Text>
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <BaseButton
href={`${callback_url}?token=${token}${metadata ? `&metadata=${metadata}` : ""}&to=${encodeURIComponent(email)}&organization_id=${organizationId}`} href={`${callback_url}?token=${token}${metadata ? `&metadata=${metadata}` : ""}&to=${encodeURIComponent(email)}&organization_id=${organizationId}`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
> >
Accept Invite Accept Invite
</Button> </BaseButton>
</Section> </Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800"> <Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]"> <Text className="mb-[0px]">
@@ -1,7 +1,9 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface PasswordResetTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface PasswordResetTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
email: string; email: string;
@@ -20,16 +22,13 @@ export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, t
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0"> <Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Account Recovery</strong> <strong>Account Recovery</strong>
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">A password reset was requested for your Infisical account.</Text> <Text className="text-[14px]">A password reset was requested for your Infisical account.</Text>
<Text className="text-[14px]"> <Text className="text-[14px]">
If you did not initiate this request, please contact{" "} If you did not initiate this request, please contact{" "}
{isCloud ? ( {isCloud ? (
<> <>
us immediately at{" "} us immediately at <BaseLink href="mailto:[email protected]">support@infisical.com</BaseLink>
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
support@infisical.com
</Link>
</> </>
) : ( ) : (
"your administrator immediately" "your administrator immediately"
@@ -37,13 +36,8 @@ export const PasswordResetTemplate = ({ email, isCloud, siteUrl, callback_url, t
. .
</Text> </Text>
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <BaseButton href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}>Reset Password</BaseButton>
href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Reset Password
</Button>
</Section> </Section>
</BaseEmailWrapper> </BaseEmailWrapper>
); );
@@ -1,6 +1,8 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components"; import { Button, Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseLink } from "@app/services/smtp/emails/BaseLink";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface PasswordSetupTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface PasswordSetupTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
@@ -16,19 +18,16 @@ export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, t
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0"> <Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
<strong>Password Setup</strong> <strong>Password Setup</strong>
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-[14px]">Someone requested to set up a password for your Infisical account.</Text> <Text className="text-[14px]">Someone requested to set up a password for your Infisical account.</Text>
<Text className="text-[14px] text-red-500"> <Text className="text-[14px] text-red-600">
Make sure you are already logged in to Infisical in the current browser before clicking the link below. Make sure you are already logged in to Infisical in the current browser before clicking the link below.
</Text> </Text>
<Text className="text-[14px]"> <Text className="text-[14px]">
If you did not initiate this request, please contact{" "} If you did not initiate this request, please contact{" "}
{isCloud ? ( {isCloud ? (
<> <>
us immediately at{" "} us immediately at <BaseLink href="mailto:[email protected]">support@infisical.com</BaseLink>
<Link href="mailto:[email protected]" className="text-slate-700 no-underline">
support@infisical.com
</Link>
</> </>
) : ( ) : (
"your administrator immediately" "your administrator immediately"
@@ -36,7 +35,7 @@ export const PasswordSetupTemplate = ({ email, isCloud, siteUrl, callback_url, t
. .
</Text> </Text>
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <Button
href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`} href={`${callback_url}?token=${token}&to=${encodeURIComponent(email)}`}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium" className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
@@ -1,7 +1,9 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface ProjectAccessRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { interface ProjectAccessRequestTemplateProps extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
projectName: string; projectName: string;
@@ -30,26 +32,17 @@ export const ProjectAccessRequestTemplate = ({
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0"> <Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
A user has requested access to the project <strong>{projectName}</strong> A user has requested access to the project <strong>{projectName}</strong>
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]"> <Text className="text-black text-[14px] leading-[24px]">
<strong>{requesterName}</strong> ( <strong>{requesterName}</strong> (<BaseLink href={`mailto:${requesterEmail}`}>{requesterEmail}</BaseLink>) has
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline"> requested access to the project <strong>{projectName}</strong> in the organization <strong>{orgName}</strong>.
{requesterEmail}
</Link>
) has requested access to the project <strong>{projectName}</strong> in the organization{" "}
<strong>{orgName}</strong>.
</Text> </Text>
<Text className="text-[14px] text-slate-700 leading-[24px]"> <Text className="text-[14px] text-slate-700 leading-[24px]">
<strong className="text-black">User note:</strong> "{note}" <strong className="text-black">User note:</strong> "{note}"
</Text> </Text>
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <BaseButton href={callback_url}>Grant Access</BaseButton>
href={callback_url}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Grant Access
</Button>
</Section> </Section>
</BaseEmailWrapper> </BaseEmailWrapper>
); );
@@ -1,6 +1,7 @@
import { Button, Heading, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ProjectInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> { interface ProjectInvitationTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
@@ -18,18 +19,13 @@ export const ProjectInvitationTemplate = ({ callback_url, workspaceName, siteUrl
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0"> <Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
You've been invited to join a project on Infisical You've been invited to join a project on Infisical
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]"> <Text className="text-black text-[14px] leading-[24px]">
You've been invited to join the project <strong>{workspaceName}</strong>. You've been invited to join the project <strong>{workspaceName}</strong>.
</Text> </Text>
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <BaseButton href={callback_url}>Join Project</BaseButton>
href={callback_url}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Join Project
</Button>
</Section> </Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800"> <Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]"> <Text className="mb-[0px]">
@@ -1,6 +1,7 @@
import { Button, Heading, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
interface ScimUserProvisionedTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> { interface ScimUserProvisionedTemplateProps extends Omit<BaseEmailWrapperProps, "preview" | "title"> {
@@ -24,18 +25,13 @@ export const ScimUserProvisionedTemplate = ({
<br /> <br />
<strong>{organizationName}</strong> on <strong>Infisical</strong> <strong>{organizationName}</strong> on <strong>Infisical</strong>
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border text-center border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]"> <Text className="text-black text-[14px] leading-[24px]">
You've been invited to collaborate on <strong>{organizationName}</strong>. You've been invited to collaborate on <strong>{organizationName}</strong>.
</Text> </Text>
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <BaseButton href={callback_url}>Accept Invite</BaseButton>
href={callback_url}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Accept Invite
</Button>
</Section> </Section>
<Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800"> <Section className="mt-[24px] bg-gray-50 pt-[2px] pb-[16px] border border-solid border-gray-200 px-[24px] rounded-md text-gray-800">
<Text className="mb-[0px]"> <Text className="mb-[0px]">
@@ -1,7 +1,9 @@
import { Button, Heading, Link, Section, Text } from "@react-email/components"; import { Heading, Section, Text } from "@react-email/components";
import React from "react"; import React from "react";
import { BaseButton } from "./BaseButton";
import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper"; import { BaseEmailWrapper, BaseEmailWrapperProps } from "./BaseEmailWrapper";
import { BaseLink } from "./BaseLink";
interface SecretApprovalRequestBypassedTemplateProps interface SecretApprovalRequestBypassedTemplateProps
extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> { extends Omit<BaseEmailWrapperProps, "title" | "preview" | "children"> {
@@ -35,13 +37,10 @@ export const SecretApprovalRequestBypassedTemplate = ({
<Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0"> <Heading className="text-black text-[18px] leading-[28px] text-center font-normal p-0 mx-0">
A secret approval request has been bypassed in the project <strong>{projectName}</strong> A secret approval request has been bypassed in the project <strong>{projectName}</strong>
</Heading> </Heading>
<Section className="px-[24px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50"> <Section className="px-[24px] mb-[28px] mt-[36px] pt-[12px] pb-[8px] border border-solid border-gray-200 rounded-md bg-gray-50">
<Text className="text-black text-[14px] leading-[24px]"> <Text className="text-black text-[14px] leading-[24px]">
<strong>{requesterFullName}</strong> ( <strong>{requesterFullName}</strong> (<BaseLink href={`mailto:${requesterEmail}`}>{requesterEmail}</BaseLink>)
<Link href={`mailto:${requesterEmail}`} className="text-slate-700 no-underline"> has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "}
{requesterEmail}
</Link>
) has {requestType === "change" ? "merged" : "accessed"} a secret {requestType === "change" ? "to" : "in"}{" "}
<strong>{secretPath}</strong> in the <strong>{environment}</strong> environment without obtaining the required <strong>{secretPath}</strong> in the <strong>{environment}</strong> environment without obtaining the required
approval. approval.
</Text> </Text>
@@ -50,13 +49,8 @@ export const SecretApprovalRequestBypassedTemplate = ({
{bypassReason}" {bypassReason}"
</Text> </Text>
</Section> </Section>
<Section className="text-center mt-[28px]"> <Section className="text-center">
<Button <BaseButton href={approvalUrl}>Review Bypass</BaseButton>
href={approvalUrl}
className="rounded-md p-3 px-[28px] my-[8px] text-center text-[16px] bg-[#EBF852] border-solid border border-[#d1e309] text-black font-medium"
>
Review Bypass
</Button>
</Section> </Section>
</BaseEmailWrapper> </BaseEmailWrapper>
); );

Some files were not shown because too many files have changed in this diff Show More