From a61826690560d4e88c3a0edb52b281798c5cf5a4 Mon Sep 17 00:00:00 2001 From: Yuichi Oneda Date: Wed, 29 May 2024 14:46:20 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=20feature(auth):=20LDAP=20Authenti?= =?UTF-8?q?cation=20(#2859)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🔧 chore: npm install passport-ldapauth * ✨ feat(auth): add ldap authentication support * chore: merge conflict fix --------- Co-authored-by: Danny Avila --- .env.example | 8 + api/models/schema/userSchema.js | 5 + api/package.json | 1 + api/server/index.js | 7 +- api/server/middleware/index.js | 2 + api/server/middleware/requireLdapAuth.js | 22 +++ api/server/routes/__tests__/config.spec.js | 11 ++ api/server/routes/auth.js | 11 +- api/server/routes/config.js | 5 +- api/strategies/index.js | 2 + api/strategies/ldapStrategy.js | 67 +++++++ .../components/Auth/__tests__/Login.spec.tsx | 1 + package-lock.json | 163 ++++++++++++++++++ packages/data-provider/src/types.ts | 1 + 14 files changed, 303 insertions(+), 3 deletions(-) create mode 100644 api/server/middleware/requireLdapAuth.js create mode 100644 api/strategies/ldapStrategy.js diff --git a/.env.example b/.env.example index 70370ef071..bba381ae7a 100644 --- a/.env.example +++ b/.env.example @@ -360,6 +360,14 @@ OPENID_REQUIRED_ROLE_PARAMETER_PATH= OPENID_BUTTON_LABEL= OPENID_IMAGE_URL= +# LDAP +LDAP_URL= +LDAP_BIND_DN= +LDAP_BIND_CREDENTIALS= +LDAP_USER_SEARCH_BASE= +LDAP_SEARCH_FILTER=mail={{username}} +LDAP_CA_CERT_PATH= + #========================# # Email Password Reset # #========================# diff --git a/api/models/schema/userSchema.js b/api/models/schema/userSchema.js index 80e635fc60..25fababd91 100644 --- a/api/models/schema/userSchema.js +++ b/api/models/schema/userSchema.js @@ -64,6 +64,11 @@ const userSchema = mongoose.Schema( unique: true, sparse: true, }, + ldapId: { + type: String, + unique: true, + sparse: true, + }, githubId: { type: String, unique: true, diff --git a/api/package.json b/api/package.json index ac254dfcb7..ca1a62daf8 100644 --- a/api/package.json +++ b/api/package.json @@ -86,6 +86,7 @@ "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", + "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", "pino": "^8.12.1", "sharp": "^0.32.6", diff --git a/api/server/index.js b/api/server/index.js index f47af6725a..6a6e135245 100644 --- a/api/server/index.js +++ b/api/server/index.js @@ -15,7 +15,7 @@ const AppService = require('./services/AppService'); const noIndex = require('./middleware/noIndex'); const { isEnabled } = require('~/server/utils'); const { logger } = require('~/config'); - +const { ldapLogin } = require('~/strategies'); const routes = require('./routes'); const { PORT, HOST, ALLOW_SOCIAL_LOGIN } = process.env ?? {}; @@ -60,6 +60,11 @@ const startServer = async () => { passport.use(await jwtLogin()); passport.use(passportLogin()); + // LDAP Auth + if (process.env.LDAP_URL && process.env.LDAP_BIND_DN && process.env.LDAP_USER_SEARCH_BASE) { + passport.use(ldapLogin); + } + if (isEnabled(ALLOW_SOCIAL_LOGIN)) { configureSocialLogins(app); } diff --git a/api/server/middleware/index.js b/api/server/middleware/index.js index 77ca016570..a7663f3d75 100644 --- a/api/server/middleware/index.js +++ b/api/server/middleware/index.js @@ -6,6 +6,7 @@ const setHeaders = require('./setHeaders'); const loginLimiter = require('./loginLimiter'); const validateModel = require('./validateModel'); const requireJwtAuth = require('./requireJwtAuth'); +const requireLdapAuth = require('./requireLdapAuth'); const uploadLimiters = require('./uploadLimiters'); const registerLimiter = require('./registerLimiter'); const messageLimiters = require('./messageLimiters'); @@ -29,6 +30,7 @@ module.exports = { setHeaders, loginLimiter, requireJwtAuth, + requireLdapAuth, registerLimiter, requireLocalAuth, validateEndpoint, diff --git a/api/server/middleware/requireLdapAuth.js b/api/server/middleware/requireLdapAuth.js new file mode 100644 index 0000000000..4208050c0b --- /dev/null +++ b/api/server/middleware/requireLdapAuth.js @@ -0,0 +1,22 @@ +const passport = require('passport'); + +const requireLdapAuth = (req, res, next) => { + passport.authenticate('ldapauth', (err, user, info) => { + if (err) { + console.log({ + title: '(requireLdapAuth) Error at passport.authenticate', + parameters: [{ name: 'error', value: err }], + }); + return next(err); + } + if (!user) { + console.log({ + title: '(requireLdapAuth) Error: No user', + }); + return res.status(422).send(info); + } + req.user = user; + next(); + })(req, res, next); +}; +module.exports = requireLdapAuth; diff --git a/api/server/routes/__tests__/config.spec.js b/api/server/routes/__tests__/config.spec.js index bc3742dfff..0d3251aff9 100644 --- a/api/server/routes/__tests__/config.spec.js +++ b/api/server/routes/__tests__/config.spec.js @@ -25,6 +25,11 @@ afterEach(() => { delete process.env.DOMAIN_SERVER; delete process.env.ALLOW_REGISTRATION; delete process.env.ALLOW_SOCIAL_LOGIN; + delete process.env.LDAP_URL; + delete process.env.LDAP_BIND_DN; + delete process.env.LDAP_BIND_CREDENTIALS; + delete process.env.LDAP_USER_SEARCH_BASE; + delete process.env.LDAP_SEARCH_FILTER; }); //TODO: This works/passes locally but http request tests fail with 404 in CI. Need to figure out why. @@ -50,6 +55,11 @@ describe.skip('GET /', () => { process.env.DOMAIN_SERVER = 'http://test-server.com'; process.env.ALLOW_REGISTRATION = 'true'; process.env.ALLOW_SOCIAL_LOGIN = 'true'; + process.env.LDAP_URL = 'Test LDAP URL'; + process.env.LDAP_BIND_DN = 'Test LDAP Bind DN'; + process.env.LDAP_BIND_CREDENTIALS = 'Test LDAP Bind Credentials'; + process.env.LDAP_USER_SEARCH_BASE = 'Test LDAP User Search Base'; + process.env.LDAP_SEARCH_FILTER = 'Test LDAP Search Filter'; const response = await request(app).get('/'); @@ -64,6 +74,7 @@ describe.skip('GET /', () => { openidLoginEnabled: true, openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', + ldapLoginEnabled: true, serverDomain: 'http://test-server.com', emailLoginEnabled: 'true', registrationEnabled: 'true', diff --git a/api/server/routes/auth.js b/api/server/routes/auth.js index 862a098fa5..3a19e77bf6 100644 --- a/api/server/routes/auth.js +++ b/api/server/routes/auth.js @@ -12,15 +12,24 @@ const { loginLimiter, registerLimiter, requireJwtAuth, + requireLdapAuth, requireLocalAuth, validateRegistration, } = require('../middleware'); const router = express.Router(); +const ldapAuth = + !!process.env.LDAP_URL && !!process.env.LDAP_BIND_DN && !!process.env.LDAP_USER_SEARCH_BASE; //Local router.post('/logout', requireJwtAuth, logoutController); -router.post('/login', loginLimiter, checkBan, requireLocalAuth, loginController); +router.post( + '/login', + loginLimiter, + checkBan, + ldapAuth ? requireLdapAuth : requireLocalAuth, + loginController, +); router.post('/refresh', refreshController); router.post('/register', registerLimiter, checkBan, validateRegistration, registrationController); router.post('/requestPasswordReset', resetPasswordRequestController); diff --git a/api/server/routes/config.js b/api/server/routes/config.js index 32e03bc8e8..92b8267ca3 100644 --- a/api/server/routes/config.js +++ b/api/server/routes/config.js @@ -13,6 +13,8 @@ router.get('/', async function (req, res) { return today.getMonth() === 1 && today.getDate() === 11; }; + const ldapLoginEnabled = + !!process.env.LDAP_URL && !!process.env.LDAP_BIND_DN && !!process.env.LDAP_USER_SEARCH_BASE; try { /** @type {TStartupConfig} */ const payload = { @@ -30,9 +32,10 @@ router.get('/', async function (req, res) { !!process.env.OPENID_SESSION_SECRET, openidLabel: process.env.OPENID_BUTTON_LABEL || 'Continue with OpenID', openidImageUrl: process.env.OPENID_IMAGE_URL, + ldapLoginEnabled, serverDomain: process.env.DOMAIN_SERVER || 'http://localhost:3080', emailLoginEnabled, - registrationEnabled: isEnabled(process.env.ALLOW_REGISTRATION), + registrationEnabled: !ldapLoginEnabled && isEnabled(process.env.ALLOW_REGISTRATION), socialLoginEnabled: isEnabled(process.env.ALLOW_SOCIAL_LOGIN), emailEnabled: (!!process.env.EMAIL_SERVICE || !!process.env.EMAIL_HOST) && diff --git a/api/strategies/index.js b/api/strategies/index.js index 4cf457190b..5ff3b51d92 100644 --- a/api/strategies/index.js +++ b/api/strategies/index.js @@ -5,6 +5,7 @@ const discordLogin = require('./discordStrategy'); const facebookLogin = require('./facebookStrategy'); const setupOpenId = require('./openidStrategy'); const jwtLogin = require('./jwtStrategy'); +const ldapLogin = require('./ldapStrategy'); module.exports = { passportLogin, @@ -14,4 +15,5 @@ module.exports = { jwtLogin, facebookLogin, setupOpenId, + ldapLogin, }; diff --git a/api/strategies/ldapStrategy.js b/api/strategies/ldapStrategy.js new file mode 100644 index 0000000000..1a81bc403d --- /dev/null +++ b/api/strategies/ldapStrategy.js @@ -0,0 +1,67 @@ +const LdapStrategy = require('passport-ldapauth'); +const User = require('~/models/User'); +const fs = require('fs'); + +const ldapOptions = { + server: { + url: process.env.LDAP_URL, + bindDN: process.env.LDAP_BIND_DN, + bindCredentials: process.env.LDAP_BIND_CREDENTIALS, + searchBase: process.env.LDAP_USER_SEARCH_BASE, + searchFilter: process.env.LDAP_SEARCH_FILTER || 'mail={{username}}', + searchAttributes: ['displayName', 'mail', 'uid', 'cn', 'name', 'commonname', 'givenName', 'sn'], + ...(process.env.LDAP_CA_CERT_PATH && { + tlsOptions: { ca: [fs.readFileSync(process.env.LDAP_CA_CERT_PATH)] }, + }), + }, + usernameField: 'email', + passwordField: 'password', +}; + +const ldapLogin = new LdapStrategy(ldapOptions, async (userinfo, done) => { + if (!userinfo) { + return done(null, false, { message: 'Invalid credentials' }); + } + + try { + const firstName = userinfo.givenName; + const familyName = userinfo.surname || userinfo.sn; + const fullName = + firstName && familyName + ? `${firstName} ${familyName}` + : userinfo.cn || + userinfo.name || + userinfo.commonname || + userinfo.displayName || + userinfo.mail; + + const username = userinfo.givenName || userinfo.mail; + let user = await User.findOne({ email: userinfo.mail }); + if (user && user.provider !== 'ldap') { + return done(null, false, { message: 'Invalid credentials' }); + } + if (!user) { + user = new User({ + provider: 'ldap', + ldapId: userinfo.uid, + username, + email: userinfo.mail || '', + emailVerified: true, + name: fullName, + }); + } else { + user.provider = 'ldap'; + user.ldapId = userinfo.uid; + user.username = username; + user.name = fullName; + } + + await user.save(); + + done(null, user); + } catch (err) { + done(err); + } +}); + +module.exports = ldapLogin; diff --git a/client/src/components/Auth/__tests__/Login.spec.tsx b/client/src/components/Auth/__tests__/Login.spec.tsx index 0e332ea5fa..1f565d6ab1 100644 --- a/client/src/components/Auth/__tests__/Login.spec.tsx +++ b/client/src/components/Auth/__tests__/Login.spec.tsx @@ -21,6 +21,7 @@ const mockStartupConfig = { openidLoginEnabled: true, openidLabel: 'Test OpenID', openidImageUrl: 'http://test-server.com', + ldapLoginEnabled: false, registrationEnabled: true, emailLoginEnabled: true, socialLoginEnabled: true, diff --git a/package-lock.json b/package-lock.json index 71326648ba..f39075f7f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,6 +94,7 @@ "passport-github2": "^0.1.12", "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", + "passport-ldapauth": "^3.0.1", "passport-local": "^1.0.0", "pino": "^8.12.1", "sharp": "^0.32.6", @@ -9992,6 +9993,14 @@ "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.14.0.tgz", "integrity": "sha512-+2FW2CcT0K3P+JMR8YG846bmDwplKUTsWgT2ENwdQ1UdVfRk3GQrh6Mi4sTopy30gI8Uau5CEqHTDZ6YvWIUPA==" }, + "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/mdast": { "version": "3.0.15", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz", @@ -11144,6 +11153,14 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "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/asn1.js": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", @@ -11175,6 +11192,14 @@ "util": "^0.12.5" } }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "engines": { + "node": ">=0.8" + } + }, "node_modules/async": { "version": "3.2.5", "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", @@ -11506,6 +11531,17 @@ "@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/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -14719,6 +14755,14 @@ "node": ">=4" } }, + "node_modules/extsprintf": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz", + "integrity": "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==", + "engines": [ + "node >=0.6.0" + ] + }, "node_modules/fast-content-type-parse": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", @@ -19050,6 +19094,58 @@ "langsmith": "dist/cli/main.cjs" } }, + "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==", + "deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md", + "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": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -22159,6 +22255,18 @@ "passport-strategy": "^1.0.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-local": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", @@ -23383,6 +23491,14 @@ "node": ">=6" } }, + "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": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -27773,6 +27889,53 @@ "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/vasync/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, + "node_modules/vasync/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "engines": [ + "node >=0.6.0" + ], + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/verror/node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" + }, "node_modules/vfile": { "version": "5.3.7", "resolved": "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz", diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 2eedd37925..23ed7470bb 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -271,6 +271,7 @@ export type TStartupConfig = { openidLoginEnabled: boolean; openidLabel: string; openidImageUrl: string; + ldapLoginEnabled: boolean; serverDomain: string; emailLoginEnabled: boolean; registrationEnabled: boolean;