| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393 |
- import { GroupType } from '@growi/core';
- import { ErrorV3 } from '@growi/core/dist/models';
- import { Router, Request } from 'express';
- import {
- body, param, query, validationResult,
- } from 'express-validator';
- import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
- import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
- import { SupportedAction } from '~/interfaces/activity';
- import Crowi from '~/server/crowi';
- import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
- import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
- import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/user-group-relation-serializer';
- import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
- import { configManager } from '~/server/service/config-manager';
- import UserGroupService from '~/server/service/user-group';
- import loggerFactory from '~/utils/logger';
- const logger = loggerFactory('growi:routes:apiv3:external-user-group');
- const router = Router();
- interface AuthorizedRequest extends Request {
- user?: any
- }
- module.exports = (crowi: Crowi): Router => {
- const loginRequiredStrictly = require('~/server/middlewares/login-required')(crowi);
- const adminRequired = require('~/server/middlewares/admin-required')(crowi);
- const addActivity = generateAddActivityMiddleware(crowi);
- const activityEvent = crowi.event('activity');
- const isExecutingSync = () => {
- return crowi.ldapUserGroupSyncService?.syncStatus?.isExecutingSync || crowi.keycloakUserGroupSyncService?.syncStatus?.isExecutingSync || false;
- };
- const validators = {
- ldapSyncSettings: [
- body('ldapGroupSearchBase').optional({ nullable: true }).isString(),
- body('ldapGroupMembershipAttribute').exists({ checkFalsy: true }).isString(),
- body('ldapGroupMembershipAttributeType').exists({ checkFalsy: true }).isString(),
- body('ldapGroupChildGroupAttribute').exists({ checkFalsy: true }).isString(),
- body('autoGenerateUserOnLdapGroupSync').exists().isBoolean(),
- body('preserveDeletedLdapGroups').exists().isBoolean(),
- body('ldapGroupNameAttribute').optional({ nullable: true }).isString(),
- body('ldapGroupDescriptionAttribute').optional({ nullable: true }).isString(),
- ],
- keycloakSyncSettings: [
- body('keycloakHost').exists({ checkFalsy: true }).isString(),
- body('keycloakGroupRealm').exists({ checkFalsy: true }).isString(),
- body('keycloakGroupSyncClientRealm').exists({ checkFalsy: true }).isString(),
- body('keycloakGroupSyncClientID').exists({ checkFalsy: true }).isString(),
- body('keycloakGroupSyncClientSecret').exists({ checkFalsy: true }).isString(),
- body('autoGenerateUserOnKeycloakGroupSync').exists().isBoolean(),
- body('preserveDeletedKeycloakGroups').exists().isBoolean(),
- body('keycloakGroupDescriptionAttribute').optional({ nullable: true }).isString(),
- ],
- listChildren: [
- query('parentIds').optional().isArray(),
- query('includeGrandChildren').optional().isBoolean(),
- ],
- ancestorGroup: [
- query('groupId').isString(),
- ],
- update: [
- body('description').optional().isString(),
- ],
- delete: [
- param('id').trim().exists({ checkFalsy: true }),
- query('actionName').trim().exists({ checkFalsy: true }),
- query('transferToUserGroupId').trim(),
- ],
- detail: [
- param('id').isString(),
- ],
- };
- router.get('/', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
- const { query } = req;
- try {
- const page = query.page != null ? parseInt(query.page as string) : undefined;
- const limit = query.limit != null ? parseInt(query.limit as string) : undefined;
- const offset = query.offset != null ? parseInt(query.offset as string) : undefined;
- const pagination = query.pagination != null ? query.pagination !== 'false' : undefined;
- const result = await ExternalUserGroup.findWithPagination({
- page, limit, offset, pagination,
- });
- const { docs: userGroups, totalDocs: totalUserGroups, limit: pagingLimit } = result;
- return res.apiv3({ userGroups, totalUserGroups, pagingLimit });
- }
- catch (err) {
- const msg = 'Error occurred in fetching external user group list';
- logger.error('Error', err);
- return res.apiv3Err(new ErrorV3(msg));
- }
- });
- router.get('/ancestors', loginRequiredStrictly, adminRequired, validators.ancestorGroup, apiV3FormValidator, async(req, res: ApiV3Response) => {
- const { groupId } = req.query;
- try {
- const userGroup = await ExternalUserGroup.findOne({ _id: { $eq: groupId } });
- const ancestorUserGroups = await ExternalUserGroup.findGroupsWithAncestorsRecursively(userGroup);
- return res.apiv3({ ancestorUserGroups });
- }
- catch (err) {
- const msg = 'Error occurred while searching user groups';
- logger.error(msg, err);
- return res.apiv3Err(new ErrorV3(msg));
- }
- });
- router.get('/children', loginRequiredStrictly, adminRequired, validators.listChildren, async(req, res) => {
- try {
- const { parentIds, includeGrandChildren = false } = req.query;
- const externalUserGroupsResult = await ExternalUserGroup.findChildrenByParentIds(parentIds, includeGrandChildren);
- return res.apiv3({
- childUserGroups: externalUserGroupsResult.childUserGroups,
- grandChildUserGroups: externalUserGroupsResult.grandChildUserGroups,
- });
- }
- catch (err) {
- const msg = 'Error occurred in fetching child user group list';
- logger.error(msg, err);
- return res.apiv3Err(new ErrorV3(msg));
- }
- });
- router.get('/:id', loginRequiredStrictly, adminRequired, validators.detail, async(req, res: ApiV3Response) => {
- const { id } = req.params;
- try {
- const userGroup = await ExternalUserGroup.findById(id);
- return res.apiv3({ userGroup });
- }
- catch (err) {
- const msg = 'Error occurred while getting external user group';
- logger.error(msg, err);
- return res.apiv3Err(new ErrorV3(msg));
- }
- });
- router.delete('/:id', loginRequiredStrictly, adminRequired, validators.delete, apiV3FormValidator, addActivity,
- async(req: AuthorizedRequest, res: ApiV3Response) => {
- const { id: deleteGroupId } = req.params;
- const { actionName, transferToUserGroupId } = req.query;
- const transferGroupInfo = transferToUserGroupId != null ? {
- item: transferToUserGroupId as string,
- type: GroupType.externalUserGroup,
- } : undefined;
- try {
- const userGroups = await (crowi.userGroupService as UserGroupService)
- .removeCompletelyByRootGroupId(deleteGroupId, actionName, req.user, transferGroupInfo, ExternalUserGroup, ExternalUserGroupRelation);
- const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_DELETE };
- activityEvent.emit('update', res.locals.activity._id, parameters);
- return res.apiv3({ userGroups });
- }
- catch (err) {
- const msg = 'Error occurred while deleting user groups';
- logger.error(msg, err);
- return res.apiv3Err(new ErrorV3(msg));
- }
- });
- router.put('/:id', loginRequiredStrictly, adminRequired, validators.update, apiV3FormValidator, addActivity, async(req, res: ApiV3Response) => {
- const { id } = req.params;
- const {
- description,
- } = req.body;
- try {
- const userGroup = await ExternalUserGroup.findOneAndUpdate({ _id: id }, { $set: { description } });
- const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_UPDATE };
- activityEvent.emit('update', res.locals.activity._id, parameters);
- return res.apiv3({ userGroup });
- }
- catch (err) {
- const msg = 'Error occurred in updating an external user group';
- logger.error(msg, err);
- return res.apiv3Err(new ErrorV3(msg));
- }
- });
- router.get('/:id/external-user-group-relations', loginRequiredStrictly, adminRequired, async(req, res: ApiV3Response) => {
- const { id } = req.params;
- try {
- const externalUserGroup = await ExternalUserGroup.findById(id);
- const userGroupRelations = await ExternalUserGroupRelation.find({ relatedGroup: externalUserGroup })
- .populate('relatedUser');
- const serialized = userGroupRelations.map(relation => serializeUserGroupRelationSecurely(relation));
- return res.apiv3({ userGroupRelations: serialized });
- }
- catch (err) {
- const msg = `Error occurred in fetching user group relations for external user group: ${id}`;
- logger.error(msg, err);
- return res.apiv3Err(new ErrorV3(msg));
- }
- });
- router.get('/ldap/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
- const settings = {
- ldapGroupSearchBase: configManager?.getConfig('crowi', 'external-user-group:ldap:groupSearchBase'),
- ldapGroupMembershipAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttribute'),
- ldapGroupMembershipAttributeType: configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType'),
- ldapGroupChildGroupAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupChildGroupAttribute'),
- autoGenerateUserOnLdapGroupSync: configManager?.getConfig('crowi', 'external-user-group:ldap:autoGenerateUserOnGroupSync'),
- preserveDeletedLdapGroups: configManager?.getConfig('crowi', 'external-user-group:ldap:preserveDeletedGroups'),
- ldapGroupNameAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute'),
- ldapGroupDescriptionAttribute: configManager?.getConfig('crowi', 'external-user-group:ldap:groupDescriptionAttribute'),
- };
- return res.apiv3(settings);
- });
- router.get('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
- const settings = {
- keycloakHost: configManager?.getConfig('crowi', 'external-user-group:keycloak:host'),
- keycloakGroupRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm'),
- keycloakGroupSyncClientRealm: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientRealm'),
- keycloakGroupSyncClientID: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientID'),
- keycloakGroupSyncClientSecret: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupSyncClientSecret'),
- autoGenerateUserOnKeycloakGroupSync: configManager?.getConfig('crowi', 'external-user-group:keycloak:autoGenerateUserOnGroupSync'),
- preserveDeletedKeycloakGroups: configManager?.getConfig('crowi', 'external-user-group:keycloak:preserveDeletedGroups'),
- keycloakGroupDescriptionAttribute: configManager?.getConfig('crowi', 'external-user-group:keycloak:groupDescriptionAttribute'),
- };
- return res.apiv3(settings);
- });
- router.put('/ldap/sync-settings', loginRequiredStrictly, adminRequired, validators.ldapSyncSettings, async(req: AuthorizedRequest, res: ApiV3Response) => {
- const errors = validationResult(req);
- if (!errors.isEmpty()) {
- return res.apiv3Err(
- new ErrorV3('Invalid sync settings', 'external_user_group.invalid_sync_settings'), 400,
- );
- }
- const params = {
- 'external-user-group:ldap:groupSearchBase': req.body.ldapGroupSearchBase,
- 'external-user-group:ldap:groupMembershipAttribute': req.body.ldapGroupMembershipAttribute,
- 'external-user-group:ldap:groupMembershipAttributeType': req.body.ldapGroupMembershipAttributeType,
- 'external-user-group:ldap:groupChildGroupAttribute': req.body.ldapGroupChildGroupAttribute,
- 'external-user-group:ldap:autoGenerateUserOnGroupSync': req.body.autoGenerateUserOnLdapGroupSync,
- 'external-user-group:ldap:preserveDeletedGroups': req.body.preserveDeletedLdapGroups,
- 'external-user-group:ldap:groupNameAttribute': req.body.ldapGroupNameAttribute,
- 'external-user-group:ldap:groupDescriptionAttribute': req.body.ldapGroupDescriptionAttribute,
- };
- if (params['external-user-group:ldap:groupNameAttribute'] == null || params['external-user-group:ldap:groupNameAttribute'] === '') {
- // default is cn
- params['external-user-group:ldap:groupNameAttribute'] = 'cn';
- }
- try {
- await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
- return res.apiv3({}, 204);
- }
- catch (err) {
- logger.error(err);
- return res.apiv3Err(
- new ErrorV3('Sync settings update failed', 'external_user_group.update_sync_settings_failed'), 500,
- );
- }
- });
- router.put('/keycloak/sync-settings', loginRequiredStrictly, adminRequired, validators.keycloakSyncSettings,
- async(req: AuthorizedRequest, res: ApiV3Response) => {
- const errors = validationResult(req);
- if (!errors.isEmpty()) {
- return res.apiv3Err(
- new ErrorV3('Invalid sync settings', 'external_user_group.invalid_sync_settings'), 400,
- );
- }
- const params = {
- 'external-user-group:keycloak:host': req.body.keycloakHost,
- 'external-user-group:keycloak:groupRealm': req.body.keycloakGroupRealm,
- 'external-user-group:keycloak:groupSyncClientRealm': req.body.keycloakGroupSyncClientRealm,
- 'external-user-group:keycloak:groupSyncClientID': req.body.keycloakGroupSyncClientID,
- 'external-user-group:keycloak:groupSyncClientSecret': req.body.keycloakGroupSyncClientSecret,
- 'external-user-group:keycloak:autoGenerateUserOnGroupSync': req.body.autoGenerateUserOnKeycloakGroupSync,
- 'external-user-group:keycloak:preserveDeletedGroups': req.body.preserveDeletedKeycloakGroups,
- 'external-user-group:keycloak:groupDescriptionAttribute': req.body.keycloakGroupDescriptionAttribute,
- };
- try {
- await configManager.updateConfigsInTheSameNamespace('crowi', params, true);
- return res.apiv3({}, 204);
- }
- catch (err) {
- logger.error(err);
- return res.apiv3Err(
- new ErrorV3('Sync settings update failed', 'external_user_group.update_sync_settings_failed'), 500,
- );
- }
- });
- router.put('/ldap/sync', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
- if (isExecutingSync()) {
- return res.apiv3Err(
- new ErrorV3('There is an ongoing sync process', 'external_user_group.sync_being_executed'), 409,
- );
- }
- const isLdapEnabled = await configManager.getConfig('crowi', 'security:passport-ldap:isEnabled');
- if (!isLdapEnabled) {
- return res.apiv3Err(
- new ErrorV3('Authentication using ldap is not set', 'external_user_group.ldap.auth_not_set'), 422,
- );
- }
- try {
- await crowi.ldapUserGroupSyncService?.init(req.user.name, req.body.password);
- }
- catch (e) {
- return res.apiv3Err(
- new ErrorV3('LDAP group sync failed', 'external_user_group.sync_failed'), 500,
- );
- }
- // Do not await for sync to finish. Result (completed, failed) will be notified to the client by socket-io.
- crowi.ldapUserGroupSyncService?.syncExternalUserGroups();
- return res.apiv3({}, 202);
- });
- router.put('/keycloak/sync', loginRequiredStrictly, adminRequired, async(req: AuthorizedRequest, res: ApiV3Response) => {
- if (isExecutingSync()) {
- return res.apiv3Err(
- new ErrorV3('There is an ongoing sync process', 'external_user_group.sync_being_executed'), 409,
- );
- }
- const getAuthProviderType = () => {
- const kcHost = configManager?.getConfig('crowi', 'external-user-group:keycloak:host');
- const kcGroupRealm = configManager?.getConfig('crowi', 'external-user-group:keycloak:groupRealm');
- // starts with kcHost, contains kcGroupRealm in path
- // see: https://regex101.com/r/3ihDmf/1
- const regex = new RegExp(`^${kcHost}/.*/${kcGroupRealm}(/|$).*`);
- const isOidcEnabled = configManager.getConfig('crowi', 'security:passport-oidc:isEnabled');
- const oidcIssuerHost = configManager.getConfig('crowi', 'security:passport-oidc:issuerHost');
- if (isOidcEnabled && regex.test(oidcIssuerHost)) return 'oidc';
- const isSamlEnabled = configManager.getConfig('crowi', 'security:passport-saml:isEnabled');
- const samlEntryPoint = configManager.getConfig('crowi', 'security:passport-saml:entryPoint');
- if (isSamlEnabled && regex.test(samlEntryPoint)) return 'saml';
- return null;
- };
- const authProviderType = getAuthProviderType();
- if (authProviderType == null) {
- return res.apiv3Err(
- new ErrorV3('Authentication using keycloak is not set', 'external_user_group.keycloak.auth_not_set'), 422,
- );
- }
- crowi.keycloakUserGroupSyncService?.init(authProviderType);
- // Do not await for sync to finish. Result (completed, failed) will be notified to the client by socket-io.
- crowi.keycloakUserGroupSyncService?.syncExternalUserGroups();
- return res.apiv3({}, 202);
- });
- router.get('/ldap/sync-status', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
- const syncStatus = crowi.ldapUserGroupSyncService?.syncStatus;
- return res.apiv3({ ...syncStatus });
- });
- router.get('/keycloak/sync-status', loginRequiredStrictly, adminRequired, (req: AuthorizedRequest, res: ApiV3Response) => {
- const syncStatus = crowi.keycloakUserGroupSyncService?.syncStatus;
- return res.apiv3({ ...syncStatus });
- });
- return router;
- };
|