Compare commits

...

1 Commits

Author SHA1 Message Date
066bcdcf98 Scaffolding LDAP MVP 2024-01-13 21:21:40 +07:00
34 changed files with 1497 additions and 26 deletions

View File

@ -56,6 +56,7 @@
"passport-github": "^1.1.0", "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",
"pg": "^8.11.3", "pg": "^8.11.3",
"pino": "^8.16.1", "pino": "^8.16.1",
"pino-http": "^8.5.1", "pino-http": "^8.5.1",
@ -7413,6 +7414,14 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/ldapjs": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz",
"integrity": "sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/libsodium-wrappers": { "node_modules/@types/libsodium-wrappers": {
"version": "0.7.10", "version": "0.7.10",
"resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz",
@ -7965,6 +7974,11 @@
"node": ">=6.5" "node": ">=6.5"
} }
}, },
"node_modules/abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="
},
"node_modules/accepts": { "node_modules/accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -8263,11 +8277,18 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true "dev": true
}, },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assert-plus": { "node_modules/assert-plus": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==",
"dev": true,
"engines": { "engines": {
"node": ">=0.8" "node": ">=0.8"
} }
@ -8434,6 +8455,17 @@
"@babel/core": "^7.0.0" "@babel/core": "^7.0.0"
} }
}, },
"node_modules/backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==",
"dependencies": {
"precond": "0.2"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/balanced-match": { "node_modules/balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -8497,6 +8529,11 @@
"node": ">= 10.0.0" "node": ">= 10.0.0"
} }
}, },
"node_modules/bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"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": "2.2.3",
"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-2.2.3.tgz",
@ -9120,8 +9157,7 @@
"node_modules/core-util-is": { "node_modules/core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
"dev": true
}, },
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.5", "version": "2.8.5",
@ -9976,7 +10012,6 @@
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==",
"dev": true,
"engines": [ "engines": [
"node >=0.6.0" "node >=0.6.0"
] ]
@ -11888,6 +11923,57 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/ldap-filter": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
"integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==",
"dependencies": {
"assert-plus": "^1.0.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/ldapauth-fork": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.5.tgz",
"integrity": "sha512-LWUk76+V4AOZbny/3HIPQtGPWZyA3SW2tRhsWIBi9imP22WJktKLHV1ofd8Jo/wY7Ve6vAT7FCI5mEn3blZTjw==",
"dependencies": {
"@types/ldapjs": "^2.2.2",
"bcryptjs": "^2.4.0",
"ldapjs": "^2.2.1",
"lru-cache": "^7.10.1"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/ldapauth-fork/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"engines": {
"node": ">=12"
}
},
"node_modules/ldapjs": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
"dependencies": {
"abstract-logging": "^2.0.0",
"asn1": "^0.2.4",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"ldap-filter": "^0.3.3",
"once": "^1.4.0",
"vasync": "^2.2.0",
"verror": "^1.8.1"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/leven": { "node_modules/leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -15657,6 +15743,18 @@
"node": ">= 0.4.0" "node": ">= 0.4.0"
} }
}, },
"node_modules/passport-ldapauth": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-3.0.1.tgz",
"integrity": "sha512-TRRx3BHi8GC8MfCT9wmghjde/EGeKjll7zqHRRfGRxXbLcaDce2OftbQrFG7/AWaeFhR6zpZHtBQ/IkINdLVjQ==",
"dependencies": {
"ldapauth-fork": "^5.0.1",
"passport-strategy": "^1.0.0"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/passport-oauth2": { "node_modules/passport-oauth2": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
@ -16270,6 +16368,14 @@
"form-data": "^4.0.0" "form-data": "^4.0.0"
} }
}, },
"node_modules/precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/prelude-ls": { "node_modules/prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -18354,11 +18460,21 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/vasync": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz",
"integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
"engines": [
"node >=0.6.0"
],
"dependencies": {
"verror": "1.10.0"
}
},
"node_modules/verror": { "node_modules/verror": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"dev": true,
"engines": [ "engines": [
"node >=0.6.0" "node >=0.6.0"
], ],
@ -24541,6 +24657,14 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"@types/ldapjs": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.5.tgz",
"integrity": "sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==",
"requires": {
"@types/node": "*"
}
},
"@types/libsodium-wrappers": { "@types/libsodium-wrappers": {
"version": "0.7.10", "version": "0.7.10",
"resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz", "resolved": "https://registry.npmjs.org/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz",
@ -24984,6 +25108,11 @@
"event-target-shim": "^5.0.0" "event-target-shim": "^5.0.0"
} }
}, },
"abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
"integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA=="
},
"accepts": { "accepts": {
"version": "1.3.8", "version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@ -25204,11 +25333,18 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true "dev": true
}, },
"asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"requires": {
"safer-buffer": "~2.1.0"
}
},
"assert-plus": { "assert-plus": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
"dev": true
}, },
"asynckit": { "asynckit": {
"version": "0.4.0", "version": "0.4.0",
@ -25338,6 +25474,14 @@
"babel-preset-current-node-syntax": "^1.0.0" "babel-preset-current-node-syntax": "^1.0.0"
} }
}, },
"backoff": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/backoff/-/backoff-2.5.0.tgz",
"integrity": "sha512-wC5ihrnUXmR2douXmXLCe5O3zg3GKIyvRi/hi58a/XyRxVI+3/yM0PYueQOZXPXQ9pxBislYkw+sF9b7C/RuMA==",
"requires": {
"precond": "0.2"
}
},
"balanced-match": { "balanced-match": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@ -25379,6 +25523,11 @@
"node-addon-api": "^5.0.0" "node-addon-api": "^5.0.0"
} }
}, },
"bcryptjs": {
"version": "2.4.3",
"resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz",
"integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ=="
},
"before-after-hook": { "before-after-hook": {
"version": "2.2.3", "version": "2.2.3",
"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-2.2.3.tgz",
@ -25832,8 +25981,7 @@
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
"dev": true
}, },
"cors": { "cors": {
"version": "2.8.5", "version": "2.8.5",
@ -26470,8 +26618,7 @@
"extsprintf": { "extsprintf": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="
"dev": true
}, },
"fast-copy": { "fast-copy": {
"version": "3.0.1", "version": "3.0.1",
@ -27897,6 +28044,47 @@
"integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==",
"dev": true "dev": true
}, },
"ldap-filter": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz",
"integrity": "sha512-/tFkx5WIn4HuO+6w9lsfxq4FN3O+fDZeO9Mek8dCD8rTUpqzRa766BOBO7BcGkn3X86m5+cBm1/2S/Shzz7gMg==",
"requires": {
"assert-plus": "^1.0.0"
}
},
"ldapauth-fork": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.5.tgz",
"integrity": "sha512-LWUk76+V4AOZbny/3HIPQtGPWZyA3SW2tRhsWIBi9imP22WJktKLHV1ofd8Jo/wY7Ve6vAT7FCI5mEn3blZTjw==",
"requires": {
"@types/ldapjs": "^2.2.2",
"bcryptjs": "^2.4.0",
"ldapjs": "^2.2.1",
"lru-cache": "^7.10.1"
},
"dependencies": {
"lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="
}
}
},
"ldapjs": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.3.tgz",
"integrity": "sha512-75QiiLJV/PQqtpH+HGls44dXweviFwQ6SiIK27EqzKQ5jU/7UFrl2E5nLdQ3IYRBzJ/AVFJI66u0MZ0uofKYwg==",
"requires": {
"abstract-logging": "^2.0.0",
"asn1": "^0.2.4",
"assert-plus": "^1.0.0",
"backoff": "^2.5.0",
"ldap-filter": "^0.3.3",
"once": "^1.4.0",
"vasync": "^2.2.0",
"verror": "^1.8.1"
}
},
"leven": { "leven": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
@ -30574,6 +30762,15 @@
"passport-oauth2": "1.x.x" "passport-oauth2": "1.x.x"
} }
}, },
"passport-ldapauth": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/passport-ldapauth/-/passport-ldapauth-3.0.1.tgz",
"integrity": "sha512-TRRx3BHi8GC8MfCT9wmghjde/EGeKjll7zqHRRfGRxXbLcaDce2OftbQrFG7/AWaeFhR6zpZHtBQ/IkINdLVjQ==",
"requires": {
"ldapauth-fork": "^5.0.1",
"passport-strategy": "^1.0.0"
}
},
"passport-oauth2": { "passport-oauth2": {
"version": "1.7.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz", "resolved": "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.7.0.tgz",
@ -31013,6 +31210,11 @@
} }
} }
}, },
"precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha512-QCYG84SgGyGzqJ/vlMsxeXd/pgL/I94ixdNFyh1PusWmTCyVfPJjZ1K1jvHtsbfnXQs2TSkEP2fR7QiMZAnKFQ=="
},
"prelude-ls": { "prelude-ls": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@ -32635,11 +32837,18 @@
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="
}, },
"vasync": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz",
"integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==",
"requires": {
"verror": "1.10.0"
}
},
"verror": { "verror": {
"version": "1.10.0", "version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"dev": true,
"requires": { "requires": {
"assert-plus": "^1.0.0", "assert-plus": "^1.0.0",
"core-util-is": "1.0.2", "core-util-is": "1.0.2",

View File

@ -47,6 +47,7 @@
"passport-github": "^1.1.0", "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",
"pg": "^8.11.3", "pg": "^8.11.3",
"pino": "^8.16.1", "pino": "^8.16.1",
"pino-http": "^8.5.1", "pino-http": "^8.5.1",

View File

@ -1,5 +1,6 @@
import * as authController from "./authController"; import * as authController from "./authController";
import * as universalAuthController from "./universalAuthController"; import * as universalAuthController from "./universalAuthController";
import * as ldapController from "./ldapController";
import * as botController from "./botController"; import * as botController from "./botController";
import * as integrationAuthController from "./integrationAuthController"; import * as integrationAuthController from "./integrationAuthController";
import * as integrationController from "./integrationController"; import * as integrationController from "./integrationController";
@ -22,6 +23,7 @@ import * as adminController from "./adminController";
export { export {
authController, authController,
universalAuthController, universalAuthController,
ldapController,
botController, botController,
integrationAuthController, integrationAuthController,
integrationController, integrationController,

View File

@ -0,0 +1,233 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getSiteURL } from "../../config";
import * as reqValidator from "../../validation/ldap";
import { validateRequest } from "../../helpers/validation";
import { getLdapConfigHelper } from "../../ee/helpers/organizations";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getAuthDataOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { LDAPConfig } from "../../ee/models";
import { BotOrgService } from "../../services";
/**
* Return appropriate SSO endpoint after successful authentication with LDAP
* to finish inputting their master key for logging in or signing up
* @param req
* @param res
* @returns
*/
export const redirectLDAP = async (req: Request, res: Response) => {
let nextUrl;
if (req.isUserCompleted) {
nextUrl = `${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`;
} else {
nextUrl = `${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`
}
return res.status(200).send({
nextUrl
});
}
/**
* Return organization LDAP configuration
* @param req
* @param res
*/
export const getLDAPConfig = async (req: Request, res: Response) => {
const {
query: { organizationId }
} = await validateRequest(reqValidator.GetLdapConfigv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Sso
);
const data = await getLdapConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send(data);
}
/**
* Update organization LDAP configuration
* @param req
* @param res
* @returns
*/
export const updateLDAPConfig = async (req: Request, res: Response) => {
const {
body: {
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
} = await validateRequest(reqValidator.UpdateLdapConfigv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Sso
);
interface PatchUpdate {
isActive?: boolean;
url?: string;
encryptedBindDN?: string;
bindDNIV?: string;
bindDNTag?: string;
encryptedBindPass?: string;
bindPassIV?: string;
bindPassTag?: string;
searchBase?: string;
encryptedCACert?: string;
caCertIV?: string;
caCertTag?: string;
}
const update: PatchUpdate = {};
if (url) {
update.url = url;
}
if (searchBase) {
update.searchBase = searchBase;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
if (bindDN) {
const {
ciphertext: encryptedBindDN,
iv: bindDNIV,
tag: bindDNTag
} = client.encryptSymmetric(bindDN, key);
update.encryptedBindDN = encryptedBindDN;
update.bindDNIV = bindDNIV;
update.bindDNTag = bindDNTag;
}
if (bindPass) {
const {
ciphertext: encryptedBindPass,
iv: bindPassIV,
tag: bindPassTag
} = client.encryptSymmetric(bindPass, key);
update.encryptedBindPass = encryptedBindPass;
update.bindPassIV = bindPassIV;
update.bindPassTag = bindPassTag;
}
if (caCert) {
const {
ciphertext: encryptedCACert,
iv: caCertIV,
tag: caCertTag
} = client.encryptSymmetric(caCert, key);
update.encryptedCACert = encryptedCACert;
update.caCertIV = caCertIV;
update.caCertTag = caCertTag;
}
const ldapConfig = await LDAPConfig.findOneAndUpdate(
{ organization: new Types.ObjectId(organizationId) },
update,
{ new: true }
);
return res.status(200).send(ldapConfig);
}
/**
* Create organization LDAP configuration
* @param req
* @param res
*/
export const createLDAPConfig = async (req: Request, res: Response) => {
const {
body: {
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
} = await validateRequest(reqValidator.CreateLdapConfigv1, req);
const { permission } = await getAuthDataOrgPermissions({
authData: req.authData,
organizationId: new Types.ObjectId(organizationId)
});
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Sso
);
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
const {
ciphertext: encryptedBindDN,
iv: bindDNIV,
tag: bindDNTag
} = client.encryptSymmetric(bindDN, key);
const {
ciphertext: encryptedBindPass,
iv: bindPassIV,
tag: bindPassTag
} = client.encryptSymmetric(bindPass, key);
const {
ciphertext: encryptedCACert,
iv: caCertIV,
tag: caCertTag
} = client.encryptSymmetric(caCert, key);
const ldapConfig = await new LDAPConfig({
organization: new Types.ObjectId(organizationId),
isActive,
url,
encryptedBindDN,
bindDNIV,
bindDNTag,
encryptedBindPass,
bindPassIV,
bindPassTag,
searchBase,
encryptedCACert,
caCertIV,
caCertTag
}).save();
return res.status(200).send(ldapConfig);
}

View File

@ -104,11 +104,12 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) => // this might need to consider LDAP
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod) const hasOrgAuthMethodEnabled = user.authMethods.some((authMethod: AuthMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML, AuthMethod.LDAP].includes(authMethod)
); );
if (!hasSamlEnabled) { if (!hasOrgAuthMethodEnabled) {
// TODO: modify this part // TODO: modify this part
// initialize default organization and workspace // initialize default organization and workspace
await initializeDefaultOrg({ await initializeDefaultOrg({

View File

@ -1,5 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
LDAPConfig,
SSOConfig SSOConfig
} from "../models"; } from "../models";
import { import {
@ -62,3 +63,52 @@ export const getSSOConfigHelper = async ({
cert cert
}); });
} }
export const getLdapConfigHelper = async ({
organizationId
}: {
organizationId: Types.ObjectId;
}) => {
const ldapConfig = await LDAPConfig.findOne({
organization: organizationId
});
if (!ldapConfig) throw new Error("Failed to find organization LDAP data");
const key = await BotOrgService.getSymmetricKey(
ldapConfig.organization
);
const bindDN = client.decryptSymmetric(
ldapConfig.encryptedBindDN,
key,
ldapConfig.bindDNIV,
ldapConfig.bindDNTag
);
const bindPass = client.decryptSymmetric(
ldapConfig.encryptedBindPass,
key,
ldapConfig.bindPassIV,
ldapConfig.bindPassTag
);
const caCert = client.decryptSymmetric(
ldapConfig.encryptedCACert,
key,
ldapConfig.caCertIV,
ldapConfig.caCertTag
);
return ({
_id: ldapConfig._id,
organization: ldapConfig.organization,
isActive: ldapConfig.isActive,
url: ldapConfig.url,
bindDN,
bindPass,
searchBase: ldapConfig.searchBase,
caCert
});
}

View File

@ -3,6 +3,7 @@ export * from "./secretVersion";
export * from "./folderVersion"; export * from "./folderVersion";
export * from "./role"; export * from "./role";
export * from "./ssoConfig"; export * from "./ssoConfig";
export * from "./ldapConfig";
export * from "./trustedIp"; export * from "./trustedIp";
export * from "./auditLog"; export * from "./auditLog";
export * from "./gitRisks"; export * from "./gitRisks";

View File

@ -0,0 +1,79 @@
import { Schema, Types, model } from "mongoose";
export interface ILDAPConfig {
organization: Types.ObjectId;
isActive: boolean;
url: string;
encryptedBindDN: string;
bindDNIV: string;
bindDNTag: string;
encryptedBindPass: string;
bindPassIV: string;
bindPassTag: string;
searchBase: string;
encryptedCACert: string;
caCertIV: string;
caCertTag: string;
}
const ldapConfigSchema = new Schema<ILDAPConfig>(
{
organization: {
type: Schema.Types.ObjectId,
ref: "Organization"
},
isActive: {
type: Boolean,
required: true
},
url: {
type: String,
required: true
},
encryptedBindDN: {
type: String,
required: true
},
bindDNIV: {
type: String,
required: true
},
bindDNTag: {
type: String,
required: true
},
encryptedBindPass: {
type: String,
required: true
},
bindPassIV: {
type: String,
required: true
},
bindPassTag: {
type: String,
required: true
},
searchBase: {
type: String,
required: true
},
encryptedCACert: {
type: String,
required: true
},
caCertIV: {
type: String,
required: true
},
caCertTag: {
type: String,
required: true
},
},
{
timestamps: true
}
);
export const LDAPConfig = model<ILDAPConfig>("LDAPConfig", ldapConfigSchema);

View File

@ -42,6 +42,7 @@ import {
integration as v1IntegrationRouter, integration as v1IntegrationRouter,
inviteOrg as v1InviteOrgRouter, inviteOrg as v1InviteOrgRouter,
key as v1KeyRouter, key as v1KeyRouter,
ldap as v1LDAPRouter,
membershipOrg as v1MembershipOrgRouter, membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter, membership as v1MembershipRouter,
organization as v1OrganizationRouter, organization as v1OrganizationRouter,
@ -237,6 +238,7 @@ const main = async () => {
app.use("/api/v1/roles", v1RoleRouter); app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicyRouter); app.use("/api/v1/secret-approvals", v1SecretApprovalPolicyRouter);
app.use("/api/v1/sso", v1SSORouter); app.use("/api/v1/sso", v1SSORouter);
app.use("/api/v1/ldap", v1LDAPRouter);
app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequestRouter); app.use("/api/v1/secret-approval-requests", v1SecretApprovalRequestRouter);
// v2 routes (improvements) // v2 routes (improvements)

View File

@ -7,7 +7,8 @@ export enum AuthMethod {
GITLAB = "gitlab", GITLAB = "gitlab",
OKTA_SAML = "okta-saml", OKTA_SAML = "okta-saml",
AZURE_SAML = "azure-saml", AZURE_SAML = "azure-saml",
JUMPCLOUD_SAML = "jumpcloud-saml" JUMPCLOUD_SAML = "jumpcloud-saml",
LDAP = "ldap"
} }
export interface IUser extends Document { export interface IUser extends Document {
@ -55,7 +56,7 @@ const userSchema = new Schema<IUser>(
}, },
email: { email: {
type: String, type: String,
required: true, required: false,
unique: true unique: true
}, },
firstName: { firstName: {

View File

@ -2,6 +2,7 @@ import signup from "./signup";
import bot from "./bot"; import bot from "./bot";
import auth from "./auth"; import auth from "./auth";
import universalAuth from "./universalAuth"; import universalAuth from "./universalAuth";
import ldap from "./ldap";
import user from "./user"; import user from "./user";
import userAction from "./userAction"; import userAction from "./userAction";
import organization from "./organization"; import organization from "./organization";
@ -43,5 +44,6 @@ export {
webhooks, webhooks,
secretImps, secretImps,
sso, sso,
ldap,
admin admin
}; };

View File

@ -0,0 +1,41 @@
import express from "express";
const router = express.Router();
import passport from "passport";
import { requireAuth } from "../../middleware";
import { ldapController } from "../../controllers/v1";
import { AuthMode } from "../../variables";
router.post(
"/login",
passport.authenticate("ldapauth", {
session: false
}) as any,
ldapController.redirectLDAP
);
router.get(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ldapController.getLDAPConfig
);
router.post(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ldapController.createLDAPConfig
);
router.patch(
"/config",
requireAuth({
acceptedAuthModes: [AuthMode.JWT]
}),
ldapController.updateLDAPConfig
);
export default router;

View File

@ -40,6 +40,7 @@ declare global {
requestData: { requestData: {
[key: string]: string [key: string]: string
}; };
organizationId: string;
} }
} }
} }

View File

@ -2,3 +2,4 @@ export { initializeGoogleStrategy } from "./google";
export { initializeGitHubStrategy } from "./github"; export { initializeGitHubStrategy } from "./github";
export { initializeGitLabStrategy } from "./gitlab"; export { initializeGitLabStrategy } from "./gitlab";
export { initializeSamlStrategy } from "./saml"; export { initializeSamlStrategy } from "./saml";
export { initializeLdapStrategy } from "./ldap";

View File

@ -0,0 +1,141 @@
import { Types } from "mongoose";
import {
AuthMethod,
MembershipOrg,
Organization,
User
} from "../../../models";
import passport from "passport";
import { OrganizationNotFoundError } from "../../errors";
// eslint-disable-next-line @typescript-eslint/no-var-requires
const LdapStrategy = require("passport-ldapauth");
import { getAuthSecret, getJwtProviderAuthLifetime } from "../../../config";
import { createToken } from "../../../helpers/auth";
import {
ACCEPTED,
AuthTokenType,
MEMBER
} from "../../../variables";
import { getLdapConfigHelper } from "../../../ee/helpers/organizations";
const getLDAPConfiguration = (req: any, callback: any) => {
const {
organizationId
} = req.body as {
organizationId: string;
};
req.organizationId = organizationId;
const boot = async () => {
const ldapConfig = await getLdapConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
// example
// var opts = {
// server: {
// // url: 'ldaps://openldap:636', // connection over SSL/TLS
// url: 'ldap://openldap:389',
// bindDN: 'cn=admin,dc=acme,dc=com',
// bindCredentials: 'admin',
// searchBase: 'ou=people,dc=acme,dc=com',
// searchFilter: '(uid={{username}})',
// searchAttributes: ['uid', 'givenName', 'sn'], // optional, defaults to all (get username too)
// // tlsOptions: {
// // ca: [caCert]
// // }
// },
// passReqToCallback: true
// };
const opts = {
server: {
url: ldapConfig.url,
bindDN: ldapConfig.bindDN,
bindCredentials: ldapConfig.bindPass,
searchBase: ldapConfig.searchBase,
searchFilter: "(uid={{username}})",
searchAttributes: ["uid", "givenName", "sn"],
...(ldapConfig.caCert !== "" ? {
tlsOptions: {
ca: [ldapConfig.caCert]
}
} : {}
)
},
passReqToCallback: true
};
callback(null, opts);
}
process.nextTick(async () => {
await boot();
});
};
export const initializeLdapStrategy = async () => {
passport.use(new LdapStrategy(getLDAPConfiguration,
async (req: any, user: any, done: any) => {
const organization = await Organization.findById(req.organizationId);
if (!organization) return done(OrganizationNotFoundError());
const ldapUsername = user.uid;
const firstName = user.givenName;
const lastName = user.sn;
const ldapEmail = `ldap-${ldapUsername}-${organization._id.toString()}@ldap.com`;
try {
let user = await User.findOne({
email: ldapEmail
}).select("+publicKey");
if (!user) {
user = await new User({
email: ldapEmail,
authMethods: [AuthMethod.LDAP],
firstName,
lastName,
organization
}).save();
await new MembershipOrg({
user: user._id,
organization: organization._id,
role: MEMBER,
status: ACCEPTED
}).save();
}
const isUserCompleted = !!user.publicKey;
const providerAuthToken = createToken({
payload: {
authTokenType: AuthTokenType.PROVIDER_TOKEN,
userId: user._id.toString(),
email: user.email,
firstName,
lastName,
organizationName: organization?.name,
organizationId: organization?._id,
authMethod: AuthMethod.LDAP,
isUserCompleted,
...(req.body.RelayState ? {
callbackPort: JSON.parse(req.body.RelayState).callbackPort as string
} : {})
},
expiresIn: await getJwtProviderAuthLifetime(),
secret: await getAuthSecret(),
});
req.isUserCompleted = isUserCompleted;
req.providerAuthToken = providerAuthToken;
return done(null, user);
} catch (err) {
return done(null, false);
}
}
));
}

View File

@ -30,6 +30,7 @@ import {
initializeGitHubStrategy, initializeGitHubStrategy,
initializeGitLabStrategy, initializeGitLabStrategy,
initializeGoogleStrategy, initializeGoogleStrategy,
initializeLdapStrategy,
initializeSamlStrategy initializeSamlStrategy
} from "../authn/passport"; } from "../authn/passport";
import { logger } from "../logging"; import { logger } from "../logging";
@ -67,6 +68,7 @@ export const setup = async () => {
await initializeGitHubStrategy(); await initializeGitHubStrategy();
await initializeGitLabStrategy(); await initializeGitLabStrategy();
await initializeSamlStrategy(); await initializeSamlStrategy();
await initializeLdapStrategy();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY

View File

@ -10,3 +10,4 @@ export * from "./secrets";
export * from "./serviceTokenData"; export * from "./serviceTokenData";
export * from "./identities"; export * from "./identities";
export * from "./apiKeyDataV3"; export * from "./apiKeyDataV3";
export * from "./ldap";

View File

@ -0,0 +1,29 @@
import { z } from "zod";
export const GetLdapConfigv1 = z.object({
query: z.object({ organizationId: z.string().trim() })
});
export const CreateLdapConfigv1 = z.object({
body: z.object({
organizationId: z.string().trim(),
isActive: z.boolean(),
url: z.string().trim(),
bindDN: z.string().trim(),
bindPass: z.string().trim(),
searchBase: z.string().trim(),
caCert: z.string().trim().default("")
})
});
export const UpdateLdapConfigv1 = z.object({
body: z.object({
organizationId: z.string().trim(),
isActive: z.boolean().optional(),
url: z.string().trim().optional(),
bindDN: z.string().trim().optional(),
bindPass: z.string().trim().optional(),
searchBase: z.string().trim().optional(),
caCert: z.string().trim().optional()
})
});

View File

@ -125,11 +125,42 @@ services:
networks: networks:
- infisical-dev - infisical-dev
openldap: # note: more advanced configuration is available
image: osixia/openldap:1.5.0
restart: always
environment:
LDAP_ORGANISATION: Acme
LDAP_DOMAIN: acme.com
LDAP_ADMIN_PASSWORD: admin
ports:
- 389:389
- 636:636
volumes:
- ldap_data:/var/lib/ldap
- ldap_config:/etc/ldap/slapd.d
networks:
- infisical-dev
phpldapadmin: # username: cn=admin,dc=acme,dc=com, pass is admin
image: osixia/phpldapadmin:latest
restart: always
environment:
- PHPLDAPADMIN_LDAP_HOSTS=openldap
- PHPLDAPADMIN_HTTPS=false
ports:
- 6433:80
depends_on:
- openldap
networks:
- infisical-dev
volumes: volumes:
mongo-data: mongo-data:
driver: local driver: local
redis_data: redis_data:
driver: local driver: local
ldap_data:
ldap_config:
networks: networks:
infisical-dev: infisical-dev:

View File

@ -14,19 +14,22 @@ import {
Login1Res, Login1Res,
Login2DTO, Login2DTO,
Login2Res, Login2Res,
LoginLDAPDTO,
LoginLDAPRes,
ResetPasswordDTO, ResetPasswordDTO,
SendMfaTokenDTO, SendMfaTokenDTO,
SRP1DTO, SRP1DTO,
SRPR1Res, SRPR1Res,
VerifyMfaTokenDTO, VerifyMfaTokenDTO,
VerifyMfaTokenRes, VerifyMfaTokenRes,
VerifySignupInviteDTO VerifySignupInviteDTO,
} from "./types"; } from "./types";
const authKeys = { const authKeys = {
getAuthToken: ["token"] as const getAuthToken: ["token"] as const
}; };
export const login1 = async (loginDetails: Login1DTO) => { export const login1 = async (loginDetails: Login1DTO) => {
const { data } = await apiRequest.post<Login1Res>("/api/v3/auth/login1", loginDetails); const { data } = await apiRequest.post<Login1Res>("/api/v3/auth/login1", loginDetails);
return data; return data;
@ -37,6 +40,11 @@ export const login2 = async (loginDetails: Login2DTO) => {
return data; return data;
}; };
export const loginLDAPRedirect = async (loginLDAPDetails: LoginLDAPDTO) => {
const { data } = await apiRequest.post<LoginLDAPRes>("/api/v1/ldap/login", loginLDAPDetails); // return if account is complete or not + provider auth token
return data;
}
export const useLogin1 = () => { export const useLogin1 = () => {
return useMutation({ return useMutation({
mutationFn: async (details: { mutationFn: async (details: {

View File

@ -53,6 +53,16 @@ export type Login2Res = {
tag?: string; tag?: string;
} }
export type LoginLDAPDTO = {
organizationId: string;
username: string;
password: string;
}
export type LoginLDAPRes = {
nextUrl: string;
}
export type SRP1DTO = { export type SRP1DTO = {
clientPublicKey: string; clientPublicKey: string;
} }

View File

@ -8,6 +8,7 @@ export * from "./incidentContacts";
export * from "./integrationAuth"; export * from "./integrationAuth";
export * from "./integrations"; export * from "./integrations";
export * from "./keys"; export * from "./keys";
export * from "./ldapConfig";
export * from "./organization"; export * from "./organization";
export * from "./roles"; export * from "./roles";
export * from "./secretApproval"; export * from "./secretApproval";

View File

@ -0,0 +1,5 @@
export {
useCreateLDAPConfig,
useGetLDAPConfig,
useUpdateLDAPConfig
} from "./queries";

View File

@ -0,0 +1,103 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
const ldapConfigKeys = {
getLDAPConfig: (orgId: string) => [{ orgId }, "organization-ldap"] as const,
}
export const useGetLDAPConfig = (organizationId: string) => {
return useQuery({
queryKey: ldapConfigKeys.getLDAPConfig(organizationId),
queryFn: async () => {
const { data } = await apiRequest.get(
`/api/v1/ldap/config?organizationId=${organizationId}`
);
return data;
},
enabled: true
});
}
export const useCreateLDAPConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}: {
organizationId: string;
isActive: boolean;
url: string;
bindDN: string;
bindPass: string;
searchBase: string;
caCert?: string;
}) => {
const { data } = await apiRequest.post(
"/api/v1/ldap/config",
{
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
}
});
};
export const useUpdateLDAPConfig = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}: {
organizationId: string;
isActive?: boolean;
url?: string;
bindDN?: string;
bindPass?: string;
searchBase?: string;
caCert?: string;
}) => {
const { data } = await apiRequest.patch(
"/api/v1/ldap/config",
{
organizationId,
isActive,
url,
bindDN,
bindPass,
searchBase,
caCert
}
);
return data;
},
onSuccess(_, dto) {
queryClient.invalidateQueries(ldapConfigKeys.getLDAPConfig(dto.organizationId));
}
});
};

View File

@ -7,9 +7,9 @@ import { getAuthToken, isLoggedIn } from "@app/reactQuery";
import { import {
InitialStep, InitialStep,
LDAPStep,
MFAStep, MFAStep,
SAMLSSOStep SAMLSSOStep} from "./components";
} from "./components";
// import { navigateUserToOrg } from "../../Login.utils"; // import { navigateUserToOrg } from "../../Login.utils";
import { navigateUserToOrg } from "./Login.utils"; import { navigateUserToOrg } from "./Login.utils";
@ -73,7 +73,10 @@ export const Login = () => {
return ( return (
<SAMLSSOStep setStep={setStep} /> <SAMLSSOStep setStep={setStep} />
); );
case 3:
return (
<LDAPStep setStep={setStep} />
);
default: default:
return <div />; return <div />;
} }

View File

@ -179,7 +179,20 @@ export const InitialStep = ({ setStep, email, setEmail, password, setPassword }:
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />} leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full" className="mx-0 h-10 w-full"
> >
Continue with SSO Continue with SAML
</Button>
</div>
<div className="mt-2 w-1/4 min-w-[21.2rem] rounded-md text-center md:min-w-[20.1rem] lg:w-1/6">
<Button
colorSchema="primary"
variant="outline_bg"
onClick={() => {
setStep(3);
}}
leftIcon={<FontAwesomeIcon icon={faLock} className="mr-2" />}
className="mx-0 h-10 w-full"
>
Continue with LDAP
</Button> </Button>
</div> </div>
<div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6"> <div className="my-4 flex w-1/4 min-w-[20rem] flex-row items-center py-2 lg:w-1/6">

View File

@ -0,0 +1,128 @@
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { Button, Input } from "@app/components/v2";
import { loginLDAPRedirect } from "@app/hooks/api/auth/queries";
type Props = {
setStep: (step: number) => void;
}
export const LDAPStep = ({
setStep
}: Props) => {
const { createNotification } = useNotificationContext();
const [organizationId, setOrganizationId] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const { t } = useTranslation();
// const queryParams = new URLSearchParams(window.location.search);
const handleSubmission = async (e:React.FormEvent) => {
e.preventDefault()
try {
const { nextUrl } = await loginLDAPRedirect({
organizationId,
username,
password
});
createNotification({
text: "Successfully logged in",
type: "success"
});
// redirects either to /login/sso or /signup/sso
window.open(nextUrl);
window.close();
} catch (err) {
createNotification({
text: "Login unsuccessful. Double-check your credentials and try again.",
type: "error"
});
}
// TODO: add callback port support
// const callbackPort = queryParams.get("callback_port");
// window.open(`/api/v1/ldap/redirect/saml2/${ssoIdentifier}${callbackPort ? `?callback_port=${callbackPort}` : ""}`);
// window.close();
}
return (
<div className="mx-auto w-full max-w-md md:px-6">
<p className="mx-auto mb-6 flex w-max justify-center text-xl font-medium text-transparent bg-clip-text bg-gradient-to-b from-white to-bunker-200 text-center mb-8">
What&apos;s your LDAP Login?
</p>
<form onSubmit={handleSubmission}>
<div className="relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={organizationId}
onChange={(e) => setOrganizationId(e.target.value)}
type="text"
placeholder="Enter your organization ID..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className="mt-2 relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={username}
onChange={(e) => setUsername(e.target.value)}
type="text"
placeholder="Enter your LDAP username..."
isRequired
autoComplete="email"
id="email"
className="h-12"
/>
</div>
</div>
<div className="mt-2 relative flex items-center justify-center lg:w-1/6 w-1/4 min-w-[20rem] md:min-w-[22rem] mx-auto w-full rounded-lg max-h-24 md:max-h-28">
<div className="flex items-center justify-center w-full rounded-lg max-h-24 md:max-h-28">
<Input
value={password}
onChange={(e) => setPassword(e.target.value)}
type="password"
placeholder="Enter your LDAP password..."
isRequired
autoComplete="current-password"
id="current-password"
className="select:-webkit-autofill:focus h-10"
/>
</div>
</div>
<div className='lg:w-1/6 w-1/4 w-full mx-auto flex items-center justify-center min-w-[20rem] md:min-w-[22rem] text-center rounded-md mt-4'>
<Button
type="submit"
colorSchema="primary"
variant="outline_bg"
isFullWidth
className="h-14"
>
{t("login.login")}
</Button>
</div>
</form>
<div className="flex flex-row items-center justify-center mt-4">
<button
onClick={() => {
setStep(0);
}}
type="button"
className="text-bunker-300 text-sm hover:underline mt-2 hover:underline-offset-4 hover:decoration-primary-700 hover:text-bunker-200 duration-200 cursor-pointer"
>
{t("login.other-option")}
</button>
</div>
</div>
);
};

View File

@ -0,0 +1 @@
export { LDAPStep } from "./LDAPStep";

View File

@ -1,4 +1,5 @@
export { InitialStep } from "./InitialStep"; export { InitialStep } from "./InitialStep";
export { LDAPStep } from "./LDAPStep";
export { MFAStep } from "./MFAStep"; export { MFAStep } from "./MFAStep";
export { SAMLSSOStep } from "./SAMLSSOStep"; export { SAMLSSOStep } from "./SAMLSSOStep";

View File

@ -181,7 +181,11 @@ export const OrgMembersTable = ({
filterdUser?.map( filterdUser?.map(
({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => { ({ user: u, inviteEmail, role, customRole, _id: orgMembershipId, status }) => {
const name = u ? `${u.firstName} ${u.lastName}` : "-"; const name = u ? `${u.firstName} ${u.lastName}` : "-";
const email = u?.email || inviteEmail; let email = u?.email || inviteEmail;
if (email.startsWith("ldap-")) {
email = "-";
}
return ( return (
<Tr key={`org-membership-${orgMembershipId}`} className="w-full"> <Tr key={`org-membership-${orgMembershipId}`} className="w-full">
<Td>{name}</Td> <Td>{name}</Td>

View File

@ -0,0 +1,228 @@
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalContent,
TextArea
} from "@app/components/v2";
import { useOrganization } from "@app/context";
import {
useCreateLDAPConfig,
useGetLDAPConfig,
useUpdateLDAPConfig
} from "@app/hooks/api";
import { UsePopUpState } from "@app/hooks/usePopUp";
const schema = yup.object({
url: yup.string().required("URL is required"),
bindDN: yup.string().required("Bind DN is required"),
bindPass: yup.string().required("Bind Pass is required"),
searchBase: yup.string().required("Search Base is required"),
caCert: yup.string()
}).required();
export type AddLDAPFormData = yup.InferType<typeof schema>;
type Props = {
popUp: UsePopUpState<["addLDAP"]>;
handlePopUpClose: (popUpName: keyof UsePopUpState<["addLDAP"]>) => void;
handlePopUpToggle: (popUpName: keyof UsePopUpState<["addLDAP"]>, state?: boolean) => void;
};
export const LDAPModal = ({
popUp,
handlePopUpClose,
handlePopUpToggle
}: Props) => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { mutateAsync: createMutateAsync, isLoading: createIsLoading } = useCreateLDAPConfig();
const { mutateAsync: updateMutateAsync, isLoading: updateIsLoading } = useUpdateLDAPConfig();
const { data } = useGetLDAPConfig(currentOrg?._id ?? "");
const {
control,
handleSubmit,
reset,
} = useForm<AddLDAPFormData>({
resolver: yupResolver(schema)
});
useEffect(() => {
if (data) {
reset({
url: data?.url ?? "",
bindDN: data?.bindDN ?? "",
bindPass: data?.bindPass ?? "",
searchBase: data?.searchBase ?? "",
caCert: data?.caCert ?? ""
});
}
}, [data]);
const onSSOModalSubmit = async ({
url,
bindDN,
bindPass,
searchBase,
caCert
}: AddLDAPFormData) => {
try {
if (!currentOrg) return;
if (!data) {
await createMutateAsync({
organizationId: currentOrg._id,
isActive: false,
url,
bindDN,
bindPass,
searchBase,
caCert
});
} else {
await updateMutateAsync({
organizationId: currentOrg._id,
isActive: false,
url,
bindDN,
bindPass,
searchBase,
caCert
});
}
handlePopUpClose("addLDAP");
createNotification({
text: `Successfully ${!data ? "added" : "updated"} LDAP configuration`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${!data ? "add" : "update"} LDAP configuration`,
type: "error"
});
}
}
return (
<Modal
isOpen={popUp?.addLDAP?.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("addLDAP", isOpen);
reset();
}}
>
<ModalContent title="Add LDAP">
<form onSubmit={handleSubmit(onSSOModalSubmit)}>
<Controller
control={control}
name="url"
render={({ field, fieldState: { error } }) => (
<FormControl
label="URL"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="ldaps://ldap.myorg.com:636"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="bindDN"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bind DN"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="cn=infisical,ou=Users,dc=example,dc=com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="bindPass"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Bind Pass"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="********"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="searchBase"
render={({ field, fieldState: { error } }) => (
<FormControl
label="Search Base / User DN"
errorText={error?.message}
isError={Boolean(error)}
>
<Input
{...field}
placeholder="ou=people,dc=acme,dc=com"
/>
</FormControl>
)}
/>
<Controller
control={control}
name="caCert"
render={({ field, fieldState: { error } }) => (
<FormControl
label="CA Certificate"
errorText={error?.message}
isError={Boolean(error)}
>
<TextArea
{...field}
placeholder="-----BEGIN CERTIFICATE----- ..."
/>
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
size="sm"
type="submit"
isLoading={createIsLoading || updateIsLoading}
>
{!data ? "Add" : "Update"}
</Button>
<Button
colorSchema="secondary"
variant="plain"
onClick={() => handlePopUpClose("addLDAP")}
>
Cancel
</Button>
</div>
</form>
</ModalContent>
</Modal>
);
}

View File

@ -1,6 +1,7 @@
import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context"; import { OrgPermissionActions, OrgPermissionSubjects } from "@app/context";
import { withPermission } from "@app/hoc"; import { withPermission } from "@app/hoc";
import { OrgLDAPSection } from "./OrgLDAPSection";
import { OrgSSOSection } from "./OrgSSOSection"; import { OrgSSOSection } from "./OrgSSOSection";
export const OrgAuthTab = withPermission( export const OrgAuthTab = withPermission(
@ -8,6 +9,7 @@ export const OrgAuthTab = withPermission(
return ( return (
<div> <div>
<OrgSSOSection /> <OrgSSOSection />
<OrgLDAPSection />
</div> </div>
); );
}, },

View File

@ -0,0 +1,136 @@
import { faPlus } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { OrgPermissionCan } from "@app/components/permissions";
import { Button, Switch } from "@app/components/v2";
import {
OrgPermissionActions,
OrgPermissionSubjects,
useOrganization,
} from "@app/context";
import {
useCreateLDAPConfig,
useGetLDAPConfig,
useUpdateLDAPConfig} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { LDAPModal } from "./LDAPModal";
export const OrgLDAPSection = (): JSX.Element => {
const { currentOrg } = useOrganization();
const { createNotification } = useNotificationContext();
const { data, isLoading } = useGetLDAPConfig(currentOrg?._id ?? "");
const { mutateAsync } = useUpdateLDAPConfig();
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
"addLDAP"
] as const);
const { mutateAsync: createMutateAsync } = useCreateLDAPConfig();
const handleSamlSSOToggle = async (value: boolean) => {
try {
if (!currentOrg?._id) return;
await mutateAsync({
organizationId: currentOrg?._id,
isActive: value
});
createNotification({
text: `Successfully ${value ? "enabled" : "disabled"} LDAP`,
type: "success"
});
} catch (err) {
console.error(err);
createNotification({
text: `Failed to ${value ? "enable" : "disable"} LDAP`,
type: "error"
});
}
};
const addLDAPBtnClick = async () => {
try {
if (currentOrg) {
if (!data) {
// case: LDAP is not configured
// -> initialize empty LDAP configuration
await createMutateAsync({
organizationId: currentOrg._id,
isActive: false,
url: "",
bindDN: "",
bindPass: "",
searchBase: "",
});
}
handlePopUpOpen("addLDAP");
}
} catch (err) {
console.error(err);
}
};
return (
<div className="p-4 bg-mineshaft-900 mb-6 rounded-lg border border-mineshaft-600">
<div className="flex items-center mb-8">
<h2 className="text-xl font-semibold flex-1 text-white">LDAP Configuration</h2>
{!isLoading && (
<OrgPermissionCan I={OrgPermissionActions.Create} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Button
onClick={addLDAPBtnClick}
colorSchema="secondary"
isDisabled={!isAllowed}
leftIcon={<FontAwesomeIcon icon={faPlus} />}
>
{data ? "Update LDAP" : "Set up LDAP"}
</Button>
)}
</OrgPermissionCan>
)}
</div>
{data && (
<div className="mb-4">
<OrgPermissionCan I={OrgPermissionActions.Edit} a={OrgPermissionSubjects.Sso}>
{(isAllowed) => (
<Switch
id="enable-saml-sso"
onCheckedChange={(value) => handleSamlSSOToggle(value)}
isChecked={data ? data.isActive : false}
isDisabled={!isAllowed}
>
Enable LDAP
</Switch>
)}
</OrgPermissionCan>
</div>
)}
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">URL</h3>
<p className="text-gray-400 text-md">{data && data.url !== "" ? data.url : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Bind DN</h3>
<p className="text-gray-400 text-md">{data && data.bindDN !== "" ? data.bindDN : "-"}</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Bind Pass</h3>
<p className="text-gray-400 text-md">
{data && data.bindPass !== "" ? "*".repeat(data.bindPass.length) : "-"}
</p>
</div>
<div className="mb-4">
<h3 className="text-mineshaft-400 text-sm">Search Base / User DN</h3>
<p className="text-gray-400 text-md">{data && data.searchBase !== "" ? data.searchBase : "-"}</p>
</div>
<LDAPModal
popUp={popUp}
handlePopUpClose={handlePopUpClose}
handlePopUpToggle={handlePopUpToggle}
/>
</div>
);
};