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

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