Просмотр исходного кода

refs 124384: make getMemberUser not abstract

Futa Arai 2 лет назад
Родитель
Сommit
446a0609c4

+ 1 - 1
apps/app/src/components/Admin/UserGroup/UserGroupPage.tsx

@@ -9,7 +9,7 @@ import { IUserGroup, IUserGroupHasId } from '~/interfaces/user';
 import { useIsAclEnabled } from '~/stores/context';
 import { useSWRxUserGroupList, useSWRxChildUserGroupList, useSWRxUserGroupRelationList } from '~/stores/user-group';
 
-import { ExternalGroupManagement } from './ExternalGroup/ExternalGroupManagement';
+import { ExternalGroupManagement } from './ExternalUserGroup/ExternalUserGroupManagement';
 
 const UserGroupDeleteModal = dynamic(() => import('./UserGroupDeleteModal').then(mod => mod.UserGroupDeleteModal), { ssr: false });
 const UserGroupModal = dynamic(() => import('./UserGroupModal').then(mod => mod.UserGroupModal), { ssr: false });

+ 8 - 1
apps/app/src/interfaces/external-user-group.ts

@@ -28,10 +28,17 @@ export interface LdapGroupSyncSettings {
   ldapGroupDescriptionAttribute?: string
 }
 
+export type ExternalUserInfo = {
+  id: string, // external user id
+  username: string,
+  name: string,
+  email?: string,
+}
+
 // Data structure to express the tree structure of external groups, before converting to ExternalUserGroup model
 export interface ExternalUserGroupTreeNode {
   id: string
-  externalUserIds: string[]
+  userInfos: ExternalUserInfo[]
   childGroupNodes: ExternalUserGroupTreeNode[]
   name: string
   description?: string

+ 44 - 13
apps/app/src/server/service/external-group/external-user-group-sync-service.ts

@@ -1,17 +1,30 @@
-import { ExternalGroupProviderType, ExternalUserGroupTreeNode, IExternalUserGroupHasId } from '~/interfaces/external-user-group';
+import {
+  ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo, IExternalUserGroupHasId,
+} from '~/interfaces/external-user-group';
 import { IUserHasId } from '~/interfaces/user';
 import ExternalUserGroup from '~/server/models/external-user-group';
 import ExternalUserGroupRelation from '~/server/models/external-user-group-relation';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 
 import { configManager } from '../config-manager';
+import ExternalAccountService from '../external-account';
 
 abstract class ExternalUserGroupSyncService {
 
-  provider: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap)
+  groupProviderType: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap, keycloak)
 
