Browse Source

refs 123795: enable user bind for ldap group sync

Futa Arai 2 years ago
parent
commit
a6b9c23675

+ 1 - 0
apps/app/package.json

@@ -75,6 +75,7 @@
     "@promster/server": "^7.0.8",
     "@slack/web-api": "^6.2.4",
     "@slack/webhook": "^6.0.0",
+    "@types/ldapjs": "^2.2.5",
     "JSONStream": "^1.3.5",
     "archiver": "^5.3.0",
     "array.prototype.flatmap": "^1.2.2",

+ 44 - 8
apps/app/src/components/Admin/UserGroup/ExternalGroup/LdapGroupManagement.tsx

@@ -1,14 +1,34 @@
-import { FC, useCallback } from 'react';
+import {
+  FC, useCallback, useEffect, useState,
+} from 'react';
 
-import { apiv3Put } from '~/client/util/apiv3-client';
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 
 import { LdapGroupSyncSettingsForm } from './LdapGroupSyncSettingsForm';
 
 export const LdapGroupManagement: FC = () => {
-  const onSyncBtnClick = useCallback(async() => {
+  const [isUserBind, setIsUserBind] = useState(false);
+
+  useEffect(() => {
+    const getIsUserBind = async() => {
+      try {
+        const response = await apiv3Get('/security-setting/');
+        const { ldapAuth } = response.data.securityParams;
+        setIsUserBind(ldapAuth.isUserBind);
+      }
+      catch (e) {
+        toastError(e);
+      }
+    };
+    getIsUserBind();
+  }, []);
+
+  const onSyncBtnClick = useCallback(async(e) => {
+    e.preventDefault();
+    const password = e.target.password.value;
     try {
-      await apiv3Put('/external-user-groups/ldap/sync');
+      await apiv3Put('/external-user-groups/ldap/sync', { password });
       toastSuccess('同期に成功しました');
     }
     catch (e) {
@@ -19,9 +39,25 @@ export const LdapGroupManagement: FC = () => {
   return <>
     <LdapGroupSyncSettingsForm />
     <h3 className="border-bottom mb-3">同期実行</h3>
-    <div className="row">
-      <div className="col-md-3"></div>
-      <div className="col-md-6"><button className="btn btn-primary" onClick={onSyncBtnClick}>同期</button></div>
-    </div>
+    <form onSubmit={onSyncBtnClick}>
+      {isUserBind && <div className="row form-group">
+        <label htmlFor="ldapGroupSyncPassword" className="text-left text-md-right col-md-3 col-form-label">パスワード</label>
+        <div className="col-md-6">
+          <input
+            className="form-control"
+            type="password"
+            name="password"
+            id="ldapGroupSyncPassword"
+          />
+          <p className="form-text text-muted">
+            <small>認証設定がユーザBind のため、ログイン時のパスワードの入力が必要となります。</small>
+          </p>
+        </div>
+      </div>}
+      <div className="row">
+        <div className="col-md-3"></div>
+        <div className="col-md-6"><button className="btn btn-primary" type="submit">同期</button></div>
+      </div>
+    </form>
   </>;
 };

+ 13 - 2
apps/app/src/server/routes/apiv3/external-user-group.ts

@@ -80,8 +80,19 @@ module.exports = (crowi: Crowi): Router => {
 
   router.put('/ldap/sync', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
     try {
-      const groups = await crowi.ldapService?.searchGroup();
-      logger.debug(`ldap groups: ${groups}`);
+      const isUserBind = crowi.configManager?.getConfig('crowi', 'security:passport-ldap:isUserBind');
+      if (isUserBind) {
+        const username = req.user.name;
+        const password = req.body.password;
+        const groups = await crowi.ldapService?.searchGroup(username, password);
+        console.log('ldap groups');
+        console.log(groups);
+      }
+      else {
+        const groups = await crowi.ldapService?.searchGroup();
+        console.log('ldap groups');
+        console.log(groups);
+      }
     }
     catch (e) {
       res.apiv3Err(e, 500);

+ 19 - 5
apps/app/src/server/service/ldap.ts

@@ -20,7 +20,14 @@ class LdapService {
     this.crowi = crowi;
   }
 
-  search(filter?: string, base?: string): Promise<ldap.SearchEntry[]> {
+  /**
+   * Execute search on LDAP server and return result
+   * @param {string} username Necessary when bind type is user bind
+   * @param {string} password Necessary when bind type is user bind
+   * @param {string} filter Search filter
+   * @param {string} base Base DN to execute search on
+   */
+  search(username?: string, password?: string, filter?: string, base?: string): Promise<ldap.SearchEntry[]> {
     const { configManager } = this.crowi;
     const isLdapEnabled = configManager?.getConfig('crowi', 'security:passport-ldap:isEnabled');
 
@@ -35,7 +42,6 @@ class LdapService {
     const serverUrl = configManager?.getConfig('crowi', 'security:passport-ldap:serverUrl');
     const bindDN = configManager?.getConfig('crowi', 'security:passport-ldap:bindDN');
     const bindCredentials = configManager?.getConfig('crowi', 'security:passport-ldap:bindDNPassword');
-    const searchFilter = configManager?.getConfig('crowi', 'security:passport-ldap:searchFilter') || '(uid={{username}})';
 
     // parse serverUrl
     // see: https://regex101.com/r/0tuYBB/1
@@ -48,11 +54,17 @@ class LdapService {
     const url = match[1];
     const searchBase = match[2] || '';
 
+    // user bind
+    const fixedBindDN = (isUserBind)
+      ? bindDN.replace(/{{username}}/, username)
+      : bindDN;
+    const fixedBindCredentials = (isUserBind) ? password : bindCredentials;
+
     const client = ldap.createClient({
       url,
     });
 
-    client.bind(bindDN, bindCredentials, (err) => {
+    client.bind(fixedBindDN, fixedBindCredentials, (err) => {
       assert.ifError(err);
     });
 
@@ -64,6 +76,8 @@ class LdapService {
           reject(err);
         }
 
+        // @types/ldapjs is outdated, and pojo property (type SearchResultEntry) does not exist.
+        // Typecast and use SearchEntry in the meantime.
         res.on('searchEntry', (entry: any) => {
           const pojo = entry?.pojo as ldap.SearchEntry;
           searchResults.push(pojo);
@@ -83,13 +97,13 @@ class LdapService {
     });
   }
 
-  searchGroup(): Promise<ldap.SearchEntry[]> {
+  searchGroup(username?: string, password?: string): Promise<ldap.SearchEntry[]> {
     const { configManager } = this.crowi;
 
     const groupSearchBase = configManager?.getConfig('crowi', 'external-user-group:ldap:groupSearchBase')
     || configManager?.getConfig('crowi', 'security:passport-ldap:groupSearchBase');
 
-    return this.search(undefined, groupSearchBase);
+    return this.search(username, password, undefined, groupSearchBase);
   }
 
 }

+ 7 - 0
yarn.lock

@@ -3963,6 +3963,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/ldapjs@^2.2.5":
+  version "2.2.5"
+  resolved "https://registry.yarnpkg.com/@types/ldapjs/-/ldapjs-2.2.5.tgz#b6623bc5ad4fab85ef3cfa586db691d016a3598c"
+  integrity sha512-Lv/nD6QDCmcT+V1vaTRnEKE8UgOilVv5pHcQuzkU1LcRe4mbHHuUo/KHi0LKrpdHhQY8FJzryF38fcVdeUIrzg==
+  dependencies:
+    "@types/node" "*"
+
 "@types/lodash@^4.14.161":
   version "4.14.178"
   resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"