Merge branch 'main' into feat/orgUserAuthTokenExpiration

This commit is contained in:
carlosmonastyrski
2025-04-30 15:59:52 -03:00
85 changed files with 2380 additions and 373 deletions
+393 -81
View File
@@ -33,6 +33,7 @@
"@infisical/quic": "^1.0.8", "@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^5.0.1", "@node-saml/passport-saml": "^5.0.1",
"@octokit/auth-app": "^7.1.1", "@octokit/auth-app": "^7.1.1",
"@octokit/plugin-paginate-graphql": "^5.2.4",
"@octokit/plugin-retry": "^5.0.5", "@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
@@ -91,10 +92,10 @@
"ora": "^7.0.1", "ora": "^7.0.1",
"oracledb": "^6.4.0", "oracledb": "^6.4.0",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0", "passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1", "passport-ldapauth": "^3.0.1",
"passport-oauth2": "^1.8.0",
"pg": "^8.11.3", "pg": "^8.11.3",
"pg-boss": "^10.1.5", "pg-boss": "^10.1.5",
"pg-query-stream": "^4.5.3", "pg-query-stream": "^4.5.3",
@@ -135,7 +136,6 @@
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.17.30", "@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9", "@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3", "@types/picomatch": "^2.3.3",
@@ -7245,47 +7245,247 @@
} }
}, },
"node_modules/@octokit/core": { "node_modules/@octokit/core": {
"version": "5.0.2", "version": "6.1.5",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.0.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.5.tgz",
"integrity": "sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==", "integrity": "sha512-vvmsN0r7rguA+FySiCsbaTTobSftpIDIpPW81trAmsv9TGxg3YCujAxRYp/Uy8xmDgYCzzgulG62H7KYUFmeIg==",
"license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@octokit/auth-token": "^4.0.0", "@octokit/auth-token": "^5.0.0",
"@octokit/graphql": "^7.0.0", "@octokit/graphql": "^8.2.2",
"@octokit/request": "^8.0.2", "@octokit/request": "^9.2.3",
"@octokit/request-error": "^5.0.0", "@octokit/request-error": "^6.1.8",
"@octokit/types": "^12.0.0", "@octokit/types": "^14.0.0",
"before-after-hook": "^2.2.0", "before-after-hook": "^3.0.2",
"universal-user-agent": "^7.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/auth-token": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.2.tgz",
"integrity": "sha512-JcQDsBdg49Yky2w2ld20IHAlwr8d/d8N6NiOXbtuoPCqzbsiJgF633mVUw3x4mo0H5ypataQIX7SFu3yy44Mpw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/endpoint": {
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/openapi-types": {
"version": "25.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz",
"integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==",
"license": "MIT",
"peer": true
},
"node_modules/@octokit/core/node_modules/@octokit/request": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz",
"integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/endpoint": "^10.1.4",
"@octokit/request-error": "^6.1.8",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^2.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/request-error": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
"integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/core/node_modules/@octokit/types": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz",
"integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/openapi-types": "^25.0.0"
}
},
"node_modules/@octokit/core/node_modules/fast-content-type-parse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"peer": true
},
"node_modules/@octokit/core/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==",
"license": "ISC",
"peer": true
},
"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" "universal-user-agent": "^6.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/endpoint": { "node_modules/@octokit/endpoint/node_modules/@octokit/openapi-types": {
"version": "9.0.4", "version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", "integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/@octokit/endpoint/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^12.0.0", "@octokit/openapi-types": "^24.2.0"
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
} }
}, },
"node_modules/@octokit/graphql": { "node_modules/@octokit/graphql": {
"version": "7.0.2", "version": "8.2.2",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.2.2.tgz",
"integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", "integrity": "sha512-Yi8hcoqsrXGdt0yObxbebHXFOiUA+2v3n53epuOg1QUgOB6c4XzvisBNVXJSl8RYA5KrDuSL2yq9Qmqe5N0ryA==",
"license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@octokit/request": "^8.0.1", "@octokit/request": "^9.2.3",
"@octokit/types": "^12.0.0", "@octokit/types": "^14.0.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^7.0.0"
}, },
"engines": { "engines": {
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/graphql/node_modules/@octokit/endpoint": {
"version": "10.1.4",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.4.tgz",
"integrity": "sha512-OlYOlZIsfEVZm5HCSR8aSg02T2lbUWOsCQoPKfTXJwDzcHQBrVBGdGXb89dv2Kw2ToZaRtudp8O3ZIYoaOjKlA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/types": "^14.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql/node_modules/@octokit/openapi-types": {
"version": "25.0.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-25.0.0.tgz",
"integrity": "sha512-FZvktFu7HfOIJf2BScLKIEYjDsw6RKc7rBJCdvCTfKsVnx2GEB/Nbzjr29DUdb7vQhlzS/j8qDzdditP0OC6aw==",
"license": "MIT",
"peer": true
},
"node_modules/@octokit/graphql/node_modules/@octokit/request": {
"version": "9.2.3",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.2.3.tgz",
"integrity": "sha512-Ma+pZU8PXLOEYzsWf0cn/gY+ME57Wq8f49WTXA8FMHp2Ps9djKw//xYJ1je8Hm0pR2lU9FUGeJRWOtxq6olt4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/endpoint": "^10.1.4",
"@octokit/request-error": "^6.1.8",
"@octokit/types": "^14.0.0",
"fast-content-type-parse": "^2.0.0",
"universal-user-agent": "^7.0.2"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql/node_modules/@octokit/request-error": {
"version": "6.1.8",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.8.tgz",
"integrity": "sha512-WEi/R0Jmq+IJKydWlKDmryPcmdYSVjL3ekaiEL1L9eo1sUnqMJ+grqmC9cjk7CA7+b2/T397tO5d8YLOH3qYpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/types": "^14.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/graphql/node_modules/@octokit/types": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-14.0.0.tgz",
"integrity": "sha512-VVmZP0lEhbo2O1pdq63gZFiGCKkm8PPp8AUOijlwPO6hojEVjspA0MWKP7E4hbvGxzFKNqKr6p0IYtOH/Wf/zA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/openapi-types": "^25.0.0"
}
},
"node_modules/@octokit/graphql/node_modules/fast-content-type-parse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz",
"integrity": "sha512-nGqtvLrj5w0naR6tDPfB4cUmYCqouzyQiz6C5y/LtcDllJdrcc6WaWW6iXyIIOErTa/XRybj28aasdn4LkVk6Q==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"peer": true
},
"node_modules/@octokit/graphql/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==",
"license": "ISC",
"peer": true
},
"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",
@@ -7380,6 +7580,18 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/plugin-paginate-graphql": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-graphql/-/plugin-paginate-graphql-5.2.4.tgz",
"integrity": "sha512-pLZES1jWaOynXKHOqdnwZ5ULeVR6tVVCMm+AUbp0htdcyXDU95WbkYdU4R2ej1wKj5Tu94Mee2Ne0PjPO9cCyA==",
"license": "MIT",
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": ">=6"
}
},
"node_modules/@octokit/plugin-paginate-rest": { "node_modules/@octokit/plugin-paginate-rest": {
"version": "9.1.5", "version": "9.1.5",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz",
@@ -7461,28 +7673,14 @@
"@octokit/openapi-types": "^18.0.0" "@octokit/openapi-types": "^18.0.0"
} }
}, },
"node_modules/@octokit/plugin-throttling": {
"version": "8.1.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.1.3.tgz",
"integrity": "sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==",
"dependencies": {
"@octokit/types": "^12.2.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "^5.0.0"
}
},
"node_modules/@octokit/request": { "node_modules/@octokit/request": {
"version": "8.4.0", "version": "8.4.1",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.1.tgz",
"integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", "integrity": "sha512-qnB2+SY3hkCmBxZsR/MPCybNmbJe4KAlfWErXq+rBKkQJlbjdJeS85VI9r8UqeLYLvnAenU8Q1okM/0MBsAGXw==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/endpoint": "^9.0.1", "@octokit/endpoint": "^9.0.6",
"@octokit/request-error": "^5.1.0", "@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.1.0", "@octokit/types": "^13.1.0",
"universal-user-agent": "^6.0.0" "universal-user-agent": "^6.0.0"
}, },
@@ -7491,9 +7689,10 @@
} }
}, },
"node_modules/@octokit/request-error": { "node_modules/@octokit/request-error": {
"version": "5.1.0", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.1.tgz",
"integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", "integrity": "sha512-v9iyEQJH6ZntoENr9/yXxjuezh4My67CBSu9r6Ve/05Iu5gNgnisNWOsoJHTP6k0Rr0+HQIpnH+kyammu90q/g==",
"license": "MIT",
"dependencies": { "dependencies": {
"@octokit/types": "^13.1.0", "@octokit/types": "^13.1.0",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
@@ -7543,6 +7742,59 @@
"node": ">= 18" "node": ">= 18"
} }
}, },
"node_modules/@octokit/rest/node_modules/@octokit/core": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/rest/node_modules/@octokit/graphql": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz",
"integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^8.4.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/@octokit/rest/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/@octokit/rest/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/@octokit/rest/node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"license": "Apache-2.0"
},
"node_modules/@octokit/types": { "node_modules/@octokit/types": {
"version": "12.4.0", "version": "12.4.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz",
@@ -9871,17 +10123,6 @@
"@types/express": "*" "@types/express": "*"
} }
}, },
"node_modules/@types/passport-github": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@types/passport-github/-/passport-github-1.1.12.tgz",
"integrity": "sha512-VJpMEIH+cOoXB694QgcxuvWy2wPd1Oq3gqrg2Y9DMVBYs9TmH9L14qnqPDZsNMZKBDH+SvqRsGZj9SgHYeDgcA==",
"dev": true,
"dependencies": {
"@types/express": "*",
"@types/passport": "*",
"@types/passport-oauth2": "*"
}
},
"node_modules/@types/passport-google-oauth20": { "node_modules/@types/passport-google-oauth20": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.14.tgz",
@@ -11654,9 +11895,11 @@
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==" "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
}, },
"node_modules/before-after-hook": { "node_modules/before-after-hook": {
"version": "2.2.3", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==",
"license": "Apache-2.0",
"peer": true
}, },
"node_modules/big-integer": { "node_modules/big-integer": {
"version": "1.6.52", "version": "1.6.52",
@@ -18142,9 +18385,10 @@
"integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA==" "integrity": "sha512-p1TRH/edngVEHVbwqWnxUViEmq5znDvyB+Sik5cmuLpGOIfDf/39zLiq3swPF8Vakqn+gvNiOQAZu8djYlQILA=="
}, },
"node_modules/oauth": { "node_modules/oauth": {
"version": "0.9.15", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/oauth/-/oauth-0.9.15.tgz", "resolved": "https://registry.npmjs.org/oauth/-/oauth-0.10.2.tgz",
"integrity": "sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==" "integrity": "sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==",
"license": "MIT"
}, },
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
@@ -18827,17 +19071,6 @@
"url": "https://github.com/sponsors/jaredhanson" "url": "https://github.com/sponsors/jaredhanson"
} }
}, },
"node_modules/passport-github": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/passport-github/-/passport-github-1.1.0.tgz",
"integrity": "sha512-XARXJycE6fFh/dxF+Uut8OjlwbFEXgbPVj/+V+K7cvriRK7VcAOm+NgBmbiLM9Qv3SSxEAV+V6fIk89nYHXa8A==",
"dependencies": {
"passport-oauth2": "1.x.x"
},
"engines": {
"node": ">= 0.4.0"
}
},
"node_modules/passport-gitlab2": { "node_modules/passport-gitlab2": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz", "resolved": "https://registry.npmjs.org/passport-gitlab2/-/passport-gitlab2-5.0.0.tgz",
@@ -18873,12 +19106,13 @@
} }
}, },
"node_modules/passport-oauth2": { "node_modules/passport-oauth2": {
"version": "1.7.0", "version": "1.8.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.8.0.tgz",
"integrity": "sha512-j2gf34szdTF2Onw3+76alNnaAExlUmHvkc7cL+cmaS5NzHzDP/BvFHJruueQ9XAeNOdpI+CH+PWid8RA7KCwAQ==", "integrity": "sha512-cjsQbOrXIDE4P8nNb3FQRCCmJJ/utnFKEz2NX209f7KOHPoX18gF7gBzBbLLsj2/je4KrgiwLLGjf0lm9rtTBA==",
"license": "MIT",
"dependencies": { "dependencies": {
"base64url": "3.x.x", "base64url": "3.x.x",
"oauth": "0.9.x", "oauth": "0.10.x",
"passport-strategy": "1.x.x", "passport-strategy": "1.x.x",
"uid2": "0.0.x", "uid2": "0.0.x",
"utils-merge": "1.x.x" "utils-merge": "1.x.x"
@@ -19667,6 +19901,62 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/probot/node_modules/@octokit/core": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.1.tgz",
"integrity": "sha512-dKYCMuPO1bmrpuogcjQ8z7ICCH3FP6WmxpwC03yjzGfZhj9fTJg6+bS1+UAplekbN2C+M61UNllGOOoAfGCrdQ==",
"license": "MIT",
"dependencies": {
"@octokit/auth-token": "^4.0.0",
"@octokit/graphql": "^7.1.0",
"@octokit/request": "^8.4.1",
"@octokit/request-error": "^5.1.1",
"@octokit/types": "^13.0.0",
"before-after-hook": "^2.2.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/probot/node_modules/@octokit/core/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/probot/node_modules/@octokit/graphql": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.1.tgz",
"integrity": "sha512-3mkDltSfcDUoa176nlGoA32RGjeWjl3K7F/BwHwRMJUW/IteSa4bnSV8p2ThNkcIcZU2umkZWxwETSSCJf2Q7g==",
"license": "MIT",
"dependencies": {
"@octokit/request": "^8.4.1",
"@octokit/types": "^13.0.0",
"universal-user-agent": "^6.0.0"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/probot/node_modules/@octokit/graphql/node_modules/@octokit/types": {
"version": "13.10.0",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.10.0.tgz",
"integrity": "sha512-ifLaO34EbbPj0Xgro4G5lP5asESjwHracYJvVaPIyXMuiuXLlhic3S47cBdTb+jfODkTE5YtGCLt3Ay3+J97sA==",
"license": "MIT",
"dependencies": {
"@octokit/openapi-types": "^24.2.0"
}
},
"node_modules/probot/node_modules/@octokit/openapi-types": {
"version": "24.2.0",
"resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-24.2.0.tgz",
"integrity": "sha512-9sIH3nSUttelJSXUrmGzl7QUBFul0/mB8HRYl3fOlgHbIWG+WnYDXU3v/2zMtAvuzZ/ed00Ei6on975FhBfzrg==",
"license": "MIT"
},
"node_modules/probot/node_modules/@octokit/plugin-retry": { "node_modules/probot/node_modules/@octokit/plugin-retry": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz",
@@ -19683,6 +19973,28 @@
"@octokit/core": ">=5" "@octokit/core": ">=5"
} }
}, },
"node_modules/probot/node_modules/@octokit/plugin-throttling": {
"version": "8.2.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz",
"integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==",
"license": "MIT",
"dependencies": {
"@octokit/types": "^12.2.0",
"bottleneck": "^2.15.3"
},
"engines": {
"node": ">= 18"
},
"peerDependencies": {
"@octokit/core": "^5.0.0"
}
},
"node_modules/probot/node_modules/before-after-hook": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz",
"integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==",
"license": "Apache-2.0"
},
"node_modules/probot/node_modules/commander": { "node_modules/probot/node_modules/commander": {
"version": "12.1.0", "version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
+2 -2
View File
@@ -91,7 +91,6 @@
"@types/lodash.isequal": "^4.5.8", "@types/lodash.isequal": "^4.5.8",
"@types/node": "^20.17.30", "@types/node": "^20.17.30",
"@types/nodemailer": "^6.4.14", "@types/nodemailer": "^6.4.14",
"@types/passport-github": "^1.1.12",
"@types/passport-google-oauth20": "^2.0.14", "@types/passport-google-oauth20": "^2.0.14",
"@types/pg": "^8.10.9", "@types/pg": "^8.10.9",
"@types/picomatch": "^2.3.3", "@types/picomatch": "^2.3.3",
@@ -150,6 +149,7 @@
"@infisical/quic": "^1.0.8", "@infisical/quic": "^1.0.8",
"@node-saml/passport-saml": "^5.0.1", "@node-saml/passport-saml": "^5.0.1",
"@octokit/auth-app": "^7.1.1", "@octokit/auth-app": "^7.1.1",
"@octokit/plugin-paginate-graphql": "^5.2.4",
"@octokit/plugin-retry": "^5.0.5", "@octokit/plugin-retry": "^5.0.5",
"@octokit/rest": "^20.0.2", "@octokit/rest": "^20.0.2",
"@octokit/webhooks-types": "^7.3.1", "@octokit/webhooks-types": "^7.3.1",
@@ -208,10 +208,10 @@
"ora": "^7.0.1", "ora": "^7.0.1",
"oracledb": "^6.4.0", "oracledb": "^6.4.0",
"otplib": "^12.0.1", "otplib": "^12.0.1",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0", "passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0", "passport-google-oauth20": "^2.0.0",
"passport-ldapauth": "^3.0.1", "passport-ldapauth": "^3.0.1",
"passport-oauth2": "^1.8.0",
"pg": "^8.11.3", "pg": "^8.11.3",
"pg-boss": "^10.1.5", "pg-boss": "^10.1.5",
"pg-query-stream": "^4.5.3", "pg-query-stream": "^4.5.3",
+4
View File
@@ -15,6 +15,7 @@ import { TDynamicSecretServiceFactory } from "@app/ee/services/dynamic-secret/dy
import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service"; import { TDynamicSecretLeaseServiceFactory } from "@app/ee/services/dynamic-secret-lease/dynamic-secret-lease-service";
import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service"; import { TExternalKmsServiceFactory } from "@app/ee/services/external-kms/external-kms-service";
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 { TGroupServiceFactory } from "@app/ee/services/group/group-service"; import { TGroupServiceFactory } from "@app/ee/services/group/group-service";
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";
@@ -117,6 +118,7 @@ declare module "@fastify/request-context" {
declare module "fastify" { declare module "fastify" {
interface Session { interface Session {
callbackPort: string; callbackPort: string;
isAdminLogin: boolean;
} }
interface FastifyRequest { interface FastifyRequest {
@@ -140,6 +142,7 @@ declare module "fastify" {
passportUser: { passportUser: {
isUserCompleted: boolean; isUserCompleted: boolean;
providerAuthToken: string; providerAuthToken: string;
externalProviderAccessToken?: string;
}; };
kmipUser: { kmipUser: {
projectId: string; projectId: string;
@@ -244,6 +247,7 @@ declare module "fastify" {
gateway: TGatewayServiceFactory; gateway: TGatewayServiceFactory;
secretRotationV2: TSecretRotationV2ServiceFactory; secretRotationV2: TSecretRotationV2ServiceFactory;
assumePrivileges: TAssumePrivilegeServiceFactory; assumePrivileges: TAssumePrivilegeServiceFactory;
githubOrgSync: TGithubOrgSyncServiceFactory;
}; };
// 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
+8
View File
@@ -83,6 +83,9 @@ import {
TGitAppOrg, TGitAppOrg,
TGitAppOrgInsert, TGitAppOrgInsert,
TGitAppOrgUpdate, TGitAppOrgUpdate,
TGithubOrgSyncConfigs,
TGithubOrgSyncConfigsInsert,
TGithubOrgSyncConfigsUpdate,
TGroupProjectMembershipRoles, TGroupProjectMembershipRoles,
TGroupProjectMembershipRolesInsert, TGroupProjectMembershipRolesInsert,
TGroupProjectMembershipRolesUpdate, TGroupProjectMembershipRolesUpdate,
@@ -1004,5 +1007,10 @@ declare module "knex/types/tables" {
TSecretReminderRecipientsInsert, TSecretReminderRecipientsInsert,
TSecretReminderRecipientsUpdate TSecretReminderRecipientsUpdate
>; >;
[TableName.GithubOrgSyncConfig]: KnexOriginal.CompositeTableType<
TGithubOrgSyncConfigs,
TGithubOrgSyncConfigsInsert,
TGithubOrgSyncConfigsUpdate
>;
} }
} }
@@ -0,0 +1,26 @@
import { Knex } from "knex";
import { TableName } from "../schemas";
import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils";
export async function up(knex: Knex): Promise<void> {
const hasTable = await knex.schema.hasTable(TableName.GithubOrgSyncConfig);
if (!hasTable) {
await knex.schema.createTable(TableName.GithubOrgSyncConfig, (t) => {
t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid());
t.string("githubOrgName").notNullable();
t.boolean("isActive").defaultTo(false);
t.binary("encryptedGithubOrgAccessToken");
t.uuid("orgId").notNullable().unique();
t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE");
t.timestamps(true, true, true);
});
}
await createOnUpdateTrigger(knex, TableName.GithubOrgSyncConfig);
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists(TableName.GithubOrgSyncConfig);
await dropOnUpdateTrigger(knex, TableName.GithubOrgSyncConfig);
}
@@ -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 GithubOrgSyncConfigsSchema = z.object({
id: z.string().uuid(),
githubOrgName: z.string(),
isActive: z.boolean().default(false).nullable().optional(),
encryptedGithubOrgAccessToken: zodBuffer.nullable().optional(),
orgId: z.string().uuid(),
createdAt: z.date(),
updatedAt: z.date()
});
export type TGithubOrgSyncConfigs = z.infer<typeof GithubOrgSyncConfigsSchema>;
export type TGithubOrgSyncConfigsInsert = Omit<z.input<typeof GithubOrgSyncConfigsSchema>, TImmutableDBKeys>;
export type TGithubOrgSyncConfigsUpdate = Partial<Omit<z.input<typeof GithubOrgSyncConfigsSchema>, TImmutableDBKeys>>;
+1
View File
@@ -25,6 +25,7 @@ export * from "./external-kms";
export * from "./gateways"; export * from "./gateways";
export * from "./git-app-install-sessions"; export * from "./git-app-install-sessions";
export * from "./git-app-org"; export * from "./git-app-org";
export * from "./github-org-sync-configs";
export * from "./group-project-membership-roles"; export * from "./group-project-membership-roles";
export * from "./group-project-memberships"; export * from "./group-project-memberships";
export * from "./groups"; export * from "./groups";
+2 -1
View File
@@ -147,7 +147,8 @@ export enum TableName {
KmipClientCertificates = "kmip_client_certificates", KmipClientCertificates = "kmip_client_certificates",
SecretRotationV2 = "secret_rotations_v2", SecretRotationV2 = "secret_rotations_v2",
SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings", SecretRotationV2SecretMapping = "secret_rotation_v2_secret_mappings",
SecretReminderRecipients = "secret_reminder_recipients" SecretReminderRecipients = "secret_reminder_recipients",
GithubOrgSyncConfig = "github_org_sync_configs"
} }
export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt"; export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt";
@@ -0,0 +1,129 @@
import { z } from "zod";
import { GithubOrgSyncConfigsSchema } from "@app/db/schemas";
import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string";
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 SanitizedGithubOrgSyncSchema = GithubOrgSyncConfigsSchema.pick({
isActive: true,
id: true,
createdAt: true,
updatedAt: true,
orgId: true,
githubOrgName: true
});
const githubOrgNameValidator = zodValidateCharacters([CharacterType.AlphaNumeric, CharacterType.Hyphen]);
export const registerGithubOrgSyncRouter = async (server: FastifyZodProvider) => {
server.route({
url: "/",
method: "POST",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z.object({
githubOrgName: githubOrgNameValidator(z.string().trim(), "GitHub Org Name"),
githubOrgAccessToken: z.string().trim().max(1000).optional(),
isActive: z.boolean().default(false)
}),
response: {
200: z.object({
githubOrgSyncConfig: SanitizedGithubOrgSyncSchema
})
}
},
handler: async (req) => {
const githubOrgSyncConfig = await server.services.githubOrgSync.createGithubOrgSync({
orgPermission: req.permission,
githubOrgName: req.body.githubOrgName,
githubOrgAccessToken: req.body.githubOrgAccessToken,
isActive: req.body.isActive
});
return { githubOrgSyncConfig };
}
});
server.route({
url: "/",
method: "PATCH",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
body: z
.object({
githubOrgName: githubOrgNameValidator(z.string().trim(), "GitHub Org Name"),
githubOrgAccessToken: z.string().trim().max(1000),
isActive: z.boolean()
})
.partial(),
response: {
200: z.object({
githubOrgSyncConfig: SanitizedGithubOrgSyncSchema
})
}
},
handler: async (req) => {
const githubOrgSyncConfig = await server.services.githubOrgSync.updateGithubOrgSync({
orgPermission: req.permission,
githubOrgName: req.body.githubOrgName,
githubOrgAccessToken: req.body.githubOrgAccessToken,
isActive: req.body.isActive
});
return { githubOrgSyncConfig };
}
});
server.route({
url: "/",
method: "DELETE",
config: {
rateLimit: writeLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
response: {
200: z.object({
githubOrgSyncConfig: SanitizedGithubOrgSyncSchema
})
}
},
handler: async (req) => {
const githubOrgSyncConfig = await server.services.githubOrgSync.deleteGithubOrgSync({
orgPermission: req.permission
});
return { githubOrgSyncConfig };
}
});
server.route({
url: "/",
method: "GET",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT]),
schema: {
response: {
200: z.object({
githubOrgSyncConfig: SanitizedGithubOrgSyncSchema
})
}
},
handler: async (req) => {
const githubOrgSyncConfig = await server.services.githubOrgSync.getGithubOrgSync({
orgPermission: req.permission
});
return { githubOrgSyncConfig };
}
});
};
+2
View File
@@ -9,6 +9,7 @@ import { registerDynamicSecretLeaseRouter } from "./dynamic-secret-lease-router"
import { registerDynamicSecretRouter } from "./dynamic-secret-router"; import { registerDynamicSecretRouter } from "./dynamic-secret-router";
import { registerExternalKmsRouter } from "./external-kms-router"; import { registerExternalKmsRouter } from "./external-kms-router";
import { registerGatewayRouter } from "./gateway-router"; import { registerGatewayRouter } from "./gateway-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 { registerKmipRouter } from "./kmip-router"; import { registerKmipRouter } from "./kmip-router";
@@ -72,6 +73,7 @@ export const registerV1EERoutes = async (server: FastifyZodProvider) => {
); );
await server.register(registerGatewayRouter, { prefix: "/gateways" }); await server.register(registerGatewayRouter, { prefix: "/gateways" });
await server.register(registerGithubOrgSyncRouter, { prefix: "/github-org-sync-config" });
await server.register( await server.register(
async (pkiRouter) => { async (pkiRouter) => {
@@ -0,0 +1,10 @@
import { TDbClient } from "@app/db";
import { TableName } from "@app/db/schemas";
import { ormify } from "@app/lib/knex";
export type TGithubOrgSyncDALFactory = ReturnType<typeof githubOrgSyncDALFactory>;
export const githubOrgSyncDALFactory = (db: TDbClient) => {
const orm = ormify(db, TableName.GithubOrgSyncConfig);
return orm;
};
@@ -0,0 +1,354 @@
import { ForbiddenError } from "@casl/ability";
import { Octokit } from "@octokit/core";
import { paginateGraphQL } from "@octokit/plugin-paginate-graphql";
import { Octokit as OctokitRest } from "@octokit/rest";
import { OrgMembershipRole } from "@app/db/schemas";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TGroupDALFactory } from "../group/group-dal";
import { TUserGroupMembershipDALFactory } from "../group/user-group-membership-dal";
import { TLicenseServiceFactory } from "../license/license-service";
import { OrgPermissionActions, OrgPermissionSubjects } from "../permission/org-permission";
import { TPermissionServiceFactory } from "../permission/permission-service";
import { TGithubOrgSyncDALFactory } from "./github-org-sync-dal";
import { TCreateGithubOrgSyncDTO, TDeleteGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./github-org-sync-types";
const OctokitWithPlugin = Octokit.plugin(paginateGraphQL);
type TGithubOrgSyncServiceFactoryDep = {
githubOrgSyncDAL: TGithubOrgSyncDALFactory;
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
userGroupMembershipDAL: Pick<
TUserGroupMembershipDALFactory,
"findGroupMembershipsByUserIdInOrg" | "insertMany" | "delete"
>;
groupDAL: Pick<TGroupDALFactory, "insertMany" | "transaction" | "find">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
};
export type TGithubOrgSyncServiceFactory = ReturnType<typeof githubOrgSyncServiceFactory>;
export const githubOrgSyncServiceFactory = ({
githubOrgSyncDAL,
permissionService,
kmsService,
userGroupMembershipDAL,
groupDAL,
licenseService
}: TGithubOrgSyncServiceFactoryDep) => {
const createGithubOrgSync = async ({
githubOrgName,
orgPermission,
githubOrgAccessToken,
isActive
}: TCreateGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync);
const plan = await licenseService.getPlan(orgPermission.orgId);
if (!plan.githubOrgSync) {
throw new BadRequestError({
message:
"Failed to create github organization team sync due to plan restriction. Upgrade plan to create github organization sync."
});
}
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
if (existingConfig)
throw new BadRequestError({
message: `Organization ${orgPermission.orgId} already has GitHub Organization sync config.`
});
const octokit = new OctokitRest({
auth: githubOrgAccessToken,
request: {
signal: AbortSignal.timeout(5000)
}
});
const { data } = await octokit.rest.orgs.get({
org: githubOrgName
});
if (data.login.toLowerCase() !== githubOrgName.toLowerCase())
throw new BadRequestError({ message: "Invalid GitHub organisation" });
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: orgPermission.orgId
});
const config = await githubOrgSyncDAL.create({
orgId: orgPermission.orgId,
githubOrgName,
isActive,
encryptedGithubOrgAccessToken: githubOrgAccessToken
? encryptor({ plainText: Buffer.from(githubOrgAccessToken) }).cipherTextBlob
: null
});
return config;
};
const updateGithubOrgSync = async ({
githubOrgName,
orgPermission,
githubOrgAccessToken,
isActive
}: TUpdateGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync);
const plan = await licenseService.getPlan(orgPermission.orgId);
if (!plan.githubOrgSync) {
throw new BadRequestError({
message:
"Failed to update github organization team sync due to plan restriction. Upgrade plan to update github organization sync."
});
}
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
if (!existingConfig)
throw new BadRequestError({
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
});
const { encryptor, decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.Organization,
orgId: orgPermission.orgId
});
const newData = {
githubOrgName: githubOrgName || existingConfig.githubOrgName,
githubOrgAccessToken:
githubOrgAccessToken ||
(existingConfig.encryptedGithubOrgAccessToken
? decryptor({ cipherTextBlob: existingConfig.encryptedGithubOrgAccessToken }).toString()
: null)
};
if (githubOrgName || githubOrgAccessToken) {
const octokit = new OctokitRest({
auth: newData.githubOrgAccessToken,
request: {
signal: AbortSignal.timeout(5000)
}
});
const { data } = await octokit.rest.orgs.get({
org: newData.githubOrgName
});
if (data.login.toLowerCase() !== newData.githubOrgName.toLowerCase())
throw new BadRequestError({ message: "Invalid GitHub organisation" });
}
const config = await githubOrgSyncDAL.updateById(existingConfig.id, {
orgId: orgPermission.orgId,
githubOrgName: newData.githubOrgName,
isActive,
encryptedGithubOrgAccessToken: newData.githubOrgAccessToken
? encryptor({ plainText: Buffer.from(newData.githubOrgAccessToken) }).cipherTextBlob
: null
});
return config;
};
const deleteGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync);
const plan = await licenseService.getPlan(orgPermission.orgId);
if (!plan.githubOrgSync) {
throw new BadRequestError({
message:
"Failed to delete github organization team sync due to plan restriction. Upgrade plan to delete github organization sync."
});
}
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
if (!existingConfig)
throw new BadRequestError({
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
});
const config = await githubOrgSyncDAL.deleteById(existingConfig.id);
return config;
};
const getGithubOrgSync = async ({ orgPermission }: TDeleteGithubOrgSyncDTO) => {
const { permission } = await permissionService.getOrgPermission(
orgPermission.type,
orgPermission.id,
orgPermission.orgId,
orgPermission.authMethod,
orgPermission.orgId
);
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
const existingConfig = await githubOrgSyncDAL.findOne({ orgId: orgPermission.orgId });
if (!existingConfig)
throw new NotFoundError({
message: `Organization ${orgPermission.orgId} GitHub organization sync config missing.`
});
return existingConfig;
};
const syncUserGroups = async (orgId: string, userId: string, accessToken: string) => {
const config = await githubOrgSyncDAL.findOne({ orgId });
if (!config || !config?.isActive) return;
const infisicalUserGroups = await userGroupMembershipDAL.findGroupMembershipsByUserIdInOrg(userId, orgId);
const infisicalUserGroupSet = new Set(infisicalUserGroups.map((el) => el.groupName));
const octoRest = new OctokitRest({
auth: accessToken,
request: {
signal: AbortSignal.timeout(5000)
}
});
const { data: userOrgMembershipDetails } = await octoRest.rest.orgs
.getMembershipForAuthenticatedUser({
org: config.githubOrgName
})
.catch((err) => {
logger.error(err, "User not part of GitHub synced organization");
throw new BadRequestError({ message: "User not part of GitHub synced organization" });
});
const username = userOrgMembershipDetails?.user?.login;
if (!username) throw new BadRequestError({ message: "User not part of GitHub synced organization" });
const octokit = new OctokitWithPlugin({
auth: accessToken,
request: {
signal: AbortSignal.timeout(5000)
}
});
const data = await octokit.graphql
.paginate<{
organization: { teams: { totalCount: number; edges: { node: { name: string; description: string } }[] } };
}>(
`
query orgTeams($cursor: String,$org: String!, $username: String!){
organization(login: $org) {
teams(first: 100, userLogins: [$username], after: $cursor) {
totalCount
edges {
node {
name
description
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
`,
{
org: config.githubOrgName,
username
}
)
.catch((err) => {
if ((err as Error)?.message?.includes("Although you appear to have the correct authorization credential")) {
throw new BadRequestError({
message:
"Please check your organization have approved Infisical Oauth application. For more info: https://infisical.com/docs/documentation/platform/github-org-sync#troubleshooting"
});
}
throw new BadRequestError({ message: (err as Error)?.message });
});
const {
organization: { teams }
} = data;
const githubUserTeams = teams?.edges?.map((el) => el.node.name.toLowerCase()) || [];
const githubUserTeamSet = new Set(githubUserTeams);
const githubUserTeamOnInfisical = await groupDAL.find({ orgId, $in: { name: githubUserTeams } });
const githubUserTeamOnInfisicalGroupByName = groupBy(githubUserTeamOnInfisical, (i) => i.name);
const newTeams = githubUserTeams.filter(
(el) => !infisicalUserGroupSet.has(el) && !Object.hasOwn(githubUserTeamOnInfisicalGroupByName, el)
);
const updateTeams = githubUserTeams.filter(
(el) => !infisicalUserGroupSet.has(el) && Object.hasOwn(githubUserTeamOnInfisicalGroupByName, el)
);
const removeFromTeams = infisicalUserGroups.filter((el) => !githubUserTeamSet.has(el.groupName));
if (newTeams.length || updateTeams.length || removeFromTeams.length) {
await groupDAL.transaction(async (tx) => {
if (newTeams.length) {
const newGroups = await groupDAL.insertMany(
newTeams.map((newGroupName) => ({
name: newGroupName,
role: OrgMembershipRole.Member,
slug: newGroupName,
orgId
})),
tx
);
await userGroupMembershipDAL.insertMany(
newGroups.map((el) => ({
groupId: el.id,
userId
})),
tx
);
}
if (updateTeams.length) {
await userGroupMembershipDAL.insertMany(
updateTeams.map((el) => ({
groupId: githubUserTeamOnInfisicalGroupByName[el][0].id,
userId
})),
tx
);
}
if (removeFromTeams.length) {
await userGroupMembershipDAL.delete(
{ userId, $in: { groupId: removeFromTeams.map((el) => el.groupId) } },
tx
);
}
});
}
};
return {
createGithubOrgSync,
updateGithubOrgSync,
deleteGithubOrgSync,
getGithubOrgSync,
syncUserGroups
};
};
@@ -0,0 +1,23 @@
import { OrgServiceActor } from "@app/lib/types";
export interface TCreateGithubOrgSyncDTO {
orgPermission: OrgServiceActor;
githubOrgName: string;
githubOrgAccessToken?: string;
isActive?: boolean;
}
export interface TUpdateGithubOrgSyncDTO {
orgPermission: OrgServiceActor;
githubOrgName?: string;
githubOrgAccessToken?: string;
isActive?: boolean;
}
export interface TDeleteGithubOrgSyncDTO {
orgPermission: OrgServiceActor;
}
export interface TGetGithubOrgSyncDTO {
orgPermission: OrgServiceActor;
}
@@ -22,6 +22,7 @@ export const getDefaultOnPremFeatures = (): TFeatureSet => ({
pitRecovery: false, pitRecovery: false,
ipAllowlisting: false, ipAllowlisting: false,
rbac: false, rbac: false,
githubOrgSync: false,
customRateLimits: false, customRateLimits: false,
customAlerts: false, customAlerts: false,
secretAccessInsights: false, secretAccessInsights: false,
@@ -45,6 +45,7 @@ export type TFeatureSet = {
auditLogsRetentionDays: 0; auditLogsRetentionDays: 0;
auditLogStreams: false; auditLogStreams: false;
auditLogStreamLimit: 3; auditLogStreamLimit: 3;
githubOrgSync: false;
samlSSO: false; samlSSO: false;
hsm: false; hsm: false;
oidcSSO: false; oidcSSO: false;
@@ -685,10 +685,16 @@ export const oidcConfigServiceFactory = ({
id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm id_token_signed_response_alg: oidcCfg.jwtSignatureAlgorithm
}); });
// Check if the OIDC provider supports PKCE
const codeChallengeMethods = client.issuer.metadata.code_challenge_methods_supported;
const supportsPKCE = Array.isArray(codeChallengeMethods) && codeChallengeMethods.includes("S256");
const strategy = new OpenIdStrategy( const strategy = new OpenIdStrategy(
{ {
client, client,
passReqToCallback: true passReqToCallback: true,
usePKCE: supportsPKCE,
params: supportsPKCE ? { code_challenge_method: "S256" } : undefined
}, },
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
(_req: any, tokenSet: TokenSet, cb: any) => { (_req: any, tokenSet: TokenSet, cb: any) => {
@@ -8,7 +8,8 @@ export enum OIDCConfigurationType {
export enum OIDCJWTSignatureAlgorithm { export enum OIDCJWTSignatureAlgorithm {
RS256 = "RS256", RS256 = "RS256",
HS256 = "HS256", HS256 = "HS256",
RS512 = "RS512" RS512 = "RS512",
EDDSA = "EdDSA"
} }
export type TOidcLoginDTO = { export type TOidcLoginDTO = {
@@ -74,6 +74,7 @@ export enum OrgPermissionSubjects {
IncidentAccount = "incident-contact", IncidentAccount = "incident-contact",
Sso = "sso", Sso = "sso",
Scim = "scim", Scim = "scim",
GithubOrgSync = "github-org-sync",
Ldap = "ldap", Ldap = "ldap",
Groups = "groups", Groups = "groups",
Billing = "billing", Billing = "billing",
@@ -101,6 +102,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.GithubOrgSync]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups] | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
| [OrgPermissionActions, OrgPermissionSubjects.SecretScanning] | [OrgPermissionActions, OrgPermissionSubjects.SecretScanning]
@@ -165,6 +167,10 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."), subject: z.literal(OrgPermissionSubjects.Scim).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}), }),
z.object({
subject: z.literal(OrgPermissionSubjects.GithubOrgSync).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
}),
z.object({ z.object({
subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."), subject: z.literal(OrgPermissionSubjects.Ldap).describe("The entity this permission pertains to."),
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.") action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionActions).describe("Describe what action an entity can take.")
@@ -273,6 +279,11 @@ const buildAdminPermission = () => {
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim); can(OrgPermissionActions.Delete, OrgPermissionSubjects.Scim);
can(OrgPermissionActions.Read, OrgPermissionSubjects.GithubOrgSync);
can(OrgPermissionActions.Create, OrgPermissionSubjects.GithubOrgSync);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.GithubOrgSync);
can(OrgPermissionActions.Delete, OrgPermissionSubjects.GithubOrgSync);
can(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Read, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Create, OrgPermissionSubjects.Ldap);
can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap); can(OrgPermissionActions.Edit, OrgPermissionSubjects.Ldap);
+1
View File
@@ -0,0 +1 @@
export const INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN = "x-infisical-github-auth-access-token";
+3 -3
View File
@@ -2,7 +2,7 @@ export const daysToMillisecond = (days: number) => days * 24 * 60 * 60 * 1000;
export const secondsToMillis = (seconds: number) => seconds * 1000; export const secondsToMillis = (seconds: number) => seconds * 1000;
export const applyJitter = (delayMs: number, jitterMs: number) => { export const applyJitter = (delay: number, jitter: number) => {
const jitter = Math.floor(Math.random() * (2 * jitterMs)) - jitterMs; const jitterTime = Math.floor(Math.random() * (2 * jitter)) - jitter;
return delayMs + jitter; return delay + jitterTime;
}; };
+20 -5
View File
@@ -2,6 +2,8 @@
import { Knex } from "knex"; import { Knex } from "knex";
import { Tables } from "knex/types/tables"; import { Tables } from "knex/types/tables";
import { TableName } from "@app/db/schemas";
import { DatabaseError } from "../errors"; import { DatabaseError } from "../errors";
import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic"; import { buildDynamicKnexQuery, TKnexDynamicOperator } from "./dynamic";
@@ -25,28 +27,41 @@ export type TFindFilter<R extends object = object> = Partial<R> & {
$search?: Partial<{ [k in keyof R]: R[k] }>; $search?: Partial<{ [k in keyof R]: R[k] }>;
$complex?: TKnexDynamicOperator<R>; $complex?: TKnexDynamicOperator<R>;
}; };
export const buildFindFilter = export const buildFindFilter =
<R extends object = object>({ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>) => <R extends object = object>(
{ $in, $notNull, $search, $complex, ...filter }: TFindFilter<R>,
tableName?: TableName,
excludeKeys?: Array<keyof R>
) =>
(bd: Knex.QueryBuilder<R, R>) => { (bd: Knex.QueryBuilder<R, R>) => {
void bd.where(filter); const processedFilter = tableName
? Object.fromEntries(
Object.entries(filter)
.filter(([key]) => !excludeKeys || !excludeKeys.includes(key as keyof R))
.map(([key, value]) => [`${tableName}.${key}`, value])
)
: filter;
void bd.where(processedFilter);
if ($in) { if ($in) {
Object.entries($in).forEach(([key, val]) => { Object.entries($in).forEach(([key, val]) => {
if (val) { if (val) {
void bd.whereIn(key as never, val as never); void bd.whereIn([`${tableName ? `${tableName}.` : ""}${key}`] as never, val as never);
} }
}); });
} }
if ($notNull?.length) { if ($notNull?.length) {
$notNull.forEach((key) => { $notNull.forEach((key) => {
void bd.whereNotNull(key as never); void bd.whereNotNull([`${tableName ? `${tableName}.` : ""}${key as string}`] as never);
}); });
} }
if ($search) { if ($search) {
Object.entries($search).forEach(([key, val]) => { Object.entries($search).forEach(([key, val]) => {
if (val) { if (val) {
void bd.whereILike(key as never, val as never); void bd.whereILike([`${tableName ? `${tableName}.` : ""}${key}`] as never, val as never);
} }
}); });
} }
+14
View File
@@ -16,3 +16,17 @@ export const fetchGithubEmails = async (accessToken: string) => {
}); });
return data; return data;
}; };
type TGithubUser = {
name?: string;
login: string;
};
export const fetchGithubUser = async (accessToken: string) => {
const { data } = await request.get<TGithubUser>(`${INTEGRATION_GITHUB_API_URL}/user`, {
headers: {
Authorization: `Bearer ${accessToken}`
}
});
return data;
};
+4 -4
View File
@@ -15,13 +15,13 @@ export const blockLocalAndPrivateIpAddresses = async (url: string) => {
const validUrl = new URL(url); const validUrl = new URL(url);
const inputHostIps: string[] = []; const inputHostIps: string[] = [];
if (isIPv4(validUrl.host)) { if (isIPv4(validUrl.hostname)) {
inputHostIps.push(validUrl.host); inputHostIps.push(validUrl.hostname);
} else { } else {
if (validUrl.host === "localhost" || validUrl.host === "host.docker.internal") { if (validUrl.hostname === "localhost" || validUrl.hostname === "host.docker.internal") {
throw new BadRequestError({ message: "Local IPs not allowed as URL" }); throw new BadRequestError({ message: "Local IPs not allowed as URL" });
} }
const resolvedIps = await dns.resolve4(validUrl.host); const resolvedIps = await dns.resolve4(validUrl.hostname);
inputHostIps.push(...resolvedIps); inputHostIps.push(...resolvedIps);
} }
const isInternalIp = inputHostIps.some((el) => isPrivateIp(el)); const isInternalIp = inputHostIps.some((el) => isPrivateIp(el));
+15 -25
View File
@@ -33,6 +33,8 @@ import { gatewayDALFactory } from "@app/ee/services/gateway/gateway-dal";
import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { gatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal"; import { orgGatewayConfigDALFactory } from "@app/ee/services/gateway/org-gateway-config-dal";
import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal"; import { projectGatewayDALFactory } from "@app/ee/services/gateway/project-gateway-dal";
import { githubOrgSyncDALFactory } from "@app/ee/services/github-org-sync/github-org-sync-dal";
import { githubOrgSyncServiceFactory } from "@app/ee/services/github-org-sync/github-org-sync-service";
import { groupDALFactory } from "@app/ee/services/group/group-dal"; import { groupDALFactory } from "@app/ee/services/group/group-dal";
import { groupServiceFactory } from "@app/ee/services/group/group-service"; import { groupServiceFactory } from "@app/ee/services/group/group-service";
import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal"; import { userGroupMembershipDALFactory } from "@app/ee/services/group/user-group-membership-dal";
@@ -421,6 +423,7 @@ export const registerRoutes = async (
const gatewayDAL = gatewayDALFactory(db); const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db); const projectGatewayDAL = projectGatewayDALFactory(db);
const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db); const secretReminderRecipientsDAL = secretReminderRecipientsDALFactory(db);
const githubOrgSyncDAL = githubOrgSyncDALFactory(db);
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL); const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
@@ -558,6 +561,15 @@ export const registerRoutes = async (
externalGroupOrgRoleMappingDAL externalGroupOrgRoleMappingDAL
}); });
const githubOrgSyncConfigService = githubOrgSyncServiceFactory({
licenseService,
githubOrgSyncDAL,
kmsService,
permissionService,
groupDAL,
userGroupMembershipDAL
});
const ldapService = ldapConfigServiceFactory({ const ldapService = ldapConfigServiceFactory({
ldapConfigDAL, ldapConfigDAL,
ldapGroupMapDAL, ldapGroupMapDAL,
@@ -1529,6 +1541,7 @@ export const registerRoutes = async (
const secretSyncService = secretSyncServiceFactory({ const secretSyncService = secretSyncServiceFactory({
secretSyncDAL, secretSyncDAL,
secretImportDAL,
permissionService, permissionService,
appConnectionService, appConnectionService,
folderDAL, folderDAL,
@@ -1689,7 +1702,8 @@ export const registerRoutes = async (
kmipOperation: kmipOperationService, kmipOperation: kmipOperationService,
gateway: gatewayService, gateway: gatewayService,
secretRotationV2: secretRotationV2Service, secretRotationV2: secretRotationV2Service,
assumePrivileges: assumePrivilegeService assumePrivileges: assumePrivilegeService,
githubOrgSync: githubOrgSyncConfigService
}); });
const cronJobs: CronJob[] = []; const cronJobs: CronJob[] = [];
@@ -1750,30 +1764,6 @@ export const registerRoutes = async (
logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`); logger.info(`Raw event loop stats: ${JSON.stringify(histogram, null, 2)}`);
// try {
// await db.raw("SELECT NOW()");
// } catch (err) {
// logger.error("Health check: database connection failed", err);
// return reply.code(503).send({
// date: new Date(),
// message: "Service unavailable"
// });
// }
// if (cfg.isRedisConfigured) {
// const redis = new Redis(cfg.REDIS_URL);
// try {
// await redis.ping();
// redis.disconnect();
// } catch (err) {
// logger.error("Health check: redis connection failed", err);
// return reply.code(503).send({
// date: new Date(),
// message: "Service unavailable"
// });
// }
// }
return { return {
date: new Date(), date: new Date(),
message: "Ok", message: "Ok",
@@ -154,7 +154,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: z secrets: z
.object({ .object({
secretId: z.string(), secretId: z.string(),
referencedSecretKey: z.string() referencedSecretKey: z.string(),
referencedSecretEnv: z.string()
}) })
.array() .array()
.optional() .optional()
@@ -166,6 +167,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}) })
.array() .array()
.optional(), .optional(),
usedBySecretSyncs: z
.object({
name: z.string(),
destination: z.string(),
environment: z.string(),
id: z.string(),
path: z.string()
})
.array()
.optional(),
totalFolderCount: z.number().optional(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(), totalSecretCount: z.number().optional(),
@@ -500,6 +511,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
} }
} }
const usedBySecretSyncs: { name: string; destination: string; environment: string; id: string; path: string }[] =
[];
for await (const environment of environments) {
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
{ projectId, secretPath, environment },
req.permission
);
secretSyncs.forEach((sync) => {
usedBySecretSyncs.push({
name: sync.name,
destination: sync.destination,
environment,
id: sync.id,
path: sync.folder?.path || "/"
});
});
}
return { return {
folders, folders,
dynamicSecrets, dynamicSecrets,
@@ -512,6 +541,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount, totalSecretCount,
totalSecretRotationCount, totalSecretRotationCount,
importedByEnvs, importedByEnvs,
usedBySecretSyncs,
totalCount: totalCount:
(totalFolderCount ?? 0) + (totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) + (totalDynamicSecretCount ?? 0) +
@@ -611,6 +641,16 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalFolderCount: z.number().optional(), totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(), totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(), totalSecretCount: z.number().optional(),
usedBySecretSyncs: z
.object({
name: z.string(),
destination: z.string(),
environment: z.string(),
id: z.string(),
path: z.string()
})
.array()
.optional(),
importedBy: z importedBy: z
.object({ .object({
environment: z.object({ environment: z.object({
@@ -624,7 +664,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets: z secrets: z
.object({ .object({
secretId: z.string(), secretId: z.string(),
referencedSecretKey: z.string() referencedSecretKey: z.string(),
referencedSecretEnv: z.string()
}) })
.array() .array()
.optional() .optional()
@@ -904,6 +945,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
secrets secrets
}); });
const secretSyncs = await server.services.secretSync.listSecretSyncsBySecretPath(
{ projectId, secretPath, environment },
req.permission
);
const usedBySecretSyncs = secretSyncs.map((sync) => ({
name: sync.name,
destination: sync.destination,
environment: sync.environment?.name || environment,
id: sync.id,
path: sync.folder?.path || "/"
}));
if (secrets?.length || secretRotations?.length) { if (secrets?.length || secretRotations?.length) {
const secretCount = const secretCount =
(secrets?.length ?? 0) + (secrets?.length ?? 0) +
@@ -950,6 +1003,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalSecretCount, totalSecretCount,
totalSecretRotationCount, totalSecretRotationCount,
importedBy, importedBy,
usedBySecretSyncs,
totalCount: totalCount:
(totalImportCount ?? 0) + (totalImportCount ?? 0) +
(totalFolderCount ?? 0) + (totalFolderCount ?? 0) +
+87 -29
View File
@@ -9,15 +9,17 @@
import { Authenticator } from "@fastify/passport"; import { Authenticator } from "@fastify/passport";
import fastifySession from "@fastify/session"; import fastifySession from "@fastify/session";
import RedisStore from "connect-redis"; import RedisStore from "connect-redis";
import { Strategy as GitHubStrategy } from "passport-github";
import { Strategy as GitLabStrategy } from "passport-gitlab2"; import { Strategy as GitLabStrategy } from "passport-gitlab2";
import { Strategy as GoogleStrategy } from "passport-google-oauth20"; import { Strategy as GoogleStrategy } from "passport-google-oauth20";
import { Strategy as OAuth2Strategy } from "passport-oauth2";
import { z } from "zod"; import { z } from "zod";
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { BadRequestError, NotFoundError } from "@app/lib/errors"; import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
import { fetchGithubEmails } from "@app/lib/requests/github"; import { ms } from "@app/lib/ms";
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit } from "@app/server/config/rateLimiter";
import { AuthMethod } from "@app/services/auth/auth-type"; import { AuthMethod } from "@app/services/auth/auth-type";
import { OrgAuthMethod } from "@app/services/org/org-types"; import { OrgAuthMethod } from "@app/services/org/org-types";
@@ -42,6 +44,7 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
}); });
await server.register(passport.initialize()); await server.register(passport.initialize());
await server.register(passport.secureSession()); await server.register(passport.secureSession());
// passport oauth strategy for Google // passport oauth strategy for Google
const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN); const isGoogleOauthActive = Boolean(appCfg.CLIENT_ID_GOOGLE_LOGIN && appCfg.CLIENT_SECRET_GOOGLE_LOGIN);
if (isGoogleOauthActive) { if (isGoogleOauthActive) {
@@ -52,8 +55,9 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string, clientID: appCfg.CLIENT_ID_GOOGLE_LOGIN as string,
clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string, clientSecret: appCfg.CLIENT_SECRET_GOOGLE_LOGIN as string,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/google`,
scope: ["profile", " email"], scope: ["profile", "email"],
state: true state: true,
pkce: true
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req, _accessToken, _refreshToken, profile, cb) => { async (req, _accessToken, _refreshToken, profile, cb) => {
@@ -89,34 +93,44 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
const isGithubOauthActive = Boolean(appCfg.CLIENT_SECRET_GITHUB_LOGIN && appCfg.CLIENT_ID_GITHUB_LOGIN); const isGithubOauthActive = Boolean(appCfg.CLIENT_SECRET_GITHUB_LOGIN && appCfg.CLIENT_ID_GITHUB_LOGIN);
if (isGithubOauthActive) { if (isGithubOauthActive) {
passport.use( passport.use(
new GitHubStrategy( "github",
new OAuth2Strategy(
{ {
passReqToCallback: true, authorizationURL: "https://github.com/login/oauth/authorize",
clientID: appCfg.CLIENT_ID_GITHUB_LOGIN as string, tokenURL: "https://github.com/login/oauth/access_token",
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN as string, clientID: appCfg.CLIENT_ID_GITHUB_LOGIN!,
clientSecret: appCfg.CLIENT_SECRET_GITHUB_LOGIN!,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/github`,
scope: ["user:email"], scope: ["user:email", "read:org"],
// akhilmhdh: because the ts type for this is outdated by the maintainer state: true,
state: true as unknown as string pkce: true,
passReqToCallback: true
}, },
// eslint-disable-next-line // eslint-disable-next-line
async (req, accessToken, _refreshToken, profile, cb) => { async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
// @ts-expect-error this is because this is express type and not fastify
const callbackPort = req.session.get("callbackPort");
try { try {
const ghEmails = await fetchGithubEmails(accessToken); const ghEmails = await fetchGithubEmails(accessToken);
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0]; const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
if (!email) throw new Error("No primary email found");
// profile does not get automatically populated so we need to manually fetch user info
const user = await fetchGithubUser(accessToken);
const callbackPort = req.session.get("callbackPort");
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({ const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
email, email,
firstName: profile.displayName || profile.username || "", firstName: user.name || user.login,
lastName: "", lastName: "",
authMethod: AuthMethod.GITHUB, authMethod: AuthMethod.GITHUB,
callbackPort callbackPort
}); });
return cb(null, { isUserCompleted, providerAuthToken });
} catch (error) { done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
logger.error(error); } catch (err) {
cb(error as Error, false); logger.error(err);
done(err as Error, false);
} }
} }
) )
@@ -136,7 +150,8 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN, clientSecret: appCfg.CLIENT_SECRET_GITLAB_LOGIN,
callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`, callbackURL: `${appCfg.SITE_URL}/api/v1/sso/gitlab`,
baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL, baseURL: appCfg.CLIENT_GITLAB_LOGIN_URL,
state: true state: true,
pkce: true
}, },
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => { async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
try { try {
@@ -166,17 +181,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
method: "GET", method: "GET",
schema: { schema: {
querystring: z.object({ querystring: z.object({
callback_port: z.string().optional() callback_port: z.string().optional(),
is_admin_login: z
.string()
.optional()
.transform((val) => val === "true")
}) })
}, },
preValidation: [ preValidation: [
async (req, res) => { async (req, res) => {
const { callback_port: callbackPort } = req.query; const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
// ensure fresh session state per login attempt // ensure fresh session state per login attempt
await req.session.regenerate(); await req.session.regenerate();
if (callbackPort) { if (callbackPort) {
req.session.set("callbackPort", callbackPort); req.session.set("callbackPort", callbackPort);
} }
if (isAdminLogin) {
req.session.set("isAdminLogin", isAdminLogin);
}
return ( return (
passport.authenticate("google", { passport.authenticate("google", {
scope: ["profile", "email"], scope: ["profile", "email"],
@@ -200,10 +222,13 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
// this is due to zod type difference // this is due to zod type difference
}) as never, }) as never,
handler: async (req, res) => { handler: async (req, res) => {
const isAdminLogin = req.session.get("isAdminLogin");
await req.session.destroy(); await req.session.destroy();
if (req.passportUser.isUserCompleted) { if (req.passportUser.isUserCompleted) {
return res.redirect( return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${
isAdminLogin ? `&isAdminLogin=${isAdminLogin}` : ""
}`
); );
} }
return res.redirect( return res.redirect(
@@ -217,18 +242,26 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
method: "GET", method: "GET",
schema: { schema: {
querystring: z.object({ querystring: z.object({
callback_port: z.string().optional() callback_port: z.string().optional(),
is_admin_login: z
.string()
.optional()
.transform((val) => val === "true")
}) })
}, },
preValidation: [ preValidation: [
async (req, res) => { async (req, res) => {
const { callback_port: callbackPort } = req.query; const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
// ensure fresh session state per login attempt // ensure fresh session state per login attempt
await req.session.regenerate(); await req.session.regenerate();
if (callbackPort) { if (callbackPort) {
req.session.set("callbackPort", callbackPort); req.session.set("callbackPort", callbackPort);
} }
if (isAdminLogin) {
req.session.set("isAdminLogin", isAdminLogin);
}
return ( return (
passport.authenticate("github", { passport.authenticate("github", {
session: false, session: false,
@@ -289,10 +322,24 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
// this is due to zod type difference // this is due to zod type difference
}) as any, }) as any,
handler: async (req, res) => { handler: async (req, res) => {
const isAdminLogin = req.session.get("isAdminLogin");
await req.session.destroy(); await req.session.destroy();
if (req.passportUser.externalProviderAccessToken) {
void res.cookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, req.passportUser.externalProviderAccessToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: appCfg.HTTPS_ENABLED,
expires: new Date(Date.now() + ms(appCfg.JWT_PROVIDER_AUTH_LIFETIME))
});
}
if (req.passportUser.isUserCompleted) { if (req.passportUser.isUserCompleted) {
return res.redirect( return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${
isAdminLogin ? `&isAdminLogin=${isAdminLogin}` : ""
}`
); );
} }
return res.redirect( return res.redirect(
@@ -306,18 +353,26 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
method: "GET", method: "GET",
schema: { schema: {
querystring: z.object({ querystring: z.object({
callback_port: z.string().optional() callback_port: z.string().optional(),
is_admin_login: z
.string()
.optional()
.transform((val) => val === "true")
}) })
}, },
preValidation: [ preValidation: [
async (req, res) => { async (req, res) => {
const { callback_port: callbackPort } = req.query; const { callback_port: callbackPort, is_admin_login: isAdminLogin } = req.query;
// ensure fresh session state per login attempt // ensure fresh session state per login attempt
await req.session.regenerate(); await req.session.regenerate();
if (callbackPort) { if (callbackPort) {
req.session.set("callbackPort", callbackPort); req.session.set("callbackPort", callbackPort);
} }
if (isAdminLogin) {
req.session.set("isAdminLogin", isAdminLogin);
}
return ( return (
passport.authenticate("gitlab", { passport.authenticate("gitlab", {
session: false, session: false,
@@ -342,10 +397,13 @@ export const registerSsoRouter = async (server: FastifyZodProvider) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
}) as any, }) as any,
handler: async (req, res) => { handler: async (req, res) => {
const isAdminLogin = req.session.get("isAdminLogin");
await req.session.destroy(); await req.session.destroy();
if (req.passportUser.isUserCompleted) { if (req.passportUser.isUserCompleted) {
return res.redirect( return res.redirect(
`${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}` `${appCfg.SITE_URL}/login/sso?token=${encodeURIComponent(req.passportUser.providerAuthToken)}${
isAdminLogin ? `&isAdminLogin=${isAdminLogin}` : ""
}`
); );
} }
return res.redirect( return res.redirect(
@@ -1,5 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN } from "@app/lib/config/const";
import { getConfig } from "@app/lib/config/env"; import { getConfig } from "@app/lib/config/env";
import { authRateLimit } from "@app/server/config/rateLimiter"; import { authRateLimit } from "@app/server/config/rateLimiter";
@@ -70,6 +71,21 @@ export const registerLoginRouter = async (server: FastifyZodProvider) => {
}; };
} }
const githubOauthAccessToken = req.cookies[INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN];
if (githubOauthAccessToken) {
await server.services.githubOrgSync
.syncUserGroups(req.body.organizationId, tokens.user.userId, githubOauthAccessToken)
.finally(() => {
void res.setCookie(INFISICAL_PROVIDER_GITHUB_ACCESS_TOKEN, "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: cfg.HTTPS_ENABLED,
maxAge: 0
});
});
}
void res.setCookie("jid", tokens.refresh, { void res.setCookie("jid", tokens.refresh, {
httpOnly: true, httpOnly: true,
path: "/", path: "/",
@@ -69,6 +69,5 @@ export const listTeamCityProjects = async (appConnection: TTeamCityConnection) =
} }
); );
// Filter out the root project. Should not be seen by users. return resp.data.project;
return resp.data.project.filter((proj) => proj.id !== "_Root");
}; };
@@ -487,6 +487,7 @@ export const authLoginServiceFactory = ({
return { return {
...tokens, ...tokens,
user,
isMfaEnabled: false isMfaEnabled: false
}; };
}; };
@@ -795,7 +796,7 @@ export const authLoginServiceFactory = ({
organizationId organizationId
}); });
return { token, isMfaEnabled: false, user: userEnc } as const; return { token, isMfaEnabled: false, user: userEnc, decodedProviderToken } as const;
}; };
/* /*
@@ -177,6 +177,7 @@ export const deleteGithubSecrets = async ({
selected_repositories_url?: string | undefined; selected_repositories_url?: string | undefined;
} }
// @ts-expect-error just octokit ts compatiability issue
const OctokitWithRetry = Octokit.plugin(retry); const OctokitWithRetry = Octokit.plugin(retry);
let octokit: Octokit; let octokit: Octokit;
const appCfg = getConfig(); const appCfg = getConfig();
@@ -171,6 +171,19 @@ export const secretImportDALFactory = (db: TDbClient) => {
} }
}; };
const getFolderImports = async (secretPath: string, environmentId: string, tx?: Knex) => {
try {
const folderImports = await (tx || db.replicaNode())(TableName.SecretImport)
.where({ importPath: secretPath, importEnv: environmentId })
.join(TableName.SecretFolder, `${TableName.SecretImport}.folderId`, `${TableName.SecretFolder}.id`)
.join(TableName.Environment, `${TableName.SecretFolder}.envId`, `${TableName.Environment}.id`)
.select(db.ref("id").withSchema(TableName.SecretFolder).as("folderId"));
return folderImports;
} catch (error) {
throw new DatabaseError({ error, name: "get secret imports" });
}
};
const getFolderIsImportedBy = async ( const getFolderIsImportedBy = async (
secretPath: string, secretPath: string,
environmentId: string, environmentId: string,
@@ -203,7 +216,8 @@ export const secretImportDALFactory = (db: TDbClient) => {
db.ref("name").withSchema(TableName.Environment).as("envName"), db.ref("name").withSchema(TableName.Environment).as("envName"),
db.ref("slug").withSchema(TableName.Environment).as("envSlug"), db.ref("slug").withSchema(TableName.Environment).as("envSlug"),
db.ref("id").withSchema(TableName.SecretFolder).as("folderId"), db.ref("id").withSchema(TableName.SecretFolder).as("folderId"),
db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey") db.ref("secretKey").withSchema(TableName.SecretReferenceV2).as("referencedSecretKey"),
db.ref("environment").withSchema(TableName.SecretReferenceV2).as("referencedSecretEnv")
); );
const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({ const folderResults = folderImports.map(({ envName, envSlug, folderName, folderId }) => ({
@@ -214,13 +228,14 @@ export const secretImportDALFactory = (db: TDbClient) => {
})); }));
const secretResults = secretReferences.map( const secretResults = secretReferences.map(
({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey }) => ({ ({ envName, envSlug, secretId, folderName, folderId, referencedSecretKey, referencedSecretEnv }) => ({
envName, envName,
envSlug, envSlug,
secretId, secretId,
folderName, folderName,
folderId, folderId,
referencedSecretKey referencedSecretKey,
referencedSecretEnv
}) })
); );
@@ -235,6 +250,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
secrets: { secrets: {
secretId: string; secretId: string;
referencedSecretKey: string; referencedSecretKey: string;
referencedSecretEnv: string;
}[]; }[];
folderId: string; folderId: string;
folderImported: boolean; folderImported: boolean;
@@ -264,7 +280,11 @@ export const secretImportDALFactory = (db: TDbClient) => {
if ("secretId" in item && item.secretId) { if ("secretId" in item && item.secretId) {
updatedAcc[env].folders[folder].secrets = [ updatedAcc[env].folders[folder].secrets = [
...updatedAcc[env].folders[folder].secrets, ...updatedAcc[env].folders[folder].secrets,
{ secretId: item.secretId, referencedSecretKey: item.referencedSecretKey } {
secretId: item.secretId,
referencedSecretKey: item.referencedSecretKey,
referencedSecretEnv: item.referencedSecretEnv
}
]; ];
} else { } else {
updatedAcc[env].folders[folder].folderImported = true; updatedAcc[env].folders[folder].folderImported = true;
@@ -309,6 +329,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
findLastImportPosition, findLastImportPosition,
updateAllPosition, updateAllPosition,
getProjectImportCount, getProjectImportCount,
getFolderIsImportedBy getFolderIsImportedBy,
getFolderImports
}; };
}; };
@@ -808,7 +808,7 @@ export const secretImportServiceFactory = ({
actorOrgId, actorOrgId,
secrets secrets
}: TGetSecretImportsDTO & { }: TGetSecretImportsDTO & {
secrets: { secretKey: string; secretValue: string }[] | undefined; secrets: { secretKey: string; secretValue: string; id: string }[] | undefined;
}) => { }) => {
const { permission } = await permissionService.getProjectPermission({ const { permission } = await permissionService.getProjectPermission({
actor, actor,
@@ -877,7 +877,8 @@ export const secretImportServiceFactory = ({
) )
.map((otherSecret) => ({ .map((otherSecret) => ({
secretId: secret.secretKey, secretId: secret.secretKey,
referencedSecretKey: otherSecret.secretKey referencedSecretKey: otherSecret.secretKey,
referencedSecretEnv: environment
})); }));
}) || []; }) || [];
if (locallyReferenced.length > 0) { if (locallyReferenced.length > 0) {
@@ -56,11 +56,12 @@ export type FolderResult = {
export type SecretResult = { export type SecretResult = {
secretId: string; secretId: string;
referencedSecretKey: string; referencedSecretKey: string;
referencedSecretEnv: string;
} & FolderResult; } & FolderResult;
export type FolderInfo = { export type FolderInfo = {
folderName: string; folderName: string;
secrets?: { secretId: string; referencedSecretKey: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
folderId: string; folderId: string;
folderImported: boolean; folderImported: boolean;
envSlug?: string; envSlug?: string;
@@ -23,6 +23,7 @@ import {
TDeleteSecretSyncDTO, TDeleteSecretSyncDTO,
TFindSecretSyncByIdDTO, TFindSecretSyncByIdDTO,
TFindSecretSyncByNameDTO, TFindSecretSyncByNameDTO,
TListSecretSyncsByFolderId,
TListSecretSyncsByProjectId, TListSecretSyncsByProjectId,
TSecretSync, TSecretSync,
TTriggerSecretSyncImportSecretsByIdDTO, TTriggerSecretSyncImportSecretsByIdDTO,
@@ -31,12 +32,14 @@ import {
TUpdateSecretSyncDTO TUpdateSecretSyncDTO
} from "@app/services/secret-sync/secret-sync-types"; } from "@app/services/secret-sync/secret-sync-types";
import { TSecretImportDALFactory } from "../secret-import/secret-import-dal";
import { TSecretSyncDALFactory } from "./secret-sync-dal"; import { TSecretSyncDALFactory } from "./secret-sync-dal";
import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps"; import { SECRET_SYNC_CONNECTION_MAP, SECRET_SYNC_NAME_MAP } from "./secret-sync-maps";
import { TSecretSyncQueueFactory } from "./secret-sync-queue"; import { TSecretSyncQueueFactory } from "./secret-sync-queue";
type TSecretSyncServiceFactoryDep = { type TSecretSyncServiceFactoryDep = {
secretSyncDAL: TSecretSyncDALFactory; secretSyncDAL: TSecretSyncDALFactory;
secretImportDAL: TSecretImportDALFactory;
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">; appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">; permissionService: Pick<TPermissionServiceFactory, "getProjectPermission" | "getOrgPermission">;
projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">; projectBotService: Pick<TProjectBotServiceFactory, "getBotKey">;
@@ -53,6 +56,7 @@ export type TSecretSyncServiceFactory = ReturnType<typeof secretSyncServiceFacto
export const secretSyncServiceFactory = ({ export const secretSyncServiceFactory = ({
secretSyncDAL, secretSyncDAL,
folderDAL, folderDAL,
secretImportDAL,
permissionService, permissionService,
appConnectionService, appConnectionService,
projectBotService, projectBotService,
@@ -85,6 +89,37 @@ export const secretSyncServiceFactory = ({
return secretSyncs as TSecretSync[]; return secretSyncs as TSecretSync[];
}; };
const listSecretSyncsBySecretPath = async (
{ projectId, secretPath, environment }: TListSecretSyncsByFolderId,
actor: OrgServiceActor
) => {
const { permission } = await permissionService.getProjectPermission({
actor: actor.type,
actorId: actor.id,
actorAuthMethod: actor.authMethod,
actorOrgId: actor.orgId,
actionProjectType: ActionProjectType.SecretManager,
projectId
});
if (permission.cannot(ProjectPermissionSecretSyncActions.Read, ProjectPermissionSub.SecretSyncs)) {
return [];
}
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return [];
const folderImports = await secretImportDAL.getFolderImports(secretPath, folder.envId);
const secretSyncs = await secretSyncDAL.find({
$in: {
folderId: folderImports.map((folderImport) => folderImport.folderId).concat(folder.id)
}
});
return secretSyncs as TSecretSync[];
};
const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => { const findSecretSyncById = async ({ destination, syncId }: TFindSecretSyncByIdDTO, actor: OrgServiceActor) => {
const secretSync = await secretSyncDAL.findById(syncId); const secretSync = await secretSyncDAL.findById(syncId);
@@ -518,6 +553,7 @@ export const secretSyncServiceFactory = ({
return { return {
listSecretSyncOptions, listSecretSyncOptions,
listSecretSyncsByProjectId, listSecretSyncsByProjectId,
listSecretSyncsBySecretPath,
findSecretSyncById, findSecretSyncById,
findSecretSyncByName, findSecretSyncByName,
createSecretSync, createSecretSync,
@@ -144,6 +144,13 @@ export type TListSecretSyncsByProjectId = {
destination?: SecretSync; destination?: SecretSync;
}; };
export type TListSecretSyncsByFolderId = {
projectId: string;
secretPath: string;
environment: string;
destination?: SecretSync;
};
export type TFindSecretSyncByIdDTO = { export type TFindSecretSyncByIdDTO = {
syncId: string; syncId: string;
destination: SecretSync; destination: SecretSync;
@@ -10,7 +10,7 @@ import {
TTeamCitySyncWithCredentials TTeamCitySyncWithCredentials
} from "@app/services/secret-sync/teamcity/teamcity-sync-types"; } from "@app/services/secret-sync/teamcity/teamcity-sync-types";
// Note: Most variables won't be returned with a value due to them being a "password" type (starting with "env."). // Note: Most variables won't be returned with a value due to them being a "password" type.
// TeamCity API returns empty string for password-type variables for security reasons. // TeamCity API returns empty string for password-type variables for security reasons.
const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => { const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildConfig }: TTeamCityListVariables) => {
const { data } = await request.get<TTeamCityListVariablesResponse>( const { data } = await request.get<TTeamCityListVariablesResponse>(
@@ -25,12 +25,16 @@ const listTeamCityVariables = async ({ instanceUrl, accessToken, project, buildC
} }
); );
// Filters for only non-inherited environment variables
// Strips out "env." from map key, but the "name" field still has the original unaltered key. // Strips out "env." from map key, but the "name" field still has the original unaltered key.
return Object.fromEntries( return Object.fromEntries(
data.property.map((variable) => [ data.property
variable.name.startsWith("env.") ? variable.name.substring(4) : variable.name, .filter((variable) => !variable.inherited)
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security .filter((variable) => variable.name.startsWith("env."))
]) .map((variable) => [
variable.name.substring(4),
{ ...variable, value: variable.value || "" } // Password values will be empty strings from the API for security
])
); );
}; };
@@ -22,6 +22,7 @@ import type {
TFindSecretsByFolderIdsFilter, TFindSecretsByFolderIdsFilter,
TGetSecretsDTO TGetSecretsDTO
} from "@app/services/secret-v2-bridge/secret-v2-bridge-types"; } from "@app/services/secret-v2-bridge/secret-v2-bridge-types";
import { applyJitter } from "@app/lib/dates";
export const SecretServiceCacheKeys = { export const SecretServiceCacheKeys = {
get productKey() { get productKey() {
@@ -48,7 +49,7 @@ interface TSecretV2DalArg {
keyStore: TKeyStoreFactory; keyStore: TKeyStoreFactory;
} }
export const SECRET_DAL_TTL = 5 * 60; export const SECRET_DAL_TTL = () => applyJitter(10 * 60, 2 * 60);
export const SECRET_DAL_VERSION_TTL = 15 * 60; export const SECRET_DAL_VERSION_TTL = 15 * 60;
export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024; export const MAX_SECRET_CACHE_BYTES = 25 * 1024 * 1024;
export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => { export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
@@ -63,7 +64,8 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => { const findOne = async (filter: Partial<TSecretsV2>, tx?: Knex) => {
try { try {
const docs = await (tx || db)(TableName.SecretV2) const docs = await (tx || db)(TableName.SecretV2)
.where(filter) // eslint-disable-next-line @typescript-eslint/no-misused-promises
.where(buildFindFilter(filter, TableName.SecretV2))
.leftJoin( .leftJoin(
TableName.SecretV2JnTag, TableName.SecretV2JnTag,
`${TableName.SecretV2}.id`, `${TableName.SecretV2}.id`,
@@ -2,7 +2,7 @@ import path from "node:path";
import RE2 from "re2"; import RE2 from "re2";
import { TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas"; import { SecretType, TableName, TSecretFolders, TSecretsV2 } from "@app/db/schemas";
import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { ForbiddenRequestError, NotFoundError } from "@app/lib/errors";
import { groupBy } from "@app/lib/fn"; import { groupBy } from "@app/lib/fn";
import { logger } from "@app/lib/logger"; import { logger } from "@app/lib/logger";
@@ -720,7 +720,7 @@ export const reshapeBridgeSecret = (
secretReminderRecipients: secret.secretReminderRecipients || [], secretReminderRecipients: secret.secretReminderRecipients || [],
...(secretValueHidden ...(secretValueHidden
? { ? {
secretValue: INFISICAL_SECRET_VALUE_HIDDEN_MASK, secretValue: secret.type === SecretType.Personal ? secret.value : INFISICAL_SECRET_VALUE_HIDDEN_MASK,
secretValueHidden: true secretValueHidden: true
} }
: { : {
@@ -962,7 +962,7 @@ export const secretV2BridgeServiceFactory = ({
const encryptedCachedSecrets = await keyStore.getItem(cacheKey); const encryptedCachedSecrets = await keyStore.getItem(cacheKey);
if (encryptedCachedSecrets) { if (encryptedCachedSecrets) {
try { try {
await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL); await keyStore.setExpiry(cacheKey, SECRET_DAL_TTL());
const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") }); const cachedSecrets = secretManagerDecryptor({ cipherTextBlob: Buffer.from(encryptedCachedSecrets, "base64") });
const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as { const { secrets, imports = [] } = JSON.parse(cachedSecrets.toString("utf8")) as {
secrets: typeof decryptedSecrets; secrets: typeof decryptedSecrets;
@@ -1132,7 +1132,7 @@ export const secretV2BridgeServiceFactory = ({
plainText: Buffer.from(JSON.stringify(payload)) plainText: Buffer.from(JSON.stringify(payload))
}).cipherTextBlob; }).cipherTextBlob;
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) { if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64")); await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL(), encryptedUpdatedCachedSecrets.toString("base64"));
} }
return payload; return payload;
} }
@@ -1179,7 +1179,7 @@ export const secretV2BridgeServiceFactory = ({
plainText: Buffer.from(JSON.stringify(payload)) plainText: Buffer.from(JSON.stringify(payload))
}).cipherTextBlob; }).cipherTextBlob;
if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) { if (encryptedUpdatedCachedSecrets.byteLength < MAX_SECRET_CACHE_BYTES) {
await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL, encryptedUpdatedCachedSecrets.toString("base64")); await keyStore.setItemWithExpiry(cacheKey, SECRET_DAL_TTL(), encryptedUpdatedCachedSecrets.toString("base64"));
} }
return payload; return payload;
}; };
@@ -0,0 +1,40 @@
---
title: "Assume Privileges"
description: "Learn how to temporarily assume the privileges of a user or machine identity within a project."
---
This feature allows authorized users to temporarily take on the permissions of another user or identity. It helps administrators and access managers test and verify permissions before granting access, ensuring everything is set up correctly.
It also reduces back-and-forth with end users when troubleshooting permission-related issues.
## How It Works
When an authorized user activates assume privileges mode, they temporarily inherit the target user or identity’s permissions for up to one hour.
During this time, they can perform actions within the system with the same level of access as the target user.
- **Permission-based**: Only permissions are inherited, not the full identity
- **Time-limited**: Access automatically expires after one hour
- **Audited**: All actions are logged under the original user's account. This means any action taken during the session will be recorded under the entity assuming the privileges, not the target entity.
- **Authorization required**: Only users with the specific **assume privilege** permission can use this feature
- **Scoped to a single project**: You can only assume privileges for one project at a time
## How to Assume Privileges
<Steps>
<Step title="Go to Project Access">
Click on the user or identity you want to assume.
![Access control page](/images/platform/access-controls/assume-privileges/access-control.png)
</Step>
<Step title="Click Assume Privilege">
Click **Assume Privilege**, then type `assume` to confirm and start your session.
![Access control detail page](/images/platform/access-controls/assume-privileges/access-control-detail.png)
</Step>
<Step title="Session is Active">
You will see a yellow banner indicating that your assume privilege session is active. You can exit at any time by clicking **Exit**.
![session start](/images/platform/access-controls/assume-privileges/session-start.png)
</Step>
</Steps>
@@ -0,0 +1,56 @@
---
title: "GitHub Team Sync"
description: "Learn how to automatically synchronize your GitHub teams with Infisical Groups."
---
## Overview
The GitHub Organization Synchronization feature streamlines user and group management by automatically syncing users belonging to your specified GitHub organization with corresponding groups within Infisical. This integration ensures that users logging in via GitHub are automatically added to or removed from Infisical groups based on their team memberships within your GitHub organization.
## Configuration
To enable and configure GitHub Organization Synchronization, follow these steps:
<Steps>
<Step title="Set up GitHub organization configuration">
1. Navigate to **Organization Settings** and select the **Security Tab**.
![config](../../images/platform/external-syncs/github-org-sync-section.png)
2. Click the **Configure** button and provide the name of your GitHub Organization.
![config-modal](../../images/platform/external-syncs/github-org-sync-config-modal.png)
</Step>
<Step title="Enable GitHub organization sync">
Toggle ON GitHub Organization sync to activate sync.
![toggle-on](../../images/platform/external-syncs/github-org-sync-active.png)
</Step>
<Step title="Approve the Infisical OAuth application on your organization">
Connecting the Infisical OAuth application grants it permission to **read:org** details. This approval is done by selecting your organization during the GitHub OAuth login process.
1. Initiate the login process via the GitHub OAuth flow.
![oauth-flow-start](../../images/platform/external-syncs/github-org-sync-oauth-flow-start.png)
2. Select the organization you have connected.
3. Grant access to Infisical oauth application to your configured organization. Infisical shown here is an organization, just for walkthrough.
![grant-access](../../images/platform/external-syncs/github-org-sync-oauth.png)
<Info>
This action only needs to be done once and authorizes the Infisical OAuth app to read organization details, including team information.
The following users don't need to select organization in GitHub on login anymore.
</Info>
</Step>
</Steps>
## Working
Once configured, the GitHub Organization Synchronization feature functions as follows:
When a user logs in via the GitHub OAuth flow and selects the configured organization, the system will then automatically synchronize the teams they are a part of in GitHub with corresponding groups in Infisical.
## Troubleshooting
<Accordion title="Please check if your organization has approved the Infisical OAuth application.">
If you encounter an error related to this, it indicates that you need to approve the Infisical OAuth application within your GitHub organization.
You can verify the application's approval status by navigating to **https://github.com/organizations/__your-organization__/settings/oauth_application_policy**. Replace `__your-organization__` with the actual name of your GitHub organization.
![check-approval](../../images/platform/external-syncs/github-org-sync-approved-oauth-apps.png)
</Accordion>
@@ -9,6 +9,9 @@ This guide will walk you through the steps needed to configure external KMS supp
## Prerequisites ## Prerequisites
- An AWS KMS Key configured as a `Symmetric` key and with `Encrypt and Decrypt` key usage.
![Create AWS KMS Key](/images/platform/kms/aws/aws-kms-key-create.png)
Before you begin, you'll first need to choose a method of authentication with AWS from below. Before you begin, you'll first need to choose a method of authentication with AWS from below.
<Tabs> <Tabs>
@@ -268,11 +268,11 @@ For organizations that work with US government agencies, FIPS compliance is almo
<Steps> <Steps>
<Step title="Create HSM client folder"> <Step title="Create HSM client folder">
When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes. When using Kubernetes, you need to mount the path containing the HSM client files. This section covers how to configure your Infisical instance to use an HSM with Kubernetes. In this example, we are going to be using `/etc/luna-docker`.
```bash ```bash
mkdir /etc/hsm-client mkdir /etc/luna-docker
``` ```
After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client. After [setting up your Luna Cloud HSM client](https://thalesdocs.com/gphsm/luna/7/docs/network/Content/install/client_install/add_dpod.htm), you should have a set of files, referred to as the HSM client. You don't need all the files, but for simplicity we recommend copying all the files from the client.
@@ -306,20 +306,60 @@ For organizations that work with US government agencies, FIPS compliance is almo
The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step. The most important parts of the client folder is the `Chrystoki.conf` file, and the `libs`, `plugins`, and `jsp` folders. You need to copy these files to the folder you created in the first step.
```bash ```bash
cp -r /<path-to-where-your-hsm-client-is-located> /etc/hsm-client cp -r /<path-to-where-your-luna-client-is-located>/* /etc/luna-docker
``` ```
<Note>
The `/*` wildcard will copy all files and folders within the HSM client. The wildcard is important to ensure that the file structure is inline with the rest of this guide.
</Note>
After copying the files, the `/etc/luna-docker` directory should have the following file structure:
```bash
$ ls -R /etc/luna-docker
Chrystoki.conf etc lock server-certificate.pem
Chrystoki.conf.tmp2E jsp partition-ca-certificate.pem setenv
lch-support-linux-64bit partition-certificate.pem
bin libs plugins
/etc/luna-docker/bin:
64
/etc/luna-docker/bin/64:
ckdemo cmu lunacm multitoken vtl
/etc/luna-docker/etc:
openssl.cnf
/etc/luna-docker/jsp:
64 LunaProvider.jar
/etc/luna-docker/jsp/64:
libLunaAPI.so
/etc/luna-docker/libs:
64
/etc/luna-docker/libs/64:
libCryptoki2.so
/etc/luna-docker/lock:
/etc/luna-docker/plugins:
libcloud.plugin
```
</Step> </Step>
<Step title="Update Chrystoki.conf"> <Step title="Update Chrystoki.conf">
The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths. The `Chrystoki.conf` file is used to configure the HSM client. You need to update the `Chrystoki.conf` file to point to the correct file paths.
In this example, we will be mounting the `/etc/hsm-client` folder from the host to containers in our deployment's pods at the path `/hsm-client`. This means the contents of `/etc/hsm-client` on the host will be accessible at `/hsm-client` within the containers. In this example, we will be mounting the `/etc/luna-docker` folder from the host to containers in our deployment's pods at the path `/usr/safenet/lunaclient`. This means the contents of `/etc/luna-docker` on the host will be accessible at `/usr/safenet/lunaclient` within the containers.
An example config file will look like this: An example config file will look like this:
```Chrystoki.conf ```Chrystoki.conf
Chrystoki2 = { Chrystoki2 = {
# This path points to the mounted path, /hsm-client # This path points to the mounted path, /usr/safenet/lunaclient
LibUNIX64 = /hsm-client/libs/64/libCryptoki2.so; LibUNIX64 = /usr/safenet/lunaclient/libs/64/libCryptoki2.so;
} }
Luna = { Luna = {
@@ -339,8 +379,8 @@ For organizations that work with US government agencies, FIPS compliance is almo
Misc = { Misc = {
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
PluginModuleDir = /hsm-client/plugins; PluginModuleDir = /usr/safenet/lunaclient/plugins;
MutexFolder = /hsm-client/lock; MutexFolder = /usr/safenet/lunaclient/lock;
PE1746Enabled = 1; PE1746Enabled = 1;
ToolsDir = /usr/bin; ToolsDir = /usr/bin;
@@ -353,7 +393,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
LunaSA Client = { LunaSA Client = {
ReceiveTimeout = 20000; ReceiveTimeout = 20000;
# Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step. # Update the paths to point to the mounted path if your folder structure is different from the one mentioned in the previous step.
SSLConfigFile = /hsm-client/etc/openssl.cnf; SSLConfigFile = /usr/safenet/lunaclient/etc/openssl.cnf;
ClientPrivKeyFile = ./etc/ClientNameKey.pem; ClientPrivKeyFile = ./etc/ClientNameKey.pem;
ClientCertFile = ./etc/ClientNameCert.pem; ClientCertFile = ./etc/ClientNameCert.pem;
ServerCAFile = ./etc/CAFile.pem; ServerCAFile = ./etc/CAFile.pem;
@@ -441,7 +481,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
```bash ```bash
kubectl exec hsm-setup-pod -- mkdir -p /data/ # Create the data directory kubectl exec hsm-setup-pod -- mkdir -p /data/ # Create the data directory
kubectl cp ./hsm-client/ hsm-setup-pod:/data/ # Copy the HSM client files into the PVC kubectl cp /etc/luna-docker/. hsm-setup-pod:/data/ # Copy the HSM client files into the PVC
kubectl exec hsm-setup-pod -- chmod -R 755 /data/ # Set the correct permissions for the HSM client files kubectl exec hsm-setup-pod -- chmod -R 755 /data/ # Set the correct permissions for the HSM client files
``` ```
@@ -456,7 +496,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`. Next we need to update the environment variables used for the deployment. If you followed the [setup instructions for Kubernetes deployments](/self-hosting/deployment-options/kubernetes-helm), you should have a Kubernetes secret called `infisical-secrets`.
We need to update the secret with the following environment variables: We need to update the secret with the following environment variables:
- `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/hsm-client/libs/64/libCryptoki2.so`)_ - `HSM_LIB_PATH` - The path to the HSM client library _(mapped to `/usr/safenet/lunaclient/libs/64/libCryptoki2.so`)_
- `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client - `HSM_PIN` - The PIN for the HSM device that you created when setting up your Luna Cloud HSM client
- `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client - `HSM_SLOT` - The slot number for the HSM device that you selected when setting up your Luna Cloud HSM client
- `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label. - `HSM_KEY_LABEL` - The label for the HSM key. If no key is found with the provided key label, the HSM will create a new key with the provided label.
@@ -471,7 +511,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
type: Opaque type: Opaque
stringData: stringData:
# ... Other environment variables ... # ... Other environment variables ...
HSM_LIB_PATH: "/hsm-client/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client HSM_LIB_PATH: "/usr/safenet/lunaclient/libs/64/libCryptoki2.so" # If you followed this guide, this will be the path of the Luna Cloud HSM client
HSM_PIN: "<your-hsm-device-pin>" HSM_PIN: "<your-hsm-device-pin>"
HSM_SLOT: "<hsm-device-slot>" HSM_SLOT: "<hsm-device-slot>"
HSM_KEY_LABEL: "<your-key-label>" HSM_KEY_LABEL: "<your-key-label>"
@@ -487,7 +527,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
<Step title="Updating the Deployment"> <Step title="Updating the Deployment">
After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files. After we've successfully configured the PVC and updated our environment variables, we are ready to update the deployment configuration so that the pods it creates can access the HSM client files.
We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with support for HSM encryption. We need to update the Docker image of the deployment to use `infisical/infisical-fips`. The `infisical/infisical-fips` image is a functionally identical image to the `infisical/infisical` image, but it is built with HSM support.
```yaml ```yaml
# ... The rest of the values.yaml file ... # ... The rest of the values.yaml file ...
@@ -499,8 +539,7 @@ For organizations that work with US government agencies, FIPS compliance is almo
extraVolumeMounts: extraVolumeMounts:
- name: hsm-data - name: hsm-data
mountPath: /hsm-client # The path we will mount the HSM client files to mountPath: /usr/safenet/lunaclient # The path we will mount the HSM client files to
subPath: ./hsm-client
extraVolumes: extraVolumes:
- name: hsm-data - name: hsm-data
Binary file not shown.

After

Width:  |  Height:  |  Size: 335 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

+6 -1
View File
@@ -34,7 +34,7 @@ description: "Learn how to configure a TeamCity Sync for Infisical."
- **Build Configuration**: The build configuration to sync secrets to. - **Build Configuration**: The build configuration to sync secrets to.
<Note> <Note>
Not including a Build Configuration will sync secrets to the entire project. Not including a Build Configuration will sync secrets to the project.
</Note> </Note>
5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**. 5. Configure the **Sync Options** to specify how secrets should be synced, then click **Next**.
@@ -44,6 +44,11 @@ description: "Learn how to configure a TeamCity Sync for Infisical."
- **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical. - **Overwrite Destination Secrets**: Removes any secrets at the destination endpoint not present in Infisical.
- **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over TeamCity when keys conflict. - **Import Secrets (Prioritize Infisical)**: Imports secrets from the destination endpoint before syncing, prioritizing values from Infisical over TeamCity when keys conflict.
- **Import Secrets (Prioritize TeamCity)**: Imports secrets from the destination endpoint before syncing, prioritizing values from TeamCity over Infisical when keys conflict. - **Import Secrets (Prioritize TeamCity)**: Imports secrets from the destination endpoint before syncing, prioritizing values from TeamCity over Infisical when keys conflict.
<Note>
Infisical only syncs secrets from within the target scope; inherited secrets will not be imported.
</Note>
- **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only. - **Auto-Sync Enabled**: If enabled, secrets will automatically be synced from the source location when changes occur. Disable to enforce manual syncing only.
- **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical. - **Disable Secret Deletion**: If enabled, Infisical will not remove secrets from the sync destination. Enable this option if you intend to manage some secrets manually outside of Infisical.
+5 -3
View File
@@ -160,6 +160,7 @@
}, },
"documentation/platform/access-controls/additional-privileges", "documentation/platform/access-controls/additional-privileges",
"documentation/platform/access-controls/temporary-access", "documentation/platform/access-controls/temporary-access",
"documentation/platform/access-controls/assume-privilege",
"documentation/platform/access-controls/access-requests", "documentation/platform/access-controls/access-requests",
"documentation/platform/access-controls/project-access-requests", "documentation/platform/access-controls/project-access-requests",
"documentation/platform/pr-workflows", "documentation/platform/pr-workflows",
@@ -299,7 +300,8 @@
"documentation/platform/scim/jumpcloud", "documentation/platform/scim/jumpcloud",
"documentation/platform/scim/group-mappings" "documentation/platform/scim/group-mappings"
] ]
} },
"documentation/platform/github-org-sync"
] ]
}, },
{ {
@@ -886,8 +888,8 @@
] ]
}, },
{ {
"group": "LDAP Password", "group": "LDAP Password",
"pages": [ "pages": [
"api-reference/endpoints/secret-rotations/ldap-password/create", "api-reference/endpoints/secret-rotations/ldap-password/create",
"api-reference/endpoints/secret-rotations/ldap-password/delete", "api-reference/endpoints/secret-rotations/ldap-password/delete",
"api-reference/endpoints/secret-rotations/ldap-password/get-by-id", "api-reference/endpoints/secret-rotations/ldap-password/get-by-id",
@@ -19,6 +19,7 @@ type Props = {
formContent?: ReactNode; formContent?: ReactNode;
children?: ReactNode; children?: ReactNode;
deletionMessage?: ReactNode; deletionMessage?: ReactNode;
buttonColorSchema?: "danger" | "primary" | "secondary" | "gray" | null;
}; };
export const DeleteActionModal = ({ export const DeleteActionModal = ({
@@ -32,6 +33,7 @@ export const DeleteActionModal = ({
buttonText = "Delete", buttonText = "Delete",
formContent, formContent,
deletionMessage, deletionMessage,
buttonColorSchema = "danger",
children children
}: Props): JSX.Element => { }: Props): JSX.Element => {
const [inputData, setInputData] = useState(""); const [inputData, setInputData] = useState("");
@@ -67,7 +69,7 @@ export const DeleteActionModal = ({
<div className="mx-2 flex items-center"> <div className="mx-2 flex items-center">
<Button <Button
className="mr-4" className="mr-4"
colorSchema="danger" colorSchema={buttonColorSchema}
isDisabled={!(deleteKey === inputData) || isLoading} isDisabled={!(deleteKey === inputData) || isLoading}
onClick={onDelete} onClick={onDelete}
isLoading={isLoading} isLoading={isLoading}
@@ -93,6 +93,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
onFocus={(evt) => { onFocus={(evt) => {
onFocus?.(evt); onFocus?.(evt);
setIsSecretFocused.on(); setIsSecretFocused.on();
evt.currentTarget.select();
}} }}
disabled={isDisabled} disabled={isDisabled}
spellCheck={false} spellCheck={false}
@@ -35,7 +35,8 @@ export enum OrgPermissionSubjects {
AppConnections = "app-connections", AppConnections = "app-connections",
Kmip = "kmip", Kmip = "kmip",
Gateway = "gateway", Gateway = "gateway",
SecretShare = "secret-share" SecretShare = "secret-share",
GithubOrgSync = "github-org-sync"
} }
export enum OrgPermissionAdminConsoleAction { export enum OrgPermissionAdminConsoleAction {
@@ -93,6 +94,7 @@ export type OrgPermissionSet =
| [OrgPermissionActions, OrgPermissionSubjects.Settings] | [OrgPermissionActions, OrgPermissionSubjects.Settings]
| [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount] | [OrgPermissionActions, OrgPermissionSubjects.IncidentAccount]
| [OrgPermissionActions, OrgPermissionSubjects.Scim] | [OrgPermissionActions, OrgPermissionSubjects.Scim]
| [OrgPermissionActions, OrgPermissionSubjects.GithubOrgSync]
| [OrgPermissionActions, OrgPermissionSubjects.Sso] | [OrgPermissionActions, OrgPermissionSubjects.Sso]
| [OrgPermissionActions, OrgPermissionSubjects.Ldap] | [OrgPermissionActions, OrgPermissionSubjects.Ldap]
| [OrgPermissionGroupActions, OrgPermissionSubjects.Groups] | [OrgPermissionGroupActions, OrgPermissionSubjects.Groups]
+11 -1
View File
@@ -25,9 +25,18 @@ export type DashboardProjectSecretsOverviewResponse = {
totalUniqueFoldersInPage: number; totalUniqueFoldersInPage: number;
totalUniqueSecretImportsInPage: number; totalUniqueSecretImportsInPage: number;
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[];
usedBySecretSyncs?: UsedBySecretSyncs[];
totalUniqueSecretRotationsInPage: number; totalUniqueSecretRotationsInPage: number;
}; };
export type UsedBySecretSyncs = {
name: string;
destination: string;
environment: string;
id: string;
path: string;
};
export type DashboardProjectSecretsDetailsResponse = { export type DashboardProjectSecretsDetailsResponse = {
imports?: TSecretImport[]; imports?: TSecretImport[];
folders?: TSecretFolder[]; folders?: TSecretFolder[];
@@ -43,13 +52,14 @@ export type DashboardProjectSecretsDetailsResponse = {
totalSecretRotationCount?: number; totalSecretRotationCount?: number;
totalCount: number; totalCount: number;
importedBy?: ProjectSecretsImportedBy[]; importedBy?: ProjectSecretsImportedBy[];
usedBySecretSyncs?: UsedBySecretSyncs[];
}; };
export type ProjectSecretsImportedBy = { export type ProjectSecretsImportedBy = {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}; };
@@ -0,0 +1,6 @@
export {
useCreateGithubSyncOrgConfig,
useDeleteGithubSyncOrgConfig,
useUpdateGithubSyncOrgConfig
} from "./mutations";
export { githubOrgSyncConfigQueryKeys } from "./queries";
@@ -0,0 +1,42 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { githubOrgSyncConfigQueryKeys } from "./queries";
import { TCreateGithubOrgSyncDTO, TUpdateGithubOrgSyncDTO } from "./types";
export const useCreateGithubSyncOrgConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: TCreateGithubOrgSyncDTO) => {
return apiRequest.post("/api/v1/github-org-sync-config", dto);
},
onSuccess: () => {
queryClient.invalidateQueries(githubOrgSyncConfigQueryKeys.get());
}
});
};
export const useUpdateGithubSyncOrgConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (dto: TUpdateGithubOrgSyncDTO) => {
return apiRequest.patch("/api/v1/github-org-sync-config", dto);
},
onSuccess: () => {
queryClient.invalidateQueries(githubOrgSyncConfigQueryKeys.get());
}
});
};
export const useDeleteGithubSyncOrgConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => {
return apiRequest.delete("/api/v1/github-org-sync-config");
},
onSuccess: () => {
queryClient.invalidateQueries(githubOrgSyncConfigQueryKeys.get());
}
});
};
@@ -0,0 +1,20 @@
import { queryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TGithubOrgSyncConfig } from "./types";
export const githubOrgSyncConfigQueryKeys = {
allKey: () => ["github-org-sync-config"],
getKey: () => [...githubOrgSyncConfigQueryKeys.allKey(), "list"],
get: () =>
queryOptions({
queryKey: githubOrgSyncConfigQueryKeys.getKey(),
queryFn: async () => {
const { data } = await apiRequest.get<{ githubOrgSyncConfig: TGithubOrgSyncConfig }>(
"/api/v1/github-org-sync-config"
);
return data.githubOrgSyncConfig;
}
})
};
@@ -0,0 +1,20 @@
export type TGithubOrgSyncConfig = {
id: string;
orgId: string;
githubOrgAccessToken?: string;
githubOrgName: string;
createdAt: string;
isActive?: boolean;
};
export interface TCreateGithubOrgSyncDTO {
githubOrgName: string;
githubOrgAccessToken?: string;
isActive?: boolean;
}
export interface TUpdateGithubOrgSyncDTO {
githubOrgName?: string;
githubOrgAccessToken?: string;
isActive?: boolean;
}
+1
View File
@@ -12,6 +12,7 @@ export * from "./certificateTemplates";
export * from "./dynamicSecret"; export * from "./dynamicSecret";
export * from "./dynamicSecretLease"; export * from "./dynamicSecretLease";
export * from "./gateways"; export * from "./gateways";
export * from "./githubOrgSyncConfig";
export * from "./groups"; export * from "./groups";
export * from "./identities"; export * from "./identities";
export * from "./identityProjectAdditionalPrivilege"; export * from "./identityProjectAdditionalPrivilege";
+2 -1
View File
@@ -19,5 +19,6 @@ export type OIDCConfigData = {
export enum OIDCJWTSignatureAlgorithm { export enum OIDCJWTSignatureAlgorithm {
RS256 = "RS256", RS256 = "RS256",
HS256 = "HS256", HS256 = "HS256",
RS512 = "RS512" RS512 = "RS512",
EDDSA = "EdDSA"
} }
@@ -12,6 +12,7 @@ export type SubscriptionPlan = {
customAlerts: boolean; customAlerts: boolean;
customRateLimits: boolean; customRateLimits: boolean;
pitRecovery: boolean; pitRecovery: boolean;
githubOrgSync: boolean;
ipAllowlisting: boolean; ipAllowlisting: boolean;
rbac: boolean; rbac: boolean;
secretVersioning: boolean; secretVersioning: boolean;
@@ -269,12 +269,19 @@ export const InitialStep = ({
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={() => {
const callbackPort = queryParams.get("callback_port"); const callbackPort = queryParams.get("callback_port");
const searchParams = new URLSearchParams();
window.open( if (callbackPort) {
`/api/v1/sso/redirect/google${ searchParams.append("callback_port", callbackPort);
callbackPort ? `?callback_port=${callbackPort}` : "" }
}`
); if (isAdmin) {
searchParams.append("is_admin_login", "true");
}
const queryString = searchParams.toString();
window.open(`/api/v1/sso/redirect/google${queryString ? `?${queryString}` : ""}`);
window.close(); window.close();
}} }}
className="h-10 w-full bg-mineshaft-600" className="h-10 w-full bg-mineshaft-600"
@@ -291,13 +298,19 @@ export const InitialStep = ({
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={() => {
const callbackPort = queryParams.get("callback_port"); const callbackPort = queryParams.get("callback_port");
const searchParams = new URLSearchParams();
window.open( if (callbackPort) {
`/api/v1/sso/redirect/github${ searchParams.append("callback_port", callbackPort);
callbackPort ? `?callback_port=${callbackPort}` : "" }
}`
);
if (isAdmin) {
searchParams.append("is_admin_login", "true");
}
const queryString = searchParams.toString();
window.open(`/api/v1/sso/redirect/github${queryString ? `?${queryString}` : ""}`);
window.close(); window.close();
}} }}
className="h-10 w-full bg-mineshaft-600" className="h-10 w-full bg-mineshaft-600"
@@ -314,13 +327,19 @@ export const InitialStep = ({
variant="outline_bg" variant="outline_bg"
onClick={() => { onClick={() => {
const callbackPort = queryParams.get("callback_port"); const callbackPort = queryParams.get("callback_port");
const searchParams = new URLSearchParams();
window.open( if (callbackPort) {
`/api/v1/sso/redirect/gitlab${ searchParams.append("callback_port", callbackPort);
callbackPort ? `?callback_port=${callbackPort}` : "" }
}`
);
if (isAdmin) {
searchParams.append("is_admin_login", "true");
}
const queryString = searchParams.toString();
window.open(`/api/v1/sso/redirect/gitlab${queryString ? `?${queryString}` : ""}`);
window.close(); window.close();
}} }}
className="h-10 w-full bg-mineshaft-600" className="h-10 w-full bg-mineshaft-600"
@@ -27,9 +27,16 @@ type Props = {
email: string; email: string;
password: string; password: string;
setPassword: (password: string) => void; setPassword: (password: string) => void;
isAdminLogin?: boolean;
}; };
export const PasswordStep = ({ providerAuthToken, email, password, setPassword }: Props) => { export const PasswordStep = ({
providerAuthToken,
email,
password,
setPassword,
isAdminLogin
}: Props) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
@@ -114,7 +121,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
// case: user has orgs, so we navigate the user to select an org // case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) { if (userOrgs.length > 0) {
navigateToSelectOrganization(callbackPort); navigateToSelectOrganization(callbackPort, isAdminLogin);
} }
// case: no orgs found, so we navigate the user to create an org // case: no orgs found, so we navigate the user to create an org
else { else {
@@ -216,7 +223,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
// case: user has orgs, so we navigate the user to select an org // case: user has orgs, so we navigate the user to select an org
if (userOrgs.length > 0) { if (userOrgs.length > 0) {
navigateToSelectOrganization(callbackPort); navigateToSelectOrganization(callbackPort, isAdminLogin);
} }
// case: no orgs found, so we navigate the user to create an org // case: no orgs found, so we navigate the user to create an org
else { else {
@@ -249,7 +256,7 @@ export const PasswordStep = ({ providerAuthToken, email, password, setPassword }
const userOrgs = await fetchOrganizations(); const userOrgs = await fetchOrganizations();
if (userOrgs.length > 0) { if (userOrgs.length > 0) {
navigateToSelectOrganization(); navigateToSelectOrganization(undefined, isAdminLogin);
} else { } else {
await navigateUserToOrg(navigate); await navigateUserToOrg(navigate);
} }
@@ -34,6 +34,7 @@ export const LoginSsoPage = () => {
email={username} email={username}
password={password} password={password}
setPassword={setPassword} setPassword={setPassword}
isAdminLogin={search.isAdminLogin}
/> />
); );
default: default:
@@ -5,7 +5,8 @@ import { z } from "zod";
import { LoginSsoPage } from "./LoginSsoPage"; import { LoginSsoPage } from "./LoginSsoPage";
const LoginSSOQueryParamsSchema = z.object({ const LoginSSOQueryParamsSchema = z.object({
token: z.string() token: z.string(),
isAdminLogin: z.boolean().optional().catch(false)
}); });
export const Route = createFileRoute("/_restrict-login-signup/login/sso")({ export const Route = createFileRoute("/_restrict-login-signup/login/sso")({
@@ -110,6 +110,7 @@ export const formSchema = z.object({
"secret-scanning": generalPermissionSchema, "secret-scanning": generalPermissionSchema,
sso: generalPermissionSchema, sso: generalPermissionSchema,
scim: generalPermissionSchema, scim: generalPermissionSchema,
[OrgPermissionSubjects.GithubOrgSync]: generalPermissionSchema,
ldap: generalPermissionSchema, ldap: generalPermissionSchema,
billing: generalPermissionSchema, billing: generalPermissionSchema,
identity: identityPermissionSchema, identity: identityPermissionSchema,
@@ -63,6 +63,10 @@ const SIMPLE_PERMISSION_OPTIONS = [
title: "SCIM", title: "SCIM",
formName: "scim" formName: "scim"
}, },
{
title: "GitHub Organization Sync",
formName: OrgPermissionSubjects.GithubOrgSync
},
{ {
title: "External KMS", title: "External KMS",
formName: OrgPermissionSubjects.Kms formName: OrgPermissionSubjects.Kms
@@ -0,0 +1,172 @@
import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createNotification } from "@app/components/notifications";
import { Button, DeleteActionModal, FormControl, Input } from "@app/components/v2";
import {
useCreateGithubSyncOrgConfig,
useDeleteGithubSyncOrgConfig,
useUpdateGithubSyncOrgConfig
} from "@app/hooks/api";
import { TGithubOrgSyncConfig } from "@app/hooks/api/githubOrgSyncConfig/types";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = z.object({
githubOrgName: z.string(),
githubOrgAccessToken: z.string().optional()
});
export type FormData = z.infer<typeof schema>;
type Props = {
data?: TGithubOrgSyncConfig;
popUp: UsePopUpState<["githubOrgSyncConfig", "deleteGithubOrgSyncConfig"]>;
handlePopUpOpen: (
popUpName: keyof UsePopUpState<["deleteGithubOrgSyncConfig"]>,
data?: {
scimTokenId: string;
}
) => void;
handlePopUpToggle: (
popUpName: keyof UsePopUpState<["githubOrgSyncConfig", "deleteGithubOrgSyncConfig"]>,
state?: boolean
) => void;
};
export const GithubOrgSyncConfigModal = ({
popUp,
handlePopUpOpen,
handlePopUpToggle,
data
}: Props) => {
const isUpdate = Boolean(data);
const { mutateAsync: createGithubSyncOrgConfig } = useCreateGithubSyncOrgConfig();
const { mutateAsync: updateGithubSyncOrgConfig } = useUpdateGithubSyncOrgConfig();
const { mutateAsync: deleteGithubSyncOrgConfig } = useDeleteGithubSyncOrgConfig();
const {
control,
handleSubmit,
formState: { isSubmitting }
} = useForm<FormData>({
resolver: zodResolver(schema),
values: data ? { githubOrgName: data.githubOrgName } : undefined
});
const onFormSubmit = async ({ githubOrgName, githubOrgAccessToken }: FormData) => {
try {
if (isUpdate) {
await updateGithubSyncOrgConfig({
githubOrgName,
githubOrgAccessToken
});
createNotification({
text: "Successfully updated GitHub Organization Sync",
type: "success"
});
} else {
await createGithubSyncOrgConfig({
githubOrgName,
githubOrgAccessToken,
isActive: false
});
createNotification({
text: "Successfully created GitHub Organization Sync",
type: "success"
});
}
handlePopUpToggle("githubOrgSyncConfig");
} catch {
createNotification({
text: "Failed to setup GitHub Organization Sync",
type: "error"
});
}
};
const onDelete = async () => {
try {
await deleteGithubSyncOrgConfig();
handlePopUpToggle("deleteGithubOrgSyncConfig", false);
handlePopUpToggle("githubOrgSyncConfig", false);
createNotification({
text: "Successfully deleted GitHub Organization Sync",
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: "Failed to delete GitHub Organization Sync",
type: "error"
});
}
};
return (
<>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
defaultValue=""
name="githubOrgName"
render={({ field, fieldState: { error } }) => (
<FormControl
label="GitHub Organization Name"
isError={Boolean(error)}
errorText={error?.message}
>
<Input {...field} placeholder="example" />
</FormControl>
)}
/>
{/* <Controller
control={control}
name="githubOrgAccessToken"
render={({ field, fieldState: { error } }) => (
<FormControl
label="GitHub Org Scoped Access Token"
isError={Boolean(error)}
isOptional
errorText={error?.message}
helperText="A GitHub access token is required only for private organizations. It will not be visible after saving."
>
<Input {...field} placeholder="example" />
</FormControl>
)}
/> */}
<div className="flex gap-8 pt-4">
<Button type="submit" isLoading={isSubmitting} isDisabled={isSubmitting}>
{isUpdate ? "Update" : "Configure"}
</Button>
<Button
variant="plain"
colorSchema="secondary"
onClick={() => handlePopUpToggle("githubOrgSyncConfig", false)}
>
Cancel
</Button>
<div className="flex-grow" />
{isUpdate && (
<Button
onClick={() => handlePopUpOpen("deleteGithubOrgSyncConfig")}
colorSchema="danger"
>
Delete
</Button>
)}
</div>
</form>
<DeleteActionModal
isOpen={popUp.deleteGithubOrgSyncConfig.isOpen}
title="Are you sure want to remove GitHub organization sync?"
onChange={(isOpen) => handlePopUpToggle("deleteGithubOrgSyncConfig", isOpen)}
deleteKey="confirm"
onDeleteApproved={onDelete}
/>
</>
);
};
@@ -387,6 +387,7 @@ export const OIDCModal = ({ popUp, handlePopUpClose, handlePopUpToggle, hideDele
<SelectItem value={OIDCJWTSignatureAlgorithm.RS256}>RS256</SelectItem> <SelectItem value={OIDCJWTSignatureAlgorithm.RS256}>RS256</SelectItem>
<SelectItem value={OIDCJWTSignatureAlgorithm.RS512}>RS512</SelectItem> <SelectItem value={OIDCJWTSignatureAlgorithm.RS512}>RS512</SelectItem>
<SelectItem value={OIDCJWTSignatureAlgorithm.HS256}>HS256</SelectItem> <SelectItem value={OIDCJWTSignatureAlgorithm.HS256}>HS256</SelectItem>
<SelectItem value={OIDCJWTSignatureAlgorithm.EDDSA}>EdDSA</SelectItem>
</Select> </Select>
</FormControl> </FormControl>
)} )}
@@ -18,6 +18,7 @@ import { LDAPModal } from "./LDAPModal";
import { OIDCModal } from "./OIDCModal"; import { OIDCModal } from "./OIDCModal";
import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection"; import { OrgGeneralAuthSection } from "./OrgGeneralAuthSection";
import { OrgGenericAuthSection } from "./OrgGenericAuthSection"; import { OrgGenericAuthSection } from "./OrgGenericAuthSection";
import { OrgGithubSyncSection } from "./OrgGithubSyncSection";
import { OrgLDAPSection } from "./OrgLDAPSection"; import { OrgLDAPSection } from "./OrgLDAPSection";
import { OrgOIDCSection } from "./OrgOIDCSection"; import { OrgOIDCSection } from "./OrgOIDCSection";
import { OrgScimSection } from "./OrgSCIMSection"; import { OrgScimSection } from "./OrgSCIMSection";
@@ -183,6 +184,7 @@ export const OrgAuthTab = withPermission(
</> </>
)} )}
<OrgScimSection /> <OrgScimSection />
<OrgGithubSyncSection />
<UpgradePlanModal <UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen} isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)} onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
@@ -0,0 +1,114 @@
import { useQuery } from "@tanstack/react-query";
import { UpgradePlanModal } from "@app/components/license/UpgradePlanModal";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Modal, ModalContent, Skeleton, Spinner, Switch } from "@app/components/v2";
import { OrgPermissionActions, OrgPermissionSubjects, useSubscription } from "@app/context";
import { githubOrgSyncConfigQueryKeys, useUpdateGithubSyncOrgConfig } from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { GithubOrgSyncConfigModal } from "./GithubOrgSyncConfigModal";
export const OrgGithubSyncSection = () => {
const { subscription } = useSubscription();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
"upgradePlan",
"githubOrgSyncConfig",
"deleteGithubOrgSyncConfig"
] as const);
const githubOrgSyncConfig = useQuery({
...githubOrgSyncConfigQueryKeys.get(),
enabled: subscription.githubOrgSync,
retry: false
});
const updateGithubSyncOrgConfig = useUpdateGithubSyncOrgConfig();
const isPending = subscription.githubOrgSync && githubOrgSyncConfig.isPending;
const data = !isPending && !githubOrgSyncConfig?.isError ? githubOrgSyncConfig?.data : undefined;
return (
<div className="mt-4 rounded-lg border border-mineshaft-600 bg-mineshaft-900 p-6">
<p className="text-xl font-semibold text-gray-200">
Sync user groups from your GitHub Organization
</p>
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">GitHub Organization</h2>
<div className="flex gap-4">
<OrgPermissionCan I={OrgPermissionActions.Read} a={OrgPermissionSubjects.GithubOrgSync}>
{(isAllowed) => (
<Button
onClick={() =>
subscription.githubOrgSync
? handlePopUpOpen("githubOrgSyncConfig")
: handlePopUpOpen("upgradePlan")
}
colorSchema="secondary"
isDisabled={!isAllowed}
isLoading={isPending}
>
Configure
</Button>
)}
</OrgPermissionCan>
</div>
</div>
<p className="text-sm text-mineshaft-300">
{isPending ? <Skeleton /> : null}
{data ? data?.githubOrgName : "Not configured"}
</p>
</div>
{data && (
<div className="py-4">
<div className="mb-2 flex items-center justify-between">
<h2 className="text-md text-mineshaft-100">Enable GitHub Sync</h2>
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.GithubOrgSync}>
{(isAllowed) => (
<Switch
id="enable-sync"
onCheckedChange={(value) =>
updateGithubSyncOrgConfig.mutate({
isActive: value
})
}
isChecked={githubOrgSyncConfig?.data?.isActive ?? false}
isDisabled={!isAllowed}
>
{updateGithubSyncOrgConfig?.isPending && <Spinner size="xs" />}
</Switch>
)}
</OrgPermissionCan>
</div>
<p className="text-sm text-mineshaft-300">
Allow group provisioning/deprovisioning with GitHub
</p>
</div>
)}
<Modal
isOpen={popUp?.githubOrgSyncConfig?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("githubOrgSyncConfig", isOpen);
}}
>
<ModalContent
title="Manage GitHub Organization Sync"
subTitle="Sync your GitHub teams to Infisical organization groups"
>
<GithubOrgSyncConfigModal
data={data}
popUp={popUp}
handlePopUpOpen={handlePopUpOpen}
handlePopUpToggle={handlePopUpToggle}
/>
</ModalContent>
</Modal>
<UpgradePlanModal
isOpen={popUp.upgradePlan.isOpen}
onOpenChange={(isOpen) => handlePopUpToggle("upgradePlan", isOpen)}
text="You can use GitHub Organization Plan if you switch to Infisical's Enterprise plan."
/>
</div>
);
};
@@ -74,7 +74,7 @@ import {
useUpdateSecretV3 useUpdateSecretV3
} from "@app/hooks/api"; } from "@app/hooks/api";
import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard/queries"; import { useGetProjectSecretsOverview } from "@app/hooks/api/dashboard/queries";
import { DashboardSecretsOrderBy } from "@app/hooks/api/dashboard/types"; import { DashboardSecretsOrderBy, ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types";
import { OrderByDirection } from "@app/hooks/api/generic/types"; import { OrderByDirection } from "@app/hooks/api/generic/types";
import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries"; import { useUpdateFolderBatch } from "@app/hooks/api/secretFolders/queries";
import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types"; import { TUpdateFolderBatchDTO } from "@app/hooks/api/secretFolders/types";
@@ -274,7 +274,8 @@ export const OverviewPage = () => {
totalUniqueSecretImportsInPage, totalUniqueSecretImportsInPage,
totalUniqueDynamicSecretsInPage, totalUniqueDynamicSecretsInPage,
totalUniqueSecretRotationsInPage, totalUniqueSecretRotationsInPage,
importedByEnvs importedByEnvs,
usedBySecretSyncs
} = overview ?? {}; } = overview ?? {};
const secretImportsShaped = secretImports const secretImportsShaped = secretImports
@@ -726,6 +727,98 @@ export const OverviewPage = () => {
} }
}, [routerSearch.search]); }, [routerSearch.search]);
const selectedKeysCount = Object.keys(selectedEntries.secret).length;
const secretsToDeleteKeys = useMemo(() => {
return Object.values(selectedEntries.secret).flatMap((entries) =>
Object.values(entries).map((secret) => secret.key)
);
}, [selectedEntries]);
const filterAndMergeEnvironments = (
envNames: string[],
envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]
): ProjectSecretsImportedBy[] => {
const filteredEnvs = envs.filter((env) => envNames.includes(env.environment));
if (filteredEnvs.length === 0) return [];
const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy);
const groupedBySlug: Record<string, ProjectSecretsImportedBy[]> = {};
allImportedBy.forEach((item) => {
const { slug } = item.environment;
if (!groupedBySlug[slug]) groupedBySlug[slug] = [];
groupedBySlug[slug].push(item);
});
const mergedImportedBy = Object.values(groupedBySlug).map((group) => {
const { environment } = group[0];
const allFolders = group.flatMap((item) => item.folders);
const foldersByName: Record<string, (typeof allFolders)[number][]> = {};
allFolders.forEach((folder) => {
if (!foldersByName[folder.name]) foldersByName[folder.name] = [];
foldersByName[folder.name].push(folder);
});
const mergedFolders = Object.entries(foldersByName).map(([name, foldersData]) => {
const isImported = foldersData.some((folder) => folder.isImported);
const allSecrets = foldersData.flatMap((folder) => folder.secrets || []);
const uniqueSecrets: {
secretId: string;
referencedSecretKey: string;
referencedSecretEnv: string;
}[] = [];
const secretIds = new Set<string>();
allSecrets
.filter(
(secret) =>
!secretsToDeleteKeys ||
secretsToDeleteKeys.length === 0 ||
secretsToDeleteKeys.includes(secret.referencedSecretKey)
)
.forEach((secret) => {
if (!secretIds.has(secret.secretId)) {
secretIds.add(secret.secretId);
uniqueSecrets.push(secret);
}
});
return {
name,
isImported,
...(uniqueSecrets.length > 0 ? { secrets: uniqueSecrets } : {})
};
});
return {
environment,
folders: mergedFolders.filter(
(folder) => folder.isImported || (folder.secrets && folder.secrets.length > 0)
)
};
});
return mergedImportedBy;
};
const importedBy = useMemo(() => {
if (!importedByEnvs) return [];
if (selectedKeysCount === 0) {
return filterAndMergeEnvironments(
visibleEnvs.map(({ slug }) => slug),
importedByEnvs
);
}
return filterAndMergeEnvironments(
Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)),
importedByEnvs
);
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) { if (isProjectV3 && visibleEnvs.length > 0 && isOverviewLoading) {
return ( return (
<div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]"> <div className="container mx-auto flex h-screen w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
@@ -1044,7 +1137,9 @@ export const OverviewPage = () => {
secretPath={secretPath} secretPath={secretPath}
selectedEntries={selectedEntries} selectedEntries={selectedEntries}
resetSelectedEntries={resetSelectedEntries} resetSelectedEntries={resetSelectedEntries}
importedByEnvs={importedByEnvs} importedBy={importedBy}
secretsToDeleteKeys={secretsToDeleteKeys}
usedBySecretSyncs={usedBySecretSyncs}
/> />
<div className="thin-scrollbar mt-4"> <div className="thin-scrollbar mt-4">
<TableContainer <TableContainer
@@ -1261,6 +1356,7 @@ export const OverviewPage = () => {
secretKey={key} secretKey={key}
getSecretByKey={getSecretByKey} getSecretByKey={getSecretByKey}
scrollOffset={debouncedScrollOffset} scrollOffset={debouncedScrollOffset}
importedBy={importedBy}
/> />
))} ))}
<SecretNoAccessOverviewTableRow <SecretNoAccessOverviewTableRow
@@ -4,6 +4,7 @@ import { subject } from "@casl/ability";
import { import {
faCheck, faCheck,
faCopy, faCopy,
faEyeSlash,
faProjectDiagram, faProjectDiagram,
faTrash, faTrash,
faXmark faXmark
@@ -25,13 +26,13 @@ import {
ModalTrigger, ModalTrigger,
Tooltip Tooltip
} from "@app/components/v2"; } from "@app/components/v2";
import { Blur } from "@app/components/v2/Blur";
import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput"; import { InfisicalSecretInput } from "@app/components/v2/InfisicalSecretInput";
import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context"; import { ProjectPermissionActions, ProjectPermissionSub, useProjectPermission } from "@app/context";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { SecretType } from "@app/hooks/api/types"; import { SecretType } from "@app/hooks/api/types";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { CollapsibleSecretImports } from "@app/pages/secret-manager/SecretDashboardPage/components/SecretListView/CollapsibleSecretImports";
type Props = { type Props = {
defaultValue?: string | null; defaultValue?: string | null;
@@ -54,6 +55,14 @@ type Props = {
) => Promise<void>; ) => Promise<void>;
onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>; onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
isRotatedSecret?: boolean; isRotatedSecret?: boolean;
importedBy?: {
environment: { name: string; slug: string };
folders: {
name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean;
}[];
}[];
}; };
export const SecretEditRow = ({ export const SecretEditRow = ({
@@ -70,8 +79,13 @@ export const SecretEditRow = ({
secretPath, secretPath,
isVisible, isVisible,
secretId, secretId,
isRotatedSecret isRotatedSecret,
importedBy
}: Props) => { }: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"editSecret"
] as const);
const { const {
handleSubmit, handleSubmit,
control, control,
@@ -115,6 +129,20 @@ export const SecretEditRow = ({
if (isCreatable) { if (isCreatable) {
await onSecretCreate(environment, secretName, value); await onSecretCreate(environment, secretName, value);
} else { } else {
if (
importedBy &&
importedBy.some(({ folders }) =>
folders?.some(({ secrets }) =>
secrets?.some(
({ referencedSecretKey, referencedSecretEnv }) =>
referencedSecretKey === secretName && referencedSecretEnv === environment
)
)
)
) {
handlePopUpOpen("editSecret", { secretValue: value });
return;
}
await onSecretUpdate( await onSecretUpdate(
environment, environment,
secretName, secretName,
@@ -124,7 +152,25 @@ export const SecretEditRow = ({
); );
} }
} }
reset({ value }); if (secretValueHidden && !isOverride) {
setTimeout(() => {
reset({ value: defaultValue || null });
}, 50);
} else {
reset({ value });
}
};
const handleEditSecret = async ({ secretValue }: { secretValue: string }) => {
await onSecretUpdate(
environment,
secretName,
secretValue,
isOverride ? SecretType.Personal : SecretType.Shared,
secretId
);
reset({ value: secretValue });
handlePopUpClose("editSecret");
}; };
const canReadSecretValue = hasSecretReadValueOrDescribePermission( const canReadSecretValue = hasSecretReadValueOrDescribePermission(
@@ -132,6 +178,16 @@ export const SecretEditRow = ({
ProjectPermissionSecretActions.ReadValue ProjectPermissionSecretActions.ReadValue
); );
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName,
secretTags: ["*"]
})
);
const handleDeleteSecret = useCallback(async () => { const handleDeleteSecret = useCallback(async () => {
setIsDeleting.on(); setIsDeleting.on();
setIsModalOpen(false); setIsModalOpen(false);
@@ -153,29 +209,32 @@ export const SecretEditRow = ({
deleteKey={secretName} deleteKey={secretName}
onDeleteApproved={handleDeleteSecret} onDeleteApproved={handleDeleteSecret}
/> />
{secretValueHidden && !isOverride && (
<Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
>
<FontAwesomeIcon className="pl-2" size="sm" icon={faEyeSlash} />
</Tooltip>
)}
<div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2"> <div className="flex-grow border-r border-r-mineshaft-600 pl-1 pr-2">
{secretValueHidden ? ( <Controller
<Blur tooltipText="You do not have permission to read the value of this secret." /> disabled={isImportedSecret && !defaultValue}
) : ( control={control}
<Controller name="value"
disabled={isImportedSecret && !defaultValue} render={({ field }) => (
control={control} <InfisicalSecretInput
name="value" {...field}
render={({ field }) => ( isReadOnly={isImportedSecret || (isRotatedSecret && !isOverride)}
<InfisicalSecretInput value={field.value as string}
{...field} key="secret-input"
isReadOnly={isImportedSecret || isRotatedSecret} isVisible={isVisible && !secretValueHidden}
value={field.value as string} secretPath={secretPath}
key="secret-input" environment={environment}
isVisible={isVisible} isImport={isImportedSecret}
secretPath={secretPath} defaultValue={secretValueHidden ? "" : undefined}
environment={environment} />
isImport={isImportedSecret} )}
/> />
)}
/>
)}
</div> </div>
<div <div
@@ -306,6 +365,26 @@ export const SecretEditRow = ({
</> </>
)} )}
</div> </div>
<DeleteActionModal
isOpen={popUp.editSecret.isOpen}
deleteKey="confirm"
buttonColorSchema="secondary"
buttonText="Save"
subTitle=""
title="Do you want to edit this secret?"
onChange={(isOpen) => handlePopUpToggle("editSecret", isOpen)}
onDeleteApproved={() => handleEditSecret(popUp?.editSecret?.data)}
formContent={
importedBy &&
importedBy.length > 0 && (
<CollapsibleSecretImports
importedBy={importedBy}
secretsToDelete={[secretName]}
onlyReferences
/>
)
}
/>
</div> </div>
); );
}; };
@@ -1,3 +1,4 @@
import { subject } from "@casl/ability";
import { faCircle } from "@fortawesome/free-regular-svg-icons"; import { faCircle } from "@fortawesome/free-regular-svg-icons";
import { import {
faAngleDown, faAngleDown,
@@ -14,6 +15,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2"; import { Button, Checkbox, TableContainer, Td, Tooltip, Tr } from "@app/components/v2";
import { useProjectPermission } from "@app/context";
import {
ProjectPermissionSecretActions,
ProjectPermissionSub
} from "@app/context/ProjectPermissionContext/types";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WorkspaceEnv } from "@app/hooks/api/types"; import { WorkspaceEnv } from "@app/hooks/api/types";
@@ -44,6 +50,14 @@ type Props = {
secretName: string secretName: string
) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined; ) => { secret?: SecretV3RawSanitized; environmentInfo?: WorkspaceEnv } | undefined;
scrollOffset: number; scrollOffset: number;
importedBy?: {
environment: { name: string; slug: string };
folders: {
name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean;
}[];
}[];
}; };
export const SecretOverviewTableRow = ({ export const SecretOverviewTableRow = ({
@@ -58,12 +72,35 @@ export const SecretOverviewTableRow = ({
getImportedSecretByKey, getImportedSecretByKey,
scrollOffset, scrollOffset,
onToggleSecretSelect, onToggleSecretSelect,
isSelected isSelected,
importedBy
}: Props) => { }: Props) => {
const [isFormExpanded, setIsFormExpanded] = useToggle(); const [isFormExpanded, setIsFormExpanded] = useToggle();
const totalCols = environments.length + 1; // secret key row const totalCols = environments.length + 1; // secret key row
const [isSecretVisible, setIsSecretVisible] = useToggle(); const [isSecretVisible, setIsSecretVisible] = useToggle();
const { permission } = useProjectPermission();
const getDefaultValue = (
secret: SecretV3RawSanitized | undefined,
importedSecret: { secret?: SecretV3RawSanitized } | undefined
) => {
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: secret?.env || "",
secretPath: secret?.path || "",
secretName: secret?.key || "",
secretTags: ["*"]
})
);
if (secret?.secretValueHidden && !secret?.valueOverride) {
return canEditSecretValue ? "******" : "";
}
return secret?.valueOverride || secret?.value || importedSecret?.secret?.value || "";
};
return ( return (
<> <>
<Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group"> <Tr isHoverable isSelectable onClick={() => setIsFormExpanded.toggle()} className="group">
@@ -228,13 +265,7 @@ export const SecretOverviewTableRow = ({
isVisible={isSecretVisible} isVisible={isSecretVisible}
secretName={secretKey} secretName={secretKey}
secretValueHidden={secret?.secretValueHidden || false} secretValueHidden={secret?.secretValueHidden || false}
defaultValue={ defaultValue={getDefaultValue(secret, importedSecret)}
secret?.secretValueHidden
? ""
: secret?.valueOverride ||
secret?.value ||
importedSecret?.secret?.value
}
secretId={secret?.id} secretId={secret?.id}
isOverride={Boolean(secret?.valueOverride)} isOverride={Boolean(secret?.valueOverride)}
isImportedSecret={isImportedSecret} isImportedSecret={isImportedSecret}
@@ -244,6 +275,7 @@ export const SecretOverviewTableRow = ({
onSecretUpdate={onSecretUpdate} onSecretUpdate={onSecretUpdate}
environment={slug} environment={slug}
isRotatedSecret={secret?.isRotatedSecret} isRotatedSecret={secret?.isRotatedSecret}
importedBy={importedBy}
/> />
</td> </td>
</tr> </tr>
@@ -15,7 +15,7 @@ import {
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api"; import { useDeleteFolder, useDeleteSecretBatch } from "@app/hooks/api";
import { ProjectSecretsImportedBy } from "@app/hooks/api/dashboard/types"; import { ProjectSecretsImportedBy, UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { import {
SecretType, SecretType,
SecretV3RawSanitized, SecretV3RawSanitized,
@@ -37,14 +37,18 @@ type Props = {
[EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>; [EntryType.FOLDER]: Record<string, Record<string, TSecretFolder>>;
[EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>; [EntryType.SECRET]: Record<string, Record<string, SecretV3RawSanitized>>;
}; };
importedByEnvs?: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]; importedBy?: ProjectSecretsImportedBy[] | null;
usedBySecretSyncs?: UsedBySecretSyncs[];
secretsToDeleteKeys: string[];
}; };
export const SelectionPanel = ({ export const SelectionPanel = ({
secretPath, secretPath,
resetSelectedEntries, resetSelectedEntries,
selectedEntries, selectedEntries,
importedByEnvs importedBy,
secretsToDeleteKeys,
usedBySecretSyncs = []
}: Props) => { }: Props) => {
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
@@ -81,80 +85,11 @@ export const SelectionPanel = ({
) )
); );
const secretsToDeleteKeys = useMemo(() => { const usedBySecretSyncsFiltered = useMemo(() => {
return Object.values(selectedEntries.secret).flatMap((entries) => if (selectedKeysCount === 0 || usedBySecretSyncs.length === 0) return null;
Object.values(entries).map((secret) => secret.key) const envs = Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries));
); return usedBySecretSyncs.filter((syncItem) => envs.includes(syncItem.environment));
}, [selectedEntries]); }, [selectedEntries, usedBySecretSyncs, selectedKeysCount]);
const filterAndMergeEnvironments = (
envNames: string[],
envs: { environment: string; importedBy: ProjectSecretsImportedBy[] }[]
): ProjectSecretsImportedBy[] => {
const filteredEnvs = envs.filter((env) => envNames.includes(env.environment));
if (filteredEnvs.length === 0) return [];
const allImportedBy = filteredEnvs.flatMap((env) => env.importedBy);
const groupedBySlug: Record<string, ProjectSecretsImportedBy[]> = {};
allImportedBy.forEach((item) => {
const { slug } = item.environment;
if (!groupedBySlug[slug]) groupedBySlug[slug] = [];
groupedBySlug[slug].push(item);
});
const mergedImportedBy = Object.values(groupedBySlug).map((group) => {
const { environment } = group[0];
const allFolders = group.flatMap((item) => item.folders);
const foldersByName: Record<string, (typeof allFolders)[number][]> = {};
allFolders.forEach((folder) => {
if (!foldersByName[folder.name]) foldersByName[folder.name] = [];
foldersByName[folder.name].push(folder);
});
const mergedFolders = Object.entries(foldersByName).map(([name, folders]) => {
const isImported = folders.some((folder) => folder.isImported);
const allSecrets = folders.flatMap((folder) => folder.secrets || []);
const uniqueSecrets: { secretId: string; referencedSecretKey: string }[] = [];
const secretIds = new Set<string>();
allSecrets
.filter((secret) => secretsToDeleteKeys.includes(secret.referencedSecretKey))
.forEach((secret) => {
if (!secretIds.has(secret.secretId)) {
secretIds.add(secret.secretId);
uniqueSecrets.push(secret);
}
});
return {
name,
isImported,
...(uniqueSecrets.length > 0 ? { secrets: uniqueSecrets } : {})
};
});
return {
environment,
folders: mergedFolders.filter(
(folder) => folder.isImported || (folder.secrets && folder.secrets.length > 0)
)
};
});
return mergedImportedBy;
};
const importedBy = useMemo(() => {
if (selectedKeysCount === 0 || !importedByEnvs) return null;
return filterAndMergeEnvironments(
Object.values(selectedEntries.secret).flatMap((entries) => Object.keys(entries)),
importedByEnvs
);
}, [importedByEnvs, selectedEntries, selectedKeysCount]);
const getDeleteModalTitle = () => { const getDeleteModalTitle = () => {
if (selectedFolderCount > 0 && selectedKeysCount > 0) { if (selectedFolderCount > 0 && selectedKeysCount > 0) {
@@ -326,11 +261,12 @@ export const SelectionPanel = ({
onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)} onChange={(isOpen) => handlePopUpToggle("bulkDeleteEntries", isOpen)}
onDeleteApproved={handleBulkDelete} onDeleteApproved={handleBulkDelete}
formContent={ formContent={
importedBy && ((usedBySecretSyncsFiltered && usedBySecretSyncsFiltered.length > 0) ||
importedBy.some((element) => element.folders.length > 0) && ( (importedBy && importedBy.some((element) => element.folders.length > 0))) && (
<CollapsibleSecretImports <CollapsibleSecretImports
importedBy={importedBy} importedBy={importedBy || []}
secretsToDelete={secretsToDeleteKeys} secretsToDelete={secretsToDeleteKeys}
usedBySecretSyncs={usedBySecretSyncsFiltered}
/> />
) )
} }
@@ -220,6 +220,7 @@ const Page = () => {
totalSecretCount = 0, totalSecretCount = 0,
totalCount = 0, totalCount = 0,
importedBy, importedBy,
usedBySecretSyncs,
totalSecretRotationCount = 0 totalSecretRotationCount = 0
} = data ?? {}; } = data ?? {};
@@ -441,6 +442,7 @@ const Page = () => {
onClickRollbackMode={() => handlePopUpToggle("snapshots", true)} onClickRollbackMode={() => handlePopUpToggle("snapshots", true)}
protectedBranchPolicyName={boardPolicy?.name} protectedBranchPolicyName={boardPolicy?.name}
importedBy={importedBy} importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
/> />
<div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300"> <div className="thin-scrollbar mt-3 overflow-y-auto overflow-x-hidden rounded-md rounded-b-none bg-mineshaft-800 text-left text-sm text-bunker-300">
<div className="flex flex-col" id="dashboard"> <div className="flex flex-col" id="dashboard">
@@ -530,6 +532,7 @@ const Page = () => {
secretPath={secretPath} secretPath={secretPath}
isProtectedBranch={isProtectedBranch} isProtectedBranch={isProtectedBranch}
importedBy={importedBy} importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
/> />
)} )}
{noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />} {noAccessSecretCount > 0 && <SecretNoAccessListView count={noAccessSecretCount} />}
@@ -69,6 +69,7 @@ import {
dashboardKeys, dashboardKeys,
fetchDashboardProjectSecretsByKeys fetchDashboardProjectSecretsByKeys
} from "@app/hooks/api/dashboard/queries"; } from "@app/hooks/api/dashboard/queries";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries"; import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types"; import { ApiErrorTypes, SecretType, TApiErrors, WsTag } from "@app/hooks/api/types";
@@ -113,11 +114,12 @@ type Props = {
onVisibilityToggle: () => void; onVisibilityToggle: () => void;
onToggleRowType: (rowType: RowType) => void; onToggleRowType: (rowType: RowType) => void;
onClickRollbackMode: () => void; onClickRollbackMode: () => void;
usedBySecretSyncs?: UsedBySecretSyncs[];
importedBy?: { importedBy?: {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
@@ -139,7 +141,8 @@ export const ActionBar = ({
onClickRollbackMode, onClickRollbackMode,
onToggleRowType, onToggleRowType,
protectedBranchPolicyName, protectedBranchPolicyName,
importedBy importedBy,
usedBySecretSyncs
}: Props) => { }: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([ const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"addFolder", "addFolder",
@@ -1071,11 +1074,12 @@ export const ActionBar = ({
onChange={(isOpen) => handlePopUpToggle("bulkDeleteSecrets", isOpen)} onChange={(isOpen) => handlePopUpToggle("bulkDeleteSecrets", isOpen)}
onDeleteApproved={handleSecretBulkDelete} onDeleteApproved={handleSecretBulkDelete}
formContent={ formContent={
importedBy && ((importedBy && importedBy.length > 0) ||
importedBy.length > 0 && ( (usedBySecretSyncs && usedBySecretSyncs?.length > 0)) && (
<CollapsibleSecretImports <CollapsibleSecretImports
importedBy={importedBy} importedBy={importedBy}
secretsToDelete={Object.values(selectedSecrets).map((s) => s.key)} secretsToDelete={Object.values(selectedSecrets).map((s) => s.key)}
usedBySecretSyncs={usedBySecretSyncs}
/> />
) )
} }
@@ -1,13 +1,16 @@
/* eslint-disable no-nested-ternary */
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { faFileImport, faKey, faWarning } from "@fortawesome/free-solid-svg-icons"; import { faFileImport, faKey, faSync, faWarning } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Table, TBody, Td, Th, THead, Tr } from "@app/components/v2"; import { Table, TBody, Td, Th, THead, Tooltip, Tr } from "@app/components/v2";
import { useWorkspace } from "@app/context"; import { useWorkspace } from "@app/context";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
enum ItemType { enum ItemType {
Folder = "Folder", Folder = "Folder",
Secret = "Secret" Secret = "Secret",
SecretSync = "SecretSync"
} }
interface FlatItem { interface FlatItem {
@@ -17,6 +20,8 @@ interface FlatItem {
reference: string; reference: string;
id: string; id: string;
environment: { name: string; slug: string }; environment: { name: string; slug: string };
tooltipText?: string;
destination?: string;
} }
interface CollapsibleSecretImportsProps { interface CollapsibleSecretImportsProps {
@@ -24,16 +29,20 @@ interface CollapsibleSecretImportsProps {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
usedBySecretSyncs?: UsedBySecretSyncs[] | null;
secretsToDelete: string[]; secretsToDelete: string[];
onlyReferences?: boolean;
} }
export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> = ({
importedBy = [], importedBy = [],
secretsToDelete usedBySecretSyncs = [],
secretsToDelete,
onlyReferences
}) => { }) => {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
@@ -51,6 +60,15 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
}; };
const handlePathClick = (item: FlatItem) => { const handlePathClick = (item: FlatItem) => {
if (item.type === ItemType.SecretSync) {
window.open(
`/secret-manager/${currentWorkspace.id}/integrations/secret-syncs/${item.destination}/${item.id}`,
"_blank",
"noopener,noreferrer"
);
return;
}
let pathToNavigate; let pathToNavigate;
if (item.type === ItemType.Folder) { if (item.type === ItemType.Folder) {
pathToNavigate = item.path; pathToNavigate = item.path;
@@ -70,7 +88,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
importedBy.forEach((env) => { importedBy.forEach((env) => {
env.folders.forEach((folder) => { env.folders.forEach((folder) => {
if (folder.isImported) { if (folder.isImported && !onlyReferences) {
items.push({ items.push({
type: ItemType.Folder, type: ItemType.Folder,
path: folder.name, path: folder.name,
@@ -103,7 +121,26 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
}); });
}); });
// Add secret sync items
usedBySecretSyncs?.forEach((syncItem) => {
items.push({
type: ItemType.SecretSync,
destination: syncItem.destination,
path: syncItem.path,
id: syncItem.id,
reference: "Secret Sync",
environment: { name: syncItem.environment, slug: "" },
tooltipText: `Currently used by Secret Sync: ${syncItem.name}`
});
});
return items.sort((a, b) => { return items.sort((a, b) => {
if (a.type === ItemType.SecretSync && b.type !== ItemType.SecretSync) return 1;
if (a.type !== ItemType.SecretSync && b.type === ItemType.SecretSync) return -1;
if (a.type === ItemType.SecretSync && b.type === ItemType.SecretSync) {
return a.path.localeCompare(b.path);
}
const envCompare = a.environment.name.localeCompare(b.environment.name); const envCompare = a.environment.name.localeCompare(b.environment.name);
if (envCompare !== 0) return envCompare; if (envCompare !== 0) return envCompare;
@@ -119,7 +156,7 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
return aPath.localeCompare(bPath); return aPath.localeCompare(bPath);
}); });
}, [importedBy]); }, [importedBy, usedBySecretSyncs, secretsToDelete, onlyReferences]);
const hasImportedItems = importedBy.some((element) => { const hasImportedItems = importedBy.some((element) => {
if (element.folders && element.folders.length > 0) { if (element.folders && element.folders.length > 0) {
@@ -135,19 +172,33 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
return false; return false;
}); });
if (!hasImportedItems) { const hasSecretSyncItems = usedBySecretSyncs && usedBySecretSyncs.length > 0;
if (!hasImportedItems && !hasSecretSyncItems) {
return null; return null;
} }
const alertColors = onlyReferences
? {
border: "border-yellow-700/30",
bg: "bg-yellow-900/20",
text: "text-yellow-500"
}
: {
border: "border-red-700/30",
bg: "bg-red-900/20",
text: "text-red-500"
};
return ( return (
<div className="mb-4 w-full"> <div className="mb-4 w-full">
<div className="mb-4 rounded-md border border-red-700/30 bg-red-900/20"> <div className={`mb-4 rounded-md border ${alertColors.border} ${alertColors.bg}`}>
<div className="flex items-start gap-3 p-4"> <div className="flex items-start gap-3 p-4">
<div className="mt-0.5 flex-shrink-0 text-red-500"> <div className={`mt-0.5 flex-shrink-0 ${alertColors.text}`}>
<FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" /> <FontAwesomeIcon icon={faWarning} className="h-5 w-5" aria-hidden="true" />
</div> </div>
<div className="w-full"> <div className="w-full">
<p className="text-sm font-semibold text-red-500"> <p className={`text-sm font-semibold ${alertColors.text}`}>
The following resources will be affected by this change The following resources will be affected by this change
</p> </p>
</div> </div>
@@ -168,14 +219,36 @@ export const CollapsibleSecretImports: React.FC<CollapsibleSecretImportsProps> =
key={item.id} key={item.id}
onClick={() => handlePathClick(item)} onClick={() => handlePathClick(item)}
className="cursor-pointer hover:bg-mineshaft-700" className="cursor-pointer hover:bg-mineshaft-700"
title={`Navigate to ${item.path}`} title={
item.type === ItemType.SecretSync
? "Navigate to Secret Sync"
: `Navigate to ${item.path}`
}
> >
<Td> <Td>
<FontAwesomeIcon <Tooltip
icon={item.type === ItemType.Secret ? faKey : faFileImport} className="max-w-md"
className={`h-4 w-4 ${item.type === ItemType.Secret ? "text-gray-400" : "text-green-700"}`} content={item.tooltipText}
aria-hidden="true" isDisabled={!item.tooltipText}
/> >
<FontAwesomeIcon
icon={
item.type === ItemType.Secret
? faKey
: item.type === ItemType.Folder
? faFileImport
: faSync
}
className={`h-4 w-4 ${
item.type === ItemType.Secret
? "text-gray-400"
: item.type === ItemType.Folder
? "text-green-700"
: "text-mineshaft-300"
}`}
aria-hidden="true"
/>
</Tooltip>
</Td> </Td>
<Td className="px-4">{item.environment.name}</Td> <Td className="px-4">{item.environment.name}</Td>
<Td className="truncate px-4">{truncatePath(item.path)}</Td> <Td className="truncate px-4">{truncatePath(item.path)}</Td>
@@ -4,6 +4,7 @@ import { ProjectPermissionCan } from "@app/components/permissions";
import { import {
Button, Button,
Checkbox, Checkbox,
DeleteActionModal,
DropdownMenu, DropdownMenu,
DropdownMenuContent, DropdownMenuContent,
DropdownMenuItem, DropdownMenuItem,
@@ -31,7 +32,7 @@ import {
useProjectPermission, useProjectPermission,
useWorkspace useWorkspace
} from "@app/context"; } from "@app/context";
import { useToggle } from "@app/hooks"; import { usePopUp, useToggle } from "@app/hooks";
import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
import { WsTag } from "@app/hooks/api/types"; import { WsTag } from "@app/hooks/api/types";
import { subject } from "@casl/ability"; import { subject } from "@casl/ability";
@@ -46,16 +47,18 @@ import {
} from "@app/components/secrets/SecretReferenceDetails"; } from "@app/components/secrets/SecretReferenceDetails";
import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types"; import { ProjectPermissionSecretActions } from "@app/context/ProjectPermissionContext/types";
import { Blur } from "@app/components/v2/Blur";
import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission"; import { hasSecretReadValueOrDescribePermission } from "@app/lib/fn/permission";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faKey, faRotate } from "@fortawesome/free-solid-svg-icons"; import { faEyeSlash, faKey, faRotate } from "@fortawesome/free-solid-svg-icons";
import { import {
FontAwesomeSpriteName, FontAwesomeSpriteName,
formSchema, formSchema,
SecretActionType, SecretActionType,
TFormSchema TFormSchema
} from "./SecretListView.utils"; } from "./SecretListView.utils";
import { CollapsibleSecretImports } from "./CollapsibleSecretImports";
const hiddenValue = "******";
type Props = { type Props = {
secret: SecretV3RawSanitized; secret: SecretV3RawSanitized;
@@ -74,6 +77,14 @@ type Props = {
environment: string; environment: string;
secretPath: string; secretPath: string;
handleSecretShare: () => void; handleSecretShare: () => void;
importedBy?: {
environment: { name: string; slug: string };
folders: {
name: string;
secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean;
}[];
}[];
}; };
export const SecretItem = memo( export const SecretItem = memo(
@@ -89,12 +100,33 @@ export const SecretItem = memo(
onToggleSecretSelect, onToggleSecretSelect,
environment, environment,
secretPath, secretPath,
handleSecretShare handleSecretShare,
importedBy
}: Props) => { }: Props) => {
const { handlePopUpOpen, handlePopUpToggle, handlePopUpClose, popUp } = usePopUp([
"editSecret"
] as const);
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { permission } = useProjectPermission(); const { permission } = useProjectPermission();
const { isRotatedSecret } = secret; const { isRotatedSecret } = secret;
const canEditSecretValue = permission.can(
ProjectPermissionSecretActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath,
secretName: secret.key,
secretTags: ["*"]
})
);
const getDefaultValue = () => {
if (secret.secretValueHidden) {
return canEditSecretValue ? hiddenValue : "";
}
return secret.valueOverride || secret.value || "";
};
const { const {
handleSubmit, handleSubmit,
control, control,
@@ -108,11 +140,11 @@ export const SecretItem = memo(
} = useForm<TFormSchema>({ } = useForm<TFormSchema>({
defaultValues: { defaultValues: {
...secret, ...secret,
value: secret.secretValueHidden ? "" : secret.value value: getDefaultValue()
}, },
values: { values: {
...secret, ...secret,
value: secret.secretValueHidden ? "" : secret.value value: getDefaultValue()
}, },
resolver: zodResolver(formSchema) resolver: zodResolver(formSchema)
}); });
@@ -154,6 +186,7 @@ export const SecretItem = memo(
secretTags: selectedTagSlugs secretTags: selectedTagSlugs
}) })
); );
const { secretValueHidden } = secret; const { secretValueHidden } = secret;
const [isSecValueCopied, setIsSecValueCopied] = useToggle(false); const [isSecValueCopied, setIsSecValueCopied] = useToggle(false);
@@ -194,9 +227,24 @@ export const SecretItem = memo(
}; };
const handleFormSubmit = async (data: TFormSchema) => { const handleFormSubmit = async (data: TFormSchema) => {
const hasDirectReferences = importedBy?.some(({ folders }) =>
folders?.some(({ secrets }) =>
secrets?.some(({ referencedSecretKey }) => referencedSecretKey === secret.key)
)
);
if (hasDirectReferences) {
handlePopUpOpen("editSecret", data);
return;
}
await onSaveSecret(secret, { ...secret, ...data }, () => reset()); await onSaveSecret(secret, { ...secret, ...data }, () => reset());
}; };
const handleEditSecret = async (data: TFormSchema) => {
await onSaveSecret(secret, { ...secret, ...data }, () => reset());
handlePopUpClose("editSecret");
};
const handleTagSelect = (tag: WsTag) => { const handleTagSelect = (tag: WsTag) => {
if (selectedTagsGroupById?.[tag.id]) { if (selectedTagsGroupById?.[tag.id]) {
const tagPos = selectedTags.findIndex(({ id }) => id === tag.id); const tagPos = selectedTags.findIndex(({ id }) => id === tag.id);
@@ -286,6 +334,13 @@ export const SecretItem = memo(
tabIndex={0} tabIndex={0}
role="button" role="button"
> >
{secretValueHidden && !isOverriden && (
<Tooltip
content={`You do not have access to view the current value${canEditSecretValue && !isRotatedSecret ? ", but you can set a new one" : "."}`}
>
<FontAwesomeIcon className="pr-2" size="sm" icon={faEyeSlash} />
</Tooltip>
)}
{isOverriden ? ( {isOverriden ? (
<Controller <Controller
name="valueOverride" name="valueOverride"
@@ -301,8 +356,6 @@ export const SecretItem = memo(
/> />
)} )}
/> />
) : secretValueHidden ? (
<Blur tooltipText="You do not have permission to read the value of this secret." />
) : ( ) : (
<Controller <Controller
name="value" name="value"
@@ -312,11 +365,11 @@ export const SecretItem = memo(
<InfisicalSecretInput <InfisicalSecretInput
isReadOnly={isReadOnly || isRotatedSecret} isReadOnly={isReadOnly || isRotatedSecret}
key="secret-value" key="secret-value"
isVisible={isVisible} isVisible={isVisible && !secretValueHidden}
environment={environment} environment={environment}
secretPath={secretPath} secretPath={secretPath}
{...field} {...field}
defaultValue={secretValueHidden ? "" : undefined} defaultValue={secretValueHidden ? hiddenValue : undefined}
containerClassName="py-1.5 rounded-md transition-all" containerClassName="py-1.5 rounded-md transition-all"
/> />
)} )}
@@ -682,6 +735,26 @@ export const SecretItem = memo(
</AnimatePresence> </AnimatePresence>
</div> </div>
</div> </div>
<DeleteActionModal
isOpen={popUp.editSecret.isOpen}
deleteKey="confirm"
buttonColorSchema="secondary"
buttonText="Save"
subTitle=""
title="Do you want to edit this secret?"
onChange={(isOpen) => handlePopUpToggle("editSecret", isOpen)}
onDeleteApproved={() => handleEditSecret(popUp?.editSecret?.data)}
formContent={
importedBy &&
importedBy.length > 0 && (
<CollapsibleSecretImports
importedBy={importedBy}
secretsToDelete={[secret.key]}
onlyReferences
/>
)
}
/>
</form> </form>
); );
} }
@@ -8,6 +8,7 @@ import { DeleteActionModal } from "@app/components/v2";
import { usePopUp } from "@app/hooks"; import { usePopUp } from "@app/hooks";
import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api"; import { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "@app/hooks/api";
import { dashboardKeys } from "@app/hooks/api/dashboard/queries"; import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
import { secretKeys } from "@app/hooks/api/secrets/queries"; import { secretKeys } from "@app/hooks/api/secrets/queries";
import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types"; import { SecretType, SecretV3RawSanitized } from "@app/hooks/api/secrets/types";
@@ -29,11 +30,12 @@ type Props = {
tags?: WsTag[]; tags?: WsTag[];
isVisible?: boolean; isVisible?: boolean;
isProtectedBranch?: boolean; isProtectedBranch?: boolean;
usedBySecretSyncs?: UsedBySecretSyncs[];
importedBy?: { importedBy?: {
environment: { name: string; slug: string }; environment: { name: string; slug: string };
folders: { folders: {
name: string; name: string;
secrets?: { secretId: string; referencedSecretKey: string }[]; secrets?: { secretId: string; referencedSecretKey: string; referencedSecretEnv: string }[];
isImported: boolean; isImported: boolean;
}[]; }[];
}[]; }[];
@@ -47,10 +49,9 @@ export const SecretListView = ({
tags: wsTags = [], tags: wsTags = [],
isVisible, isVisible,
isProtectedBranch = false, isProtectedBranch = false,
usedBySecretSyncs,
importedBy importedBy
}: Props) => { }: Props) => {
console.log("secretssssss", secrets);
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([ const { popUp, handlePopUpToggle, handlePopUpOpen, handlePopUpClose } = usePopUp([
"deleteSecret", "deleteSecret",
@@ -368,6 +369,7 @@ export const SecretListView = ({
onSaveSecret={handleSaveSecret} onSaveSecret={handleSaveSecret}
onDeleteSecret={onDeleteSecret} onDeleteSecret={onDeleteSecret}
onDetailViewSecret={onDetailViewSecret} onDetailViewSecret={onDetailViewSecret}
importedBy={importedBy}
onCreateTag={onCreateTag} onCreateTag={onCreateTag}
handleSecretShare={() => handleSecretShare={() =>
handlePopUpOpen("createSharedSecret", { handlePopUpOpen("createSharedSecret", {
@@ -384,10 +386,11 @@ export const SecretListView = ({
onDeleteApproved={handleSecretDelete} onDeleteApproved={handleSecretDelete}
buttonText="Delete Secret" buttonText="Delete Secret"
formContent={ formContent={
importedBy && ((importedBy && importedBy.length > 0) ||
importedBy.length > 0 && ( (usedBySecretSyncs && usedBySecretSyncs?.length > 0)) && (
<CollapsibleSecretImports <CollapsibleSecretImports
importedBy={importedBy} importedBy={importedBy}
usedBySecretSyncs={usedBySecretSyncs}
secretsToDelete={[(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key || ""]} secretsToDelete={[(popUp.deleteSecret?.data as SecretV3RawSanitized)?.key || ""]}
/> />
) )