-  constructor(provider: ExternalGroupProviderType) {
-    this.provider = provider;
+  authProviderType: string; // auth provider type (e.g: ldap, oidc)
+
+  externalAccountService: ExternalAccountService;
+
+  crowi: any;
+
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  constructor(crowi: any, groupProviderType: ExternalGroupProviderType, authProviderType: string) {
+    this.groupProviderType = groupProviderType;
+    this.authProviderType = authProviderType;
+    this.crowi = crowi;
+    this.externalAccountService = new ExternalAccountService(crowi);
   }
 
   /** External user group tree sync method
@@ -36,9 +49,9 @@ abstract class ExternalUserGroupSyncService {
       return syncNode(root);
     }));
 
-    const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.provider}:preserveDeletedGroups`);
+    const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
     if (!preserveDeletedLdapGroups) {
-      await ExternalUserGroup.deleteMany({ _id: { $nin: existingExternalUserGroupIds }, provider: this.provider });
+      await ExternalUserGroup.deleteMany({ _id: { $nin: existingExternalUserGroupIds }, groupProviderType: this.groupProviderType });
     }
   }
 
@@ -52,11 +65,11 @@ abstract class ExternalUserGroupSyncService {
   */
   async createUpdateExternalUserGroup(node: ExternalUserGroupTreeNode, parentId?: string): Promise<IExternalUserGroupHasId> {
     const externalUserGroup = await ExternalUserGroup.findAndUpdateOrCreateGroup(
-      node.name, node.description, node.id, this.provider, parentId,
+      node.name, node.description, node.id, this.groupProviderType, parentId,
     );
-    await Promise.all(node.externalUserIds.map((externalUserId) => {
+    await Promise.all(node.userInfos.map((userInfo) => {
       return (async() => {
-        const user = await this.getMemberUser(externalUserId);
+        const user = await this.getMemberUser(userInfo);
 
         if (user != null) {
           const userGroups = await ExternalUserGroup.findGroupsWithAncestorsRecursively(externalUserGroup);
@@ -76,20 +89,38 @@ abstract class ExternalUserGroupSyncService {
   }
 
   /** Method to get group member GROWI user
-   * 1. Execute search on external app/server for user info using externalUserId
+   * 1. If externalUserInfo is an id, execute search on external app/server for user info. If it is full user info, use it as it is in 2.
    * 2. Search for GROWI user based on user info of 1, and return user
    *   - if autoGenerateUserOnHogeGroupSync is true and GROWI user is not found, create new GROWI user
-   * @param {string} externalUserId Search LDAP server using this identifier (DN or UID)
+   * @param {ExternalUserInfo} externalUserInfo Search external app/server using this identifier
    * @returns {Promise<IUserHasId | null>} User when found or created, null when neither
    */
-  abstract getMemberUser(externalUserId: string): Promise<IUserHasId | null>
+  async getMemberUser(userInfo: ExternalUserInfo): Promise<IUserHasId | null> {
+    const autoGenerateUserOnGroupSync = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:autoGenerateUserOnGroupSync`);
+
+    const getExternalAccount = async() => {
+      if (autoGenerateUserOnGroupSync) {
+        return this.externalAccountService.getOrCreateUser(userInfo, this.authProviderType);
+      }
+      return this.crowi.models.ExternalAccount
+        .findOne({ providerType: this.groupProviderType, accountId: userInfo.id });
+    };
+
+    const externalAccount = await getExternalAccount();
+
+    if (externalAccount != null) {
+      return externalAccount.getPopulatedUser();
+    }
+    return null;
+  }
 
   /** Method to generate external group tree structure
    * 1. Fetch user group info from external app/server
    * 2. Convert each group tree structure to ExternalUserGroupTreeNode
+   *   - Store the full user info in externalUserInfos if possible. Else just store the id and leave the user info fetching to getMemberUser.
    * 3. Return the root node of each tree
   */
-  abstract generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]>;
+  abstract generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]>
 
 }
 

+ 51 - 72
apps/app/src/server/service/external-group/ldap-user-group-sync-service.ts

@@ -1,8 +1,6 @@
-import { ExternalGroupProviderType, ExternalUserGroupTreeNode } from '~/interfaces/external-user-group';
-import { IUserHasId } from '~/interfaces/user';
+import { ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo } from '~/interfaces/external-user-group';
 
 import { configManager } from '../config-manager';
-import ExternalAccountService from '../external-account';
 import LdapService, { SearchResultEntry } from '../ldap';
 
 import ExternalUserGroupSyncService from './external-user-group-sync-service';
@@ -11,50 +9,10 @@ class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
 
   ldapService: LdapService;
 
-  externalAccountService: ExternalAccountService;
-
-  crowi: any;
-
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   constructor(crowi: any, userBindUsername?: string, userBindPassword?: string) {
-    super(ExternalGroupProviderType.ldap);
-    this.crowi = crowi;
+    super(crowi, ExternalGroupProviderType.ldap, 'ldap');
     this.ldapService = new LdapService(userBindUsername, userBindPassword);
-    this.externalAccountService = new ExternalAccountService(crowi);
-  }
-
-  async getMemberUser(externalUserId: string): Promise<IUserHasId | null> {
-    const groupMembershipAttributeType = configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType');
-
-    const getUser = async() => {
-      if (groupMembershipAttributeType === 'DN') {
-        return this.ldapService.search(undefined, externalUserId, 'base');
-      }
-      if (groupMembershipAttributeType === 'UID') {
-        return this.ldapService.search(`(uid=${externalUserId})`, undefined);
-      }
-    };
-
-    let userEntryArr: SearchResultEntry[] | undefined;
-    try {
-      userEntryArr = await getUser();
-    }
-    catch (e) {
-      throw Error('external_user_group.ldap.user_search_failed');
-    }
-
-    if (userEntryArr != null && userEntryArr.length > 0) {
-      const userEntry = userEntryArr[0];
-      const uid = this.ldapService.getStringValFromSearchResultEntry(userEntry, 'uid');
-      if (uid != null) {
-        const externalAccount = await this.getExternalAccount(uid, userEntry);
-        if (externalAccount != null) {
-          return externalAccount.getPopulatedUser();
-        }
-      }
-    }
-
-    return null;
   }
 
   async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
@@ -77,31 +35,34 @@ class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
       // so filter values of groupChildGroupAttribute to ones that include groupBase
       return this.ldapService.getArrayValFromSearchResultEntry(groupEntry, groupChildGroupAttribute).filter(attr => attr.includes(groupBase));
     };
-    const getExternalUserIdsFromGroupEntry = (groupEntry: SearchResultEntry) => {
+    const getUserIdsFromGroupEntry = (groupEntry: SearchResultEntry) => {
       // groupChildGroupAttribute and groupMembershipAttribute may be the same,
       // so filter values of groupMembershipAttribute to ones that does not include groupBase
       return this.ldapService.getArrayValFromSearchResultEntry(groupEntry, groupMembershipAttribute).filter(attr => !attr.includes(groupBase));
     };
 
-    const convert = (entry: SearchResultEntry, converted: string[]): ExternalUserGroupTreeNode | null => {
+    const convert = async(entry: SearchResultEntry, converted: string[]): Promise<ExternalUserGroupTreeNode | null> => {
       if (converted.includes(entry.objectName)) {
         throw Error('external_user_group.ldap.circular_reference');
       }
       converted.push(entry.objectName);
 
-      const externalUserIds = getExternalUserIdsFromGroupEntry(entry);
+      const userIds = getUserIdsFromGroupEntry(entry);
+      const userInfos = (await Promise.all(userIds.map((id) => {
+        return this.getUserInfo(id);
+      }))).filter((info): info is NonNullable<ExternalUserInfo> => info != null);
       const name = this.ldapService.getStringValFromSearchResultEntry(entry, groupNameAttribute);
       const description = this.ldapService.getStringValFromSearchResultEntry(entry, groupDescriptionAttribute);
       const childGroupDNs = getChildGroupDnsFromGroupEntry(entry);
 
-      const childGroupNodes: ExternalUserGroupTreeNode[] = childGroupDNs.map((dn) => {
+      const childGroupNodes: ExternalUserGroupTreeNode[] = (await Promise.all(childGroupDNs.map((dn) => {
         const childEntry = groupEntries.find(ge => ge.objectName === dn);
         return childEntry != null ? convert(childEntry, converted) : null;
-      }).filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
+      }))).filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
 
       return name != null ? {
         id: entry.objectName,
-        externalUserIds,
+        userInfos,
         childGroupNodes,
         name,
         description,
@@ -118,32 +79,50 @@ class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
       return !allChildGroupDNs.has(entry.objectName);
     });
 
-    return rootEntries.map(entry => convert(entry, [])).filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
+    return (await Promise.all(rootEntries.map(entry => convert(entry, [])))).filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
   }
 
-  private async getExternalAccount(uid: string, userEntry: SearchResultEntry) {
-    const autoGenerateUserOnLDAPGroupSync = configManager?.getConfig('crowi', 'external-user-group:ldap:autoGenerateUserOnGroupSync');
-
-    if (autoGenerateUserOnLDAPGroupSync) {
-      const attrMapUsername = this.crowi.passportService.getLdapAttrNameMappedToUsername();
-      const attrMapName = this.crowi.passportService.getLdapAttrNameMappedToName();
-      const attrMapMail = this.crowi.passportService.getLdapAttrNameMappedToMail();
-      const usernameToBeRegistered = attrMapUsername === 'uid' ? uid : this.ldapService.getStringValFromSearchResultEntry(userEntry, attrMapUsername);
-      const nameToBeRegistered = this.ldapService.getStringValFromSearchResultEntry(userEntry, attrMapName);
-      const mailToBeRegistered = this.ldapService.getStringValFromSearchResultEntry(userEntry, attrMapMail);
-
-      const userInfo = {
-        id: uid,
-        username: usernameToBeRegistered,
-        name: nameToBeRegistered,
-        email: mailToBeRegistered,
-      };
-
-      return this.externalAccountService.getOrCreateUser(userInfo, 'ldap');
+  private async getUserInfo(userId: string): Promise<ExternalUserInfo | null> {
+    const groupMembershipAttributeType = configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType');
+    const attrMapUsername = this.crowi.passportService.getLdapAttrNameMappedToUsername();
+    const attrMapName = this.crowi.passportService.getLdapAttrNameMappedToName();
+    const attrMapMail = this.crowi.passportService.getLdapAttrNameMappedToMail();
+
+    // get full user info from LDAP server using externalUserInfo (DN or UID)
+    const getUserEntries = async() => {
+      if (groupMembershipAttributeType === 'DN') {
+        return this.ldapService.search(undefined, userId, 'base');
+      }
+      if (groupMembershipAttributeType === 'UID') {
+        return this.ldapService.search(`(uid=${userId})`, undefined);
+      }
+    };
+
+    let userEntries: SearchResultEntry[] | undefined;
+    try {
+      userEntries = await getUserEntries();
+    }
+    catch (e) {
+      throw Error('external_user_group.ldap.user_search_failed');
     }
 
-    return this.crowi.models.ExternalAccount
-      .findOne({ providerType: 'ldap', accountId: uid });
+    if (userEntries != null && userEntries.length > 0) {
+      const userEntry = userEntries[0];
+      const uid = this.ldapService.getStringValFromSearchResultEntry(userEntry, 'uid');
+      if (uid != null) {
+        const usernameToBeRegistered = attrMapUsername === 'uid' ? uid : this.ldapService.getStringValFromSearchResultEntry(userEntry, attrMapUsername);
+        const nameToBeRegistered = this.ldapService.getStringValFromSearchResultEntry(userEntry, attrMapName);
+        const mailToBeRegistered = this.ldapService.getStringValFromSearchResultEntry(userEntry, attrMapMail);
+
+        return usernameToBeRegistered != null ? {
+          id: uid,
+          username: usernameToBeRegistered || '',
+          name: nameToBeRegistered || '',
+          email: mailToBeRegistered,
+        } : null;
+      }
+    }
+    return null;
   }
 
 }