external-user-group.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. import { GroupType } from '@growi/core';
  2. import { ErrorV3 } from '@growi/core/dist/models';
  3. import { Router, Request } from 'express';
  4. import {
  5. body, param, query, validationResult,
  6. } from 'express-validator';
  7. import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
  8. import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
  9. import { SupportedAction } from '~/interfaces/activity';
  10. import Crowi from '~/server/crowi';
  11. import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
  12. import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
  13. import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/user-group-relation-serializer';
  14. import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
  15. import { configManager } from '~/server/service/config-manager';
  16. import UserGroupService from '~/server/service/user-group';
  17. import loggerFactory from '~/utils/logger';
  18. const logger = loggerFactory('growi:routes:apiv3:external-user-group');
  19. const router = Router();
  20. interface AuthorizedRequest extends Request {
  21. user?: any
  22. }
  23. module.exports = (crowi: Crowi): Router => {
  24. const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
  25. const adminRequired = require('~/server/middlewares/admin-required')(crowi);
  26. const addActivity = generateAddActivityMiddleware(crowi);
  27. const activityEvent = crowi.event('activity');
  28. const isExecutingSync = () => {
  29. return crowi.ldapUserGroupSyncService?.syncStatus?.isExecutingSync || crowi.keycloakUserGroupSyncService?.syncStatus?.isExecutingSync || false;
  30. };
  31. const validators = {
  32. ldapSyncSettings: [
  33. body('ldapGroupSearchBase').optional({ nullable: true }).isString(),
  34. body('ldapGroupMembershipAttribute').exists({ checkFalsy: true }).isString(),
  35. body('ldapGroupMembershipAttributeType').exists({ checkFalsy: true }).isString(),
  36. body('ldapGroupChildGroupAttribute').exists({ checkFalsy: true }).isString(),
  37. body('autoGenerateUserOnLdapGroupSync').exists().isBoolean(),
  38. body('preserveDeletedLdapGroups').exists().isBoolean(),
  39. body('ldapGroupNameAttribute').optional({ nullable: true }).isString(),
  40. body('ldapGroupDescriptionAttribute').optional({ nullable: true }).isString(),
  41. ],
  42. keycloakSyncSettings: [
  43. body('keycloakHost').exists({ checkFalsy: true }).isString(),
  44. body('keycloakGroupRealm').exists({ checkFalsy: true }).isString(),
  45. body('keycloakGroupSyncClientRealm').exists({ checkFalsy: true }).isString(),
  46. body('keycloakGroupSyncClientID').exists({ checkFalsy: true }).isString(),
  47. body('keycloakGroupSyncClientSecret').exists({ checkFalsy: true }).isString(),
  48. body('autoGenerateUserOnKeycloakGroupSync').exists().isBoolean(),
  49. body('preserveDeletedKeycloakGroups').exists().isBoolean(),
  50. body('keycloakGroupDescriptionAttribute').optional({ nullable: true }).isString(),
  51. ],
  52. listChildren: [
  53. query('parentIds').optional().isArray(),
  54. query('includeGrandChildren').optional().isBoolean(),
  55. ],
  56. ancestorGroup: [
  57. query('groupId').isString(),
  58. ],
  59. update: [
  60. body('description').optional().isString(),
  61. ],
  62. delete: [
  63. param('id').trim().exists({ checkFalsy: true }),
  64. query('actionName').trim().exists({ checkFalsy: true }),
  65. query('transferToUserGroupId').trim(),
  66. ],
  67. detail: [
  68. param('id').isString(),
  69. ],
  70. };
  71. router.get('/', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
  72. const { query } = req;
  73. try {
  74. const page = query.page != null ? parseInt(query.page as string) : undefined;
  75. const limit = query.limit != null ? parseInt(query.limit as string) : undefined;
  76. const offset = query.offset != null ? parseInt(query.offset as string) : undefined;
  77. const pagination = query.pagination != null ? query.pagination !== 'false' : undefined;
  78. const result = await ExternalUserGroup.findWithPagination({
  79. page, limit, offset, pagination,
  80. });
  81. const { docs: userGroups, totalDocs: totalUserGroups, limit: pagingLimit } = result;
  82. return res.apiv3({ userGroups, totalUserGroups, pagingLimit });
  83. }
  84. catch (err) {
  85. const msg = 'Error occurred in fetching external user group list';
  86. logger.error('Error', err);
  87. return res.apiv3Err(new ErrorV3(msg));
  88. }
  89. });
  90. router.get('/ancestors', loginRequiredStrictly, adminRequired, validators.ancestorGroup, apiV3FormValidator, async(req, res: ApiV3Response) => {
  91. const { groupId } = req.query;
  92. try {
  93. const userGroup = await ExternalUserGroup.findOne({ _id: { $eq: groupId } });
  94. const ancestorUserGroups = await ExternalUserGroup.findGroupsWithAncestorsRecursively(userGroup);
  95. return res.apiv3({ ancestorUserGroups });
  96. }
  97. catch (err) {
  98. const msg = 'Error occurred while searching user groups';
  99. logger.error(msg, err);
  100. return res.apiv3Err(new ErrorV3(msg));
  101. }
  102. });
  103. router.get('/children', loginRequiredStrictly, adminRequired, validators.listChildren, async(req, res) => {
  104. try {
  105. const { parentIds, includeGrandChildren = false } = req.query;
  106. const externalUserGroupsResult = await ExternalUserGroup.findChildrenByParentIds(parentIds, includeGrandChildren);
  107. return res.apiv3({
  108. childUserGroups: externalUserGroupsResult.childUserGroups,
  109. grandChildUserGroups: externalUserGroupsResult.grandChildUserGroups,
  110. });
  111. }
  112. catch (err) {
  113. const msg = 'Error occurred in fetching child user group list';
  114. logger.error(msg, err);
  115. return res.apiv3Err(new ErrorV3(msg));
  116. }
  117. });
  118. router.get('/:id', loginRequiredStrictly, adminRequired, validators.detail, async(req, res: ApiV3Response) => {
  119. const { id } = req.params;
  120. try {
  121. const userGroup = await ExternalUserGroup.findById(id);
  122. return res.apiv3({ userGroup });
  123. }
  124. catch (err) {
  125. const msg = 'Error occurred while getting external user group';
  126. logger.error(msg, err);
  127. return res.apiv3Err(new ErrorV3(msg));
  128. }
  129. });
  130. router.delete('/:id', loginRequiredStrictly, adminRequired, validators.delete, apiV3FormValidator, addActivity,
  131. async(req: AuthorizedRequest, res: ApiV3Response) => {
  132. const { id: deleteGroupId } = req.params;
  133. const { actionName, transferToUserGroupId } = req.query;
  134. const transferGroupInfo = transferToUserGroupId != null ? {
  135. item: transferToUserGroupId as string,
  136. type: GroupType.externalUserGroup,
  137. } : undefined;
  138. try {
  139. const userGroups = await (crowi.userGroupService as UserGroupService)
  140. .removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferGroupInfo, ExternalUserGroup, ExternalUserGroupRelation);
  141. const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_DELETE };
  142. activityEvent.emit('update', res.locals.activity._id, parameters);
  143. return res.apiv3({ userGroups });
  144. }
  145. catch (err) {
  146. const msg = 'Error occurred while deleting user groups';
  147. logger.error(msg, err);
  148. return res.apiv3Err(new ErrorV3(msg));
  149. }
  150. });
  151. router.put('/:id', loginRequiredStrictly, adminRequired, validators.update, apiV3FormValidator, addActivity, async(req, res: ApiV3Response) => {
  152. const { id } = req.params;
  153. const {
  154. description,
  155. } = req.body;
  156. try {
  157. const userGroup = await ExternalUserGroup.findOneAndUpdate({ _id: id }, { $set: { description } });
  158. const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_UPDATE };
  159. activityEvent.emit('update', res.locals.activity._id, parameters);
  160. return res.apiv3({ userGroup });
  161. }
  162. catch (err) {
  163. const msg = 'Error occurred in updating an external user group';
  164. logger.error(msg, err);
  165. return res.apiv3Err(new ErrorV3(msg));
  166. }
  167. });
  168. router.get('/:id/external-user-group-relations', loginRequiredStrictly, adminRequired, async(req, res: ApiV3Response) => {
  169. const { id } = req.params;
  170. try {
  171. const externalUserGroup = await ExternalUserGroup.findById(id);
  172. const userGroupRelations = await ExternalUserGroupRelation.find({ relatedGroup: externalUserGroup })
  173. .populate('relatedUser');
  174. const serialized = userGroupRelations.map(relation => serializeUserGroupRelationSecurely(relation));
  175. return res.apiv3({ userGroupRelations: serialized });
  176. }
  177. catch (err) {
  178. const msg = `Error occurred in fetching user group relations for external user group: ${id}`;
  179. logger.error(msg, err);
  180. return res.apiv3Err(new ErrorV3(msg));
  181. }
  182. });
  183. router.get('/ldap/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
  184. const settings = {
  185. ldapGroupSearchBase: configManager?.getConfig('crowi', 'external-user-group:ldap:groupSearchBase'),
  186. ldapGroupMembershipAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttribute'),
  187. ldapGroupMembershipAttributeType: configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType'),
  188. ldapGroupChildGroupAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupChildGroupAttribute'),
  189. autoGenerateUserOnLdapGroupSync: configManager?.getConfig('crowi', 'external-user-group:ldap:autoGenerateUserOnGroupSync'),
  190. preserveDeletedLdapGroups: configManager?.getConfig('crowi', 'external-user-group:ldap:preserveDeletedGroups'),
  191. ldapGroupNameAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute'),
  192. ldapGroupDescriptionAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupDescriptionAttribute'),
  193. };
  194. return res.apiv3(settings);
  195. });
  196. router.get('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
  197. const settings = {
  198. keycloakHost: configManager?.getConfig('crowi', 'external-user-group:keycloak:host'),
  199. keycloakGroupRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm'),
  200. keycloakGroupSyncClientRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm'),
  201. keycloakGroupSyncClientID: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID'),
  202. keycloakGroupSyncClientSecret: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret'),
  203. autoGenerateUserOnKeycloakGroupSync: configManager?.getConfig('crowi', 'external-user-group:keycloak:autoGenerateUserOnGroupSync'),
  204. preserveDeletedKeycloakGroups: configManager?.getConfig('crowi', 'external-user-group:keycloak:preserveDeletedGroups'),
  205. keycloakGroupDescriptionAttribute: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute'),
  206. };
  207. return res.apiv3(settings);
  208. });
  209. router.put('/ldap/sync-settings', loginRequiredStrictly, adminRequired, validators.ldapSyncSettings, async(req: AuthorizedRequest, res: ApiV3Response) => {
  210. const errors = validationResult(req);
  211. if (!errors.isEmpty()) {
  212. return res.apiv3Err(
  213. new ErrorV3('Invalid sync settings', 'external_user_group.invalid_sync_settings'), 400,
  214. );
  215. }
  216. const params = {
  217. 'external-user-group:ldap:groupSearchBase': req.body.ldapGroupSearchBase,
  218. 'external-user-group:ldap:groupMembershipAttribute': req.body.ldapGroupMembershipAttribute,
  219. 'external-user-group:ldap:groupMembershipAttributeType': req.body.ldapGroupMembershipAttributeType,
  220. 'external-user-group:ldap:groupChildGroupAttribute': req.body.ldapGroupChildGroupAttribute,
  221. 'external-user-group:ldap:autoGenerateUserOnGroupSync': req.body.autoGenerateUserOnLdapGroupSync,
  222. 'external-user-group:ldap:preserveDeletedGroups': req.body.preserveDeletedLdapGroups,
  223. 'external-user-group:ldap:groupNameAttribute': req.body.ldapGroupNameAttribute,
  224. 'external-user-group:ldap:groupDescriptionAttribute': req.body.ldapGroupDescriptionAttribute,
  225. };
  226. if (params['external-user-group:ldap:groupNameAttribute'] == null || params['external-user-group:ldap:groupNameAttribute'] === '') {
  227. // default is cn
  228. params['external-user-group:ldap:groupNameAttribute'] = 'cn';
  229. }
  230. try {
  231. await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
  232. return res.apiv3({}, 204);
  233. }
  234. catch (err) {
  235. logger.error(err);
  236. return res.apiv3Err(
  237. new ErrorV3('Sync settings update failed', 'external_user_group.update_sync_settings_failed'), 500,
  238. );
  239. }
  240. });
  241. router.put('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, validators.keycloakSyncSettings,
  242. async(req: AuthorizedRequest, res: ApiV3Response) => {
  243. const errors = validationResult(req);
  244. if (!errors.isEmpty()) {
  245. return res.apiv3Err(
  246. new ErrorV3('Invalid sync settings', 'external_user_group.invalid_sync_settings'), 400,
  247. );
  248. }
  249. const params = {
  250. 'external-user-group:keycloak:host': req.body.keycloakHost,
  251. 'external-user-group:keycloak:groupRealm': req.body.keycloakGroupRealm,
  252. 'external-user-group:keycloak:groupSyncClientRealm': req.body.keycloakGroupSyncClientRealm,
  253. 'external-user-group:keycloak:groupSyncClientID': req.body.keycloakGroupSyncClientID,
  254. 'external-user-group:keycloak:groupSyncClientSecret': req.body.keycloakGroupSyncClientSecret,
  255. 'external-user-group:keycloak:autoGenerateUserOnGroupSync': req.body.autoGenerateUserOnKeycloakGroupSync,
  256. 'external-user-group:keycloak:preserveDeletedGroups': req.body.preserveDeletedKeycloakGroups,
  257. 'external-user-group:keycloak:groupDescriptionAttribute': req.body.keycloakGroupDescriptionAttribute,
  258. };
  259. try {
  260. await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
  261. return res.apiv3({}, 204);
  262. }
  263. catch (err) {
  264. logger.error(err);
  265. return res.apiv3Err(
  266. new ErrorV3('Sync settings update failed', 'external_user_group.update_sync_settings_failed'), 500,
  267. );
  268. }
  269. });
  270. router.put('/ldap/sync', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
  271. if (isExecutingSync()) {
  272. return res.apiv3Err(
  273. new ErrorV3('There is an ongoing sync process', 'external_user_group.sync_being_executed'), 409,
  274. );
  275. }
  276. const isLdapEnabled = await configManager.getConfig('crowi', 'security:passport-ldap:isEnabled');
  277. if (!isLdapEnabled) {
  278. return res.apiv3Err(
  279. new ErrorV3('Authentication using ldap is not set', 'external_user_group.ldap.auth_not_set'), 422,
  280. );
  281. }
  282. try {
  283. await crowi.ldapUserGroupSyncService?.init(req.user.name, req.body.password);
  284. }
  285. catch (e) {
  286. return res.apiv3Err(
  287. new ErrorV3('LDAP group sync failed', 'external_user_group.sync_failed'), 500,
  288. );
  289. }
  290. // Do not await for sync to finish. Result (completed, failed) will be notified to the client by socket-io.
  291. crowi.ldapUserGroupSyncService?.syncExternalUserGroups();
  292. return res.apiv3({}, 202);
  293. });
  294. router.put('/keycloak/sync', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
  295. if (isExecutingSync()) {
  296. return res.apiv3Err(
  297. new ErrorV3('There is an ongoing sync process', 'external_user_group.sync_being_executed'), 409,
  298. );
  299. }
  300. const getAuthProviderType = () => {
  301. const kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
  302. const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
  303. // starts with kcHost, contains kcGroupRealm in path
  304. // see: https://regex101.com/r/3ihDmf/1
  305. const regex = new RegExp(`^${kcHost}/.*/${kcGroupRealm}(/|$).*`);
  306. const isOidcEnabled = configManager.getConfig('crowi', 'security:passport-oidc:isEnabled');
  307. const oidcIssuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost');
  308. if (isOidcEnabled && regex.test(oidcIssuerHost)) return 'oidc';
  309. const isSamlEnabled = configManager.getConfig('crowi', 'security:passport-saml:isEnabled');
  310. const samlEntryPoint = configManager.getConfig('crowi', 'security:passport-saml:entryPoint');
  311. if (isSamlEnabled && regex.test(samlEntryPoint)) return 'saml';
  312. return null;
  313. };
  314. const authProviderType = getAuthProviderType();
  315. if (authProviderType == null) {
  316. return res.apiv3Err(
  317. new ErrorV3('Authentication using keycloak is not set', 'external_user_group.keycloak.auth_not_set'), 422,
  318. );
  319. }
  320. crowi.keycloakUserGroupSyncService?.init(authProviderType);
  321. // Do not await for sync to finish. Result (completed, failed) will be notified to the client by socket-io.
  322. crowi.keycloakUserGroupSyncService?.syncExternalUserGroups();
  323. return res.apiv3({}, 202);
  324. });
  325. router.get('/ldap/sync-status', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
  326. const syncStatus = crowi.ldapUserGroupSyncService?.syncStatus;
  327. return res.apiv3({ ...syncStatus });
  328. });
  329. router.get('/keycloak/sync-status', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
  330. const syncStatus = crowi.keycloakUserGroupSyncService?.syncStatus;
  331. return res.apiv3({ ...syncStatus });
  332. });
  333. return router;
  334. };