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

notify all servers about isExecutingSync

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

+ 6 - 9
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -17,9 +17,6 @@ import { configManager } from '~/server/service/config-manager';
 import UserGroupService from '~/server/service/user-group';
 import UserGroupService from '~/server/service/user-group';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
-import { keycloakUserGroupSyncService } from '../../service/keycloak-user-group-sync';
-import { ldapUserGroupSyncService } from '../../service/ldap-user-group-sync';
-
 const logger = loggerFactory('growi:routes:apiv3:external-user-group');
 const logger = loggerFactory('growi:routes:apiv3:external-user-group');
 
 
 const router = Router();
 const router = Router();
@@ -28,10 +25,6 @@ interface AuthorizedRequest extends Request {
   user?: any
   user?: any
 }
 }
 
 
-const isExecutingSync = () => {
-  return ldapUserGroupSyncService?.isExecutingSync || keycloakUserGroupSyncService?.isExecutingSync || false;
-};
-
 module.exports = (crowi: Crowi): Router => {
 module.exports = (crowi: Crowi): Router => {
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
   const adminRequired = require('~/server/middlewares/admin-required')(crowi);
@@ -39,6 +32,10 @@ module.exports = (crowi: Crowi): Router => {
 
 
   const activityEvent = crowi.event('activity');
   const activityEvent = crowi.event('activity');
 
 
+  const isExecutingSync = () => {
+    return crowi.ldapUserGroupSyncService?.isExecutingSync || crowi.keycloakUserGroupSyncService?.isExecutingSync || false;
+  };
+
   const validators = {
   const validators = {
     ldapSyncSettings: [
     ldapSyncSettings: [
       body('ldapGroupSearchBase').optional({ nullable: true }).isString(),
       body('ldapGroupSearchBase').optional({ nullable: true }).isString(),
@@ -318,7 +315,7 @@ module.exports = (crowi: Crowi): Router => {
     }
     }
 
 
     // Do not await for sync to finish. Result (completed, failed) will be notified to the client by socket-io.
     // Do not await for sync to finish. Result (completed, failed) will be notified to the client by socket-io.
-    ldapUserGroupSyncService?.syncExternalUserGroups({ userBindUsername: req.user.name, userBindPassword: req.body.password });
+    crowi.ldapUserGroupSyncService?.syncExternalUserGroups({ userBindUsername: req.user.name, userBindPassword: req.body.password });
 
 
     return res.apiv3({}, 202);
     return res.apiv3({}, 202);
   });
   });
@@ -359,7 +356,7 @@ module.exports = (crowi: Crowi): Router => {
     }
     }
 
 
     // Do not await for sync to finish. Result (completed, failed) will be notified to the client by socket-io.
     // Do not await for sync to finish. Result (completed, failed) will be notified to the client by socket-io.
-    keycloakUserGroupSyncService?.syncExternalUserGroups(authProviderType);
+    crowi.keycloakUserGroupSyncService?.syncExternalUserGroups(authProviderType);
 
 
     return res.apiv3({}, 202);
     return res.apiv3({}, 202);
   });
   });

+ 48 - 4
apps/app/src/features/external-user-group/server/service/external-user-group-sync.ts

@@ -2,6 +2,9 @@ import type { IUserHasId } from '@growi/core';
 
 
 import { SocketEventName } from '~/interfaces/websocket';
 import { SocketEventName } from '~/interfaces/websocket';
 import ExternalAccount from '~/server/models/external-account';
 import ExternalAccount from '~/server/models/external-account';
+import S2sMessage from '~/server/models/vo/s2s-message';
+import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
+import { S2sMessageHandlable } from '~/server/service/s2s-messaging/handlable';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import { excludeTestIdsFromTargetIds } from '~/server/util/compare-objectId';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
@@ -22,8 +25,14 @@ const logger = loggerFactory('growi:service:external-user-group-sync-service');
 const TREES_BATCH_SIZE = 10;
 const TREES_BATCH_SIZE = 10;
 const USERS_BATCH_SIZE = 30;
 const USERS_BATCH_SIZE = 30;
 
 
+class ExternalUserGroupSyncS2sMessage extends S2sMessage {
+
+  isExecutingSync: boolean;
+
+}
+
 // SyncParamsType: type of params to propagate and use on executing syncExternalUserGroups
 // SyncParamsType: type of params to propagate and use on executing syncExternalUserGroups
-abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
+abstract class ExternalUserGroupSyncService<SyncParamsType = any> implements S2sMessageHandlable {
 
 
   groupProviderType: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap, keycloak)
   groupProviderType: ExternalGroupProviderType; // name of external service that contains user group info (e.g: ldap, keycloak)
 
 
@@ -31,14 +40,49 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
 
 
   socketIoService: any;
   socketIoService: any;
 
 
+  s2sMessagingService: S2sMessagingService | null;
+
   isExecutingSync = false;
   isExecutingSync = false;
 
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(groupProviderType: ExternalGroupProviderType, socketIoService) {
+  constructor(groupProviderType: ExternalGroupProviderType, s2sMessagingService: S2sMessagingService | null, socketIoService) {
     this.groupProviderType = groupProviderType;
     this.groupProviderType = groupProviderType;
+    this.s2sMessagingService = s2sMessagingService;
     this.socketIoService = socketIoService;
     this.socketIoService = socketIoService;
   }
   }
 
 
+  /**
+   * @inheritdoc
+   */
+  shouldHandleS2sMessage(s2sMessage: ExternalUserGroupSyncS2sMessage): boolean {
+    return s2sMessage.eventName === 'switchExternalUserGroupExecSyncStatus';
+  }
+
+  /**
+   * @inheritdoc
+   */
+  async handleS2sMessage(s2sMessage: ExternalUserGroupSyncS2sMessage): Promise<void> {
+    logger.info(`Set isExecutingSync to ${s2sMessage.isExecutingSync} by pubsub notification`);
+    this.isExecutingSync = s2sMessage.isExecutingSync;
+  }
+
+  async switchIsExecutingSync(isExecutingSync: boolean): Promise<void> {
+    this.isExecutingSync = isExecutingSync;
+
+    if (this.s2sMessagingService != null) {
+      const s2sMessage = new ExternalUserGroupSyncS2sMessage('switchExternalUserGroupExecSyncStatus', {
+        isExecutingSync,
+      });
+
+      try {
+        await this.s2sMessagingService.publish(s2sMessage);
+      }
+      catch (e) {
+        logger.error('Failed to publish update message with S2sMessagingService: ', e.message);
+      }
+    }
+  }
+
   /** External user group tree sync method
   /** External user group tree sync method
    * 1. Generate external user group tree
    * 1. Generate external user group tree
    * 2. Use createUpdateExternalUserGroup on each node in the tree using DFS
    * 2. Use createUpdateExternalUserGroup on each node in the tree using DFS
@@ -47,7 +91,7 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
   async syncExternalUserGroups(params?: SyncParamsType): Promise<void> {
   async syncExternalUserGroups(params?: SyncParamsType): Promise<void> {
     if (this.authProviderType == null) throw new Error('auth provider type is not set');
     if (this.authProviderType == null) throw new Error('auth provider type is not set');
     if (this.isExecutingSync) throw new Error('External user group sync is already being executed');
     if (this.isExecutingSync) throw new Error('External user group sync is already being executed');
-    this.isExecutingSync = true;
+    await this.switchIsExecutingSync(true);
 
 
     const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
     const preserveDeletedLdapGroups: boolean = configManager?.getConfig('crowi', `external-user-group:${this.groupProviderType}:preserveDeletedGroups`);
     const existingExternalUserGroupIds: string[] = [];
     const existingExternalUserGroupIds: string[] = [];
@@ -87,7 +131,7 @@ abstract class ExternalUserGroupSyncService<SyncParamsType = any> {
       socket?.emit(SocketEventName.externalUserGroup[this.groupProviderType].GroupSyncFailed);
       socket?.emit(SocketEventName.externalUserGroup[this.groupProviderType].GroupSyncFailed);
     }
     }
     finally {
     finally {
-      this.isExecutingSync = false;
+      await this.switchIsExecutingSync(false);
     }
     }
   }
   }
 
 

+ 5 - 10
apps/app/src/features/external-user-group/server/service/keycloak-user-group-sync.ts

@@ -1,6 +1,7 @@
 import { GroupRepresentation, KeycloakAdminClient, UserRepresentation } from '@s3pweb/keycloak-admin-client-cjs';
 import { GroupRepresentation, KeycloakAdminClient, UserRepresentation } from '@s3pweb/keycloak-admin-client-cjs';
 
 
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
+import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 
 import { ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo } from '../../interfaces/external-user-group';
 import { ExternalGroupProviderType, ExternalUserGroupTreeNode, ExternalUserInfo } from '../../interfaces/external-user-group';
@@ -12,7 +13,7 @@ import ExternalUserGroupSyncService from './external-user-group-sync';
 // O(TREES_BATCH_SIZE * d)
 // O(TREES_BATCH_SIZE * d)
 const TREES_BATCH_SIZE = 10;
 const TREES_BATCH_SIZE = 10;
 
 
-class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
+export class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
 
 
   kcAdminClient: KeycloakAdminClient;
   kcAdminClient: KeycloakAdminClient;
 
 
@@ -20,13 +21,14 @@ class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
 
 
   groupDescriptionAttribute: string; // attribute to map to group description
   groupDescriptionAttribute: string; // attribute to map to group description
 
 
-  constructor(socketIoService) {
+  // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
+  constructor(s2sMessagingService: S2sMessagingService, socketIoService) {
     const kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
     const kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
     const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
     const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
     const kcGroupSyncClientRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm');
     const kcGroupSyncClientRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm');
     const kcGroupDescriptionAttribute = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute');
     const kcGroupDescriptionAttribute = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute');
 
 
-    super(ExternalGroupProviderType.keycloak, socketIoService);
+    super(ExternalGroupProviderType.keycloak, s2sMessagingService, socketIoService);
     this.kcAdminClient = new KeycloakAdminClient({ baseUrl: kcHost, realmName: kcGroupSyncClientRealm });
     this.kcAdminClient = new KeycloakAdminClient({ baseUrl: kcHost, realmName: kcGroupSyncClientRealm });
     this.realm = kcGroupRealm;
     this.realm = kcGroupRealm;
     this.groupDescriptionAttribute = kcGroupDescriptionAttribute;
     this.groupDescriptionAttribute = kcGroupDescriptionAttribute;
@@ -124,10 +126,3 @@ class KeycloakUserGroupSyncService extends ExternalUserGroupSyncService {
   }
   }
 
 
 }
 }
-
-// eslint-disable-next-line import/no-mutable-exports
-export let keycloakUserGroupSyncService: KeycloakUserGroupSyncService | undefined; // singleton instance
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export function instanciate(socketIoService): void {
-  keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(socketIoService);
-}

+ 4 - 10
apps/app/src/features/external-user-group/server/service/ldap-user-group-sync.ts

@@ -1,6 +1,7 @@
 import { configManager } from '~/server/service/config-manager';
 import { configManager } from '~/server/service/config-manager';
 import LdapService, { SearchResultEntry } from '~/server/service/ldap';
 import LdapService, { SearchResultEntry } from '~/server/service/ldap';
 import PassportService from '~/server/service/passport';
 import PassportService from '~/server/service/passport';
+import { S2sMessagingService } from '~/server/service/s2s-messaging/base';
 import { batchProcessPromiseAll } from '~/utils/promise';
 import { batchProcessPromiseAll } from '~/utils/promise';
 
 
 import {
 import {
@@ -17,15 +18,15 @@ const USERS_BATCH_SIZE = 30;
 
 
 type SyncParamsType = { userBindUsername: string, userBindPassword: string };
 type SyncParamsType = { userBindUsername: string, userBindPassword: string };
 
 
-class LdapUserGroupSyncService extends ExternalUserGroupSyncService<SyncParamsType> {
+export class LdapUserGroupSyncService extends ExternalUserGroupSyncService<SyncParamsType> {
 
 
   passportService: PassportService;
   passportService: PassportService;
 
 
   ldapService: LdapService;
   ldapService: LdapService;
 
 
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
   // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-  constructor(passportService, socketIoService) {
-    super(ExternalGroupProviderType.ldap, socketIoService);
+  constructor(passportService: PassportService, s2sMessagingService: S2sMessagingService, socketIoService) {
+    super(ExternalGroupProviderType.ldap, s2sMessagingService, socketIoService);
     this.authProviderType = 'ldap';
     this.authProviderType = 'ldap';
     this.passportService = passportService;
     this.passportService = passportService;
     this.ldapService = new LdapService();
     this.ldapService = new LdapService();
@@ -140,10 +141,3 @@ class LdapUserGroupSyncService extends ExternalUserGroupSyncService<SyncParamsTy
   }
   }
 
 
 }
 }
-
-// eslint-disable-next-line import/no-mutable-exports
-export let ldapUserGroupSyncService: LdapUserGroupSyncService | undefined; // singleton instance
-// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-export function instanciate(passportService: PassportService, socketIoService): void {
-  ldapUserGroupSyncService = new LdapUserGroupSyncService(passportService, socketIoService);
-}

+ 5 - 5
apps/app/src/server/crowi/index.js

@@ -10,8 +10,8 @@ import next from 'next';
 
 
 import pkg from '^/package.json';
 import pkg from '^/package.json';
 
 
-import { instanciate as instanciateKeycloakUserGroupSyncService } from '~/features/external-user-group/server/service/keycloak-user-group-sync';
-import { instanciate as instanciateLdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
+import { KeycloakUserGroupSyncService } from '~/features/external-user-group/server/service/keycloak-user-group-sync';
+import { LdapUserGroupSyncService } from '~/features/external-user-group/server/service/ldap-user-group-sync';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import QuestionnaireService from '~/features/questionnaire/server/service/questionnaire';
 import QuestionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
 import QuestionnaireCronService from '~/features/questionnaire/server/service/questionnaire-cron';
 import CdnResourcesService from '~/services/cdn-resources-service';
 import CdnResourcesService from '~/services/cdn-resources-service';
@@ -791,10 +791,10 @@ Crowi.prototype.setupExternalAccountService = function() {
   instanciateExternalAccountService(this.passportService);
   instanciateExternalAccountService(this.passportService);
 };
 };
 
 
-// execute after setupPassport and socketIoService
+// execute after setupPassport, s2sMessagingService, socketIoService
 Crowi.prototype.setupExternalUserGroupSyncService = function() {
 Crowi.prototype.setupExternalUserGroupSyncService = function() {
-  instanciateLdapUserGroupSyncService(this.passportService, this.socketIoService);
-  instanciateKeycloakUserGroupSyncService(this.socketIoService);
+  this.ldapUserGroupSyncService = new LdapUserGroupSyncService(this.passportService, this.s2sMessagingService, this.socketIoService);
+  this.keycloakUserGroupSyncService = new KeycloakUserGroupSyncService(this.s2sMessagingService, this.socketIoService);
 };
 };
 
 
 export default Crowi;
 export default Crowi;

+ 3 - 3
apps/app/test/integration/service/external-user-group-sync.test.ts

@@ -16,8 +16,8 @@ import { getInstance } from '../setup-crowi';
 // dummy class to implement generateExternalUserGroupTrees which returns test data
 // dummy class to implement generateExternalUserGroupTrees which returns test data
 class TestExternalUserGroupSyncService extends ExternalUserGroupSyncService {
 class TestExternalUserGroupSyncService extends ExternalUserGroupSyncService {
 
 
-  constructor(socketIoService) {
-    super('ldap', socketIoService);
+  constructor(s2sMessagingService, socketIoService) {
+    super('ldap', s2sMessagingService, socketIoService);
     this.authProviderType = ExternalGroupProviderType.ldap;
     this.authProviderType = ExternalGroupProviderType.ldap;
   }
   }
 
 
@@ -77,7 +77,7 @@ class TestExternalUserGroupSyncService extends ExternalUserGroupSyncService {
 
 
 }
 }
 
 
-const testService = new TestExternalUserGroupSyncService(null);
+const testService = new TestExternalUserGroupSyncService(null, null);
 
 
 const checkGroup = (group: IExternalUserGroupHasId, expected: Omit<IExternalUserGroup, 'createdAt'>) => {
 const checkGroup = (group: IExternalUserGroupHasId, expected: Omit<IExternalUserGroup, 'createdAt'>) => {
   const actual = {
   const actual = {

+ 3 - 2
apps/app/test/integration/service/ldap-user-group-sync.test.ts

@@ -1,6 +1,6 @@
 import ldap, { Client } from 'ldapjs';
 import ldap, { Client } from 'ldapjs';
 
 
-import { ldapUserGroupSyncService, instanciate } from '../../../src/features/external-user-group/server/service/ldap-user-group-sync';
+import { LdapUserGroupSyncService } from '../../../src/features/external-user-group/server/service/ldap-user-group-sync';
 import { configManager } from '../../../src/server/service/config-manager';
 import { configManager } from '../../../src/server/service/config-manager';
 import LdapService from '../../../src/server/service/ldap';
 import LdapService from '../../../src/server/service/ldap';
 import PassportService from '../../../src/server/service/passport';
 import PassportService from '../../../src/server/service/passport';
@@ -8,6 +8,7 @@ import { getInstance } from '../setup-crowi';
 
 
 describe('LdapUserGroupSyncService.generateExternalUserGroupTrees', () => {
 describe('LdapUserGroupSyncService.generateExternalUserGroupTrees', () => {
   let crowi;
   let crowi;
+  let ldapUserGroupSyncService: LdapUserGroupSyncService;
 
 
   const configParams = {
   const configParams = {
     'security:passport-ldap:attrMapName': 'name',
     'security:passport-ldap:attrMapName': 'name',
@@ -35,7 +36,7 @@ describe('LdapUserGroupSyncService.generateExternalUserGroupTrees', () => {
     mockLdapCreateClient.mockImplementation(() => { return {} as Client });
     mockLdapCreateClient.mockImplementation(() => { return {} as Client });
 
 
     const passportService = new PassportService(crowi);
     const passportService = new PassportService(crowi);
-    instanciate(passportService, null);
+    ldapUserGroupSyncService = new LdapUserGroupSyncService(passportService, null, null);
   });
   });
 
 
   describe('When there is no circular reference in group tree', () => {
   describe('When there is no circular reference in group tree', () => {