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

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