external-user-group-sync.test.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import type { IUserHasId } from '@growi/core';
  2. import mongoose, { Types } from 'mongoose';
  3. import {
  4. ExternalGroupProviderType,
  5. type ExternalUserGroupTreeNode,
  6. type IExternalUserGroup,
  7. type IExternalUserGroupHasId,
  8. } from '../../../src/features/external-user-group/interfaces/external-user-group';
  9. import ExternalUserGroup from '../../../src/features/external-user-group/server/models/external-user-group';
  10. import ExternalUserGroupRelation from '../../../src/features/external-user-group/server/models/external-user-group-relation';
  11. import ExternalUserGroupSyncService from '../../../src/features/external-user-group/server/service/external-user-group-sync';
  12. import type Crowi from '../../../src/server/crowi';
  13. import ExternalAccount from '../../../src/server/models/external-account';
  14. import { configManager } from '../../../src/server/service/config-manager';
  15. import instanciateExternalAccountService from '../../../src/server/service/external-account';
  16. import PassportService from '../../../src/server/service/passport';
  17. import { getInstance } from '../setup-crowi';
  18. // dummy class to implement generateExternalUserGroupTrees which returns test data
  19. class TestExternalUserGroupSyncService extends ExternalUserGroupSyncService {
  20. constructor(s2sMessagingService, socketIoService) {
  21. super('ldap', s2sMessagingService, socketIoService);
  22. this.authProviderType = ExternalGroupProviderType.ldap;
  23. }
  24. async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
  25. const childNode: ExternalUserGroupTreeNode = {
  26. id: 'cn=childGroup,ou=groups,dc=example,dc=org',
  27. userInfos: [
  28. {
  29. id: 'childGroupUser',
  30. username: 'childGroupUser',
  31. name: 'Child Group User',
  32. email: 'user@childgroup.com',
  33. },
  34. ],
  35. childGroupNodes: [],
  36. name: 'childGroup',
  37. description: 'this is a child group',
  38. };
  39. const parentNode: ExternalUserGroupTreeNode = {
  40. id: 'cn=parentGroup,ou=groups,dc=example,dc=org',
  41. // name is undefined
  42. userInfos: [
  43. {
  44. id: 'parentGroupUser',
  45. username: 'parentGroupUser',
  46. email: 'user@parentgroup.com',
  47. },
  48. ],
  49. childGroupNodes: [childNode],
  50. name: 'parentGroup',
  51. description: 'this is a parent group',
  52. };
  53. const grandParentNode: ExternalUserGroupTreeNode = {
  54. id: 'cn=grandParentGroup,ou=groups,dc=example,dc=org',
  55. // email is undefined
  56. userInfos: [
  57. {
  58. id: 'grandParentGroupUser',
  59. username: 'grandParentGroupUser',
  60. name: 'Grand Parent Group User',
  61. },
  62. ],
  63. childGroupNodes: [parentNode],
  64. name: 'grandParentGroup',
  65. description: 'this is a grand parent group',
  66. };
  67. const previouslySyncedNode: ExternalUserGroupTreeNode = {
  68. id: 'cn=previouslySyncedGroup,ou=groups,dc=example,dc=org',
  69. userInfos: [
  70. {
  71. id: 'previouslySyncedGroupUser',
  72. username: 'previouslySyncedGroupUser',
  73. name: 'Root Group User',
  74. email: 'user@previouslySyncedgroup.com',
  75. },
  76. ],
  77. childGroupNodes: [],
  78. name: 'previouslySyncedGroup',
  79. description: 'this is a previouslySynced group',
  80. };
  81. return [grandParentNode, previouslySyncedNode];
  82. }
  83. }
  84. const testService = new TestExternalUserGroupSyncService(null, null);
  85. const checkGroup = (
  86. group: IExternalUserGroupHasId,
  87. expected: Omit<IExternalUserGroup, 'createdAt'>,
  88. ) => {
  89. const actual = {
  90. name: group.name,
  91. parent: group.parent,
  92. description: group.description,
  93. externalId: group.externalId,
  94. provider: group.provider,
  95. };
  96. expect(actual).toStrictEqual(expected);
  97. };
  98. const checkSync = async (autoGenerateUserOnGroupSync = true) => {
  99. const grandParentGroup = await ExternalUserGroup.findOne({
  100. name: 'grandParentGroup',
  101. });
  102. checkGroup(grandParentGroup, {
  103. externalId: 'cn=grandParentGroup,ou=groups,dc=example,dc=org',
  104. name: 'grandParentGroup',
  105. description: 'this is a grand parent group',
  106. provider: 'ldap',
  107. parent: null,
  108. });
  109. const parentGroup = await ExternalUserGroup.findOne({ name: 'parentGroup' });
  110. checkGroup(parentGroup, {
  111. externalId: 'cn=parentGroup,ou=groups,dc=example,dc=org',
  112. name: 'parentGroup',
  113. description: 'this is a parent group',
  114. provider: 'ldap',
  115. parent: grandParentGroup._id,
  116. });
  117. const childGroup = await ExternalUserGroup.findOne({ name: 'childGroup' });
  118. checkGroup(childGroup, {
  119. externalId: 'cn=childGroup,ou=groups,dc=example,dc=org',
  120. name: 'childGroup',
  121. description: 'this is a child group',
  122. provider: 'ldap',
  123. parent: parentGroup._id,
  124. });
  125. const previouslySyncedGroup = await ExternalUserGroup.findOne({
  126. name: 'previouslySyncedGroup',
  127. });
  128. checkGroup(previouslySyncedGroup, {
  129. externalId: 'cn=previouslySyncedGroup,ou=groups,dc=example,dc=org',
  130. name: 'previouslySyncedGroup',
  131. description: 'this is a previouslySynced group',
  132. provider: 'ldap',
  133. parent: null,
  134. });
  135. const grandParentGroupRelations = await ExternalUserGroupRelation.find({
  136. relatedGroup: grandParentGroup._id,
  137. });
  138. const parentGroupRelations = await ExternalUserGroupRelation.find({
  139. relatedGroup: parentGroup._id,
  140. });
  141. const childGroupRelations = await ExternalUserGroupRelation.find({
  142. relatedGroup: childGroup._id,
  143. });
  144. const previouslySyncedGroupRelations = await ExternalUserGroupRelation.find({
  145. relatedGroup: previouslySyncedGroup._id,
  146. });
  147. if (autoGenerateUserOnGroupSync) {
  148. expect(grandParentGroupRelations.length).toBe(3);
  149. const populatedGrandParentGroupRelations = await Promise.all(
  150. grandParentGroupRelations.map((relation) => {
  151. return relation.populate<{ relatedUser: IUserHasId }>('relatedUser');
  152. }),
  153. );
  154. expect(populatedGrandParentGroupRelations[0].relatedUser.username).toBe(
  155. 'grandParentGroupUser',
  156. );
  157. expect(populatedGrandParentGroupRelations[1].relatedUser.username).toBe(
  158. 'parentGroupUser',
  159. );
  160. expect(populatedGrandParentGroupRelations[2].relatedUser.username).toBe(
  161. 'childGroupUser',
  162. );
  163. expect(parentGroupRelations.length).toBe(2);
  164. const populatedParentGroupRelations = await Promise.all(
  165. parentGroupRelations.map((relation) => {
  166. return relation.populate<{ relatedUser: IUserHasId }>('relatedUser');
  167. }),
  168. );
  169. expect(populatedParentGroupRelations[0].relatedUser.username).toBe(
  170. 'parentGroupUser',
  171. );
  172. expect(populatedParentGroupRelations[1].relatedUser.username).toBe(
  173. 'childGroupUser',
  174. );
  175. expect(childGroupRelations.length).toBe(1);
  176. const childGroupUser = (
  177. await childGroupRelations[0].populate<{ relatedUser: IUserHasId }>(
  178. 'relatedUser',
  179. )
  180. )?.relatedUser;
  181. expect(childGroupUser?.username).toBe('childGroupUser');
  182. expect(previouslySyncedGroupRelations.length).toBe(1);
  183. const previouslySyncedGroupUser = (
  184. await previouslySyncedGroupRelations[0].populate<{
  185. relatedUser: IUserHasId;
  186. }>('relatedUser')
  187. )?.relatedUser;
  188. expect(previouslySyncedGroupUser?.username).toBe(
  189. 'previouslySyncedGroupUser',
  190. );
  191. const userPages = await mongoose.model('Page').find({
  192. path: {
  193. $in: [
  194. '/user/childGroupUser',
  195. '/user/parentGroupUser',
  196. '/user/grandParentGroupUser',
  197. '/user/previouslySyncedGroupUser',
  198. ],
  199. },
  200. });
  201. expect(userPages.length).toBe(4);
  202. } else {
  203. expect(grandParentGroupRelations.length).toBe(0);
  204. expect(parentGroupRelations.length).toBe(0);
  205. expect(childGroupRelations.length).toBe(0);
  206. expect(previouslySyncedGroupRelations.length).toBe(0);
  207. }
  208. };
  209. describe('ExternalUserGroupSyncService.syncExternalUserGroups', () => {
  210. let crowi: Crowi;
  211. beforeAll(async () => {
  212. crowi = await getInstance();
  213. await configManager.updateConfig('app:isV5Compatible', true);
  214. const passportService = new PassportService(crowi);
  215. instanciateExternalAccountService(passportService);
  216. });
  217. beforeEach(async () => {
  218. await ExternalUserGroup.create({
  219. name: 'nameBeforeEdit',
  220. description: 'this is a description before edit',
  221. externalId: 'cn=previouslySyncedGroup,ou=groups,dc=example,dc=org',
  222. provider: 'ldap',
  223. });
  224. });
  225. afterEach(async () => {
  226. await ExternalUserGroup.deleteMany();
  227. await ExternalUserGroupRelation.deleteMany();
  228. await mongoose.model('User').deleteMany({
  229. username: {
  230. $in: [
  231. 'childGroupUser',
  232. 'parentGroupUser',
  233. 'grandParentGroupUser',
  234. 'previouslySyncedGroupUser',
  235. ],
  236. },
  237. });
  238. await ExternalAccount.deleteMany({
  239. accountId: {
  240. $in: [
  241. 'childGroupUser',
  242. 'parentGroupUser',
  243. 'grandParentGroupUser',
  244. 'previouslySyncedGroupUser',
  245. ],
  246. },
  247. });
  248. await mongoose.model('Page').deleteMany({
  249. path: {
  250. $in: [
  251. '/user/childGroupUser',
  252. '/user/parentGroupUser',
  253. '/user/grandParentGroupUser',
  254. '/user/previouslySyncedGroupUser',
  255. ],
  256. },
  257. });
  258. });
  259. describe('When autoGenerateUserOnGroupSync is true', () => {
  260. const configParams = {
  261. 'external-user-group:ldap:autoGenerateUserOnGroupSync': true,
  262. 'external-user-group:ldap:preserveDeletedGroups': false,
  263. };
  264. beforeAll(async () => {
  265. await configManager.updateConfigs(configParams);
  266. });
  267. it('syncs groups with new users', async () => {
  268. await testService.syncExternalUserGroups();
  269. await checkSync();
  270. });
  271. });
  272. describe('When autoGenerateUserOnGroupSync is false', () => {
  273. const configParams = {
  274. 'external-user-group:ldap:autoGenerateUserOnGroupSync': false,
  275. 'external-user-group:ldap:preserveDeletedGroups': true,
  276. };
  277. beforeAll(async () => {
  278. await configManager.updateConfigs(configParams);
  279. });
  280. it('syncs groups without new users', async () => {
  281. await testService.syncExternalUserGroups();
  282. await checkSync(false);
  283. });
  284. });
  285. describe('When preserveDeletedGroups is false', () => {
  286. const configParams = {
  287. 'external-user-group:ldap:autoGenerateUserOnGroupSync': true,
  288. 'external-user-group:ldap:preserveDeletedGroups': false,
  289. };
  290. beforeAll(async () => {
  291. await configManager.updateConfigs(configParams);
  292. const groupId = new Types.ObjectId();
  293. const userId = new Types.ObjectId();
  294. await ExternalUserGroup.create({
  295. _id: groupId,
  296. name: 'non existent group',
  297. externalId: 'cn=nonExistentGroup,ou=groups,dc=example,dc=org',
  298. provider: 'ldap',
  299. });
  300. await mongoose
  301. .model('User')
  302. .create({ _id: userId, username: 'nonExistentGroupUser' });
  303. await ExternalUserGroupRelation.create({
  304. relatedUser: userId,
  305. relatedGroup: groupId,
  306. });
  307. });
  308. it('syncs groups and deletes groups that do not exist externally', async () => {
  309. await testService.syncExternalUserGroups();
  310. await checkSync();
  311. expect(await ExternalUserGroup.countDocuments()).toBe(4);
  312. expect(await ExternalUserGroupRelation.countDocuments()).toBe(7);
  313. });
  314. });
  315. });