|
|
@@ -1,7 +1,5 @@
|
|
|
-import { IExternalUserGroup, LdapGroup } from '~/interfaces/external-user-group';
|
|
|
+import { ExternalGroupProviderType, ExternalUserGroupTreeNode } from '~/interfaces/external-user-group';
|
|
|
import { IUserHasId } from '~/interfaces/user';
|
|
|
-import ExternalUserGroup from '~/server/models/external-user-group';
|
|
|
-import ExternalUserGroupRelation from '~/server/models/external-user-group-relation';
|
|
|
|
|
|
import { configManager } from '../config-manager';
|
|
|
import ExternalAccountService from '../external-account';
|
|
|
@@ -9,9 +7,7 @@ import LdapService, { SearchResultEntry } from '../ldap';
|
|
|
|
|
|
import ExternalUserGroupSyncService from './external-user-group-sync-service';
|
|
|
|
|
|
-class LdapUserGroupSyncService {
|
|
|
-
|
|
|
- ldapGroups: LdapGroup[];
|
|
|
+class LdapUserGroupSyncService extends ExternalUserGroupSyncService {
|
|
|
|
|
|
ldapService: LdapService;
|
|
|
|
|
|
@@ -21,72 +17,21 @@ class LdapUserGroupSyncService {
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
|
|
constructor(crowi: any, userBindUsername?: string, userBindPassword?: string) {
|
|
|
+ super(ExternalGroupProviderType.ldap);
|
|
|
this.crowi = crowi;
|
|
|
this.ldapService = new LdapService(userBindUsername, userBindPassword);
|
|
|
this.externalAccountService = new ExternalAccountService(crowi);
|
|
|
}
|
|
|
|
|
|
- async fetchLdapGroups(): Promise<void> {
|
|
|
- const isUserBind = configManager.getConfig('crowi', 'security:passport-ldap:isUserBind');
|
|
|
- const getGroupDirData = async() => {
|
|
|
- if (isUserBind) {
|
|
|
- return this.ldapService.searchGroupDir();
|
|
|
- }
|
|
|
- return this.ldapService.searchGroupDir();
|
|
|
- };
|
|
|
-
|
|
|
- const groupDirData = await getGroupDirData();
|
|
|
-
|
|
|
- this.ldapGroups = groupDirData.map(data => this.convertSearchResultEntryToLdapGroup(data))
|
|
|
- .filter((group): group is NonNullable<LdapGroup> => group != null);
|
|
|
- }
|
|
|
-
|
|
|
- // 全グループ同期メソッド
|
|
|
- syncExternalUserGroups(): void {
|
|
|
- /*
|
|
|
- 1. ldapGroupSearchBase を使って LDAP から全てのグループを取得する
|
|
|
- - 設定値を元に LDAPGroup に変換する
|
|
|
- 2. 各親グループについて、createUpdateExternalUserGroup を呼び出す
|
|
|
- 3. 子についても同様に呼び出し、返却された子グループを親グループと紐付ける
|
|
|
- 4. 2, 3 を再起的に行う
|
|
|
- 5. preserveDeletedLDAPGroups が false の場合、木探索の過程で見つからなかった ExternalUserGroup は削除する
|
|
|
- */
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 1. Create/Update ExternalUserGroup from ldapGroup
|
|
|
- * 2. For every element in ldapGroup.users, call getMemberUser and create an ExternalUserGroupRelation with ExternalUserGroup if it does not have one
|
|
|
- * 3. Retrun ExternalUserGroup
|
|
|
- */
|
|
|
- async createUpdateExternalUserGroup(ldapGroup: LdapGroup, parentId: string): Promise<IExternalUserGroup> {
|
|
|
- const externalUserGroup = await ExternalUserGroup.createGroup(ldapGroup.name, ldapGroup.description, ldapGroup.dn, parentId);
|
|
|
-
|
|
|
- await Promise.all(ldapGroup.users.map((userIdentifier) => {
|
|
|
- return (async() => {
|
|
|
- const user = await this.getMemberUser(userIdentifier);
|
|
|
- await ExternalUserGroupRelation.findOrCreateRelation(externalUserGroup, user);
|
|
|
- })();
|
|
|
- }));
|
|
|
-
|
|
|
- return externalUserGroup;
|
|
|
- }
|
|
|
-
|
|
|
- /**
|
|
|
- * 1. Execute search on LDAP server for user using useridentifier
|
|
|
- * 2. Search for GROWI user based on LDAP user info, and return
|
|
|
- * - if autoGenerateUserOnLDAPGroupSync is true and GROWI user is not found, create new GROWI user
|
|
|
- * @param {string} userIdentifier Search LDAP server using this identifier (DN or UID)
|
|
|
- * @returns {Promise<IUser | null>} IUser when found or created, null when neither
|
|
|
- */
|
|
|
- async getMemberUser(userIdentifier: string): Promise<IUserHasId | null> {
|
|
|
+ async getMemberUser(externalUserId: string): Promise<IUserHasId | null> {
|
|
|
const groupMembershipAttributeType = configManager?.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttributeType');
|
|
|
|
|
|
const getUser = async() => {
|
|
|
if (groupMembershipAttributeType === 'DN') {
|
|
|
- return this.ldapService.search(undefined, userIdentifier, 'one');
|
|
|
+ return this.ldapService.search(undefined, externalUserId, 'base');
|
|
|
}
|
|
|
if (groupMembershipAttributeType === 'UID') {
|
|
|
- return this.ldapService.search(`(uid=${userIdentifier})`, undefined, 'one');
|
|
|
+ return this.ldapService.search(`(uid=${externalUserId})`, undefined);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
@@ -106,25 +51,62 @@ class LdapUserGroupSyncService {
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- private convertSearchResultEntryToLdapGroup(groupEntry: SearchResultEntry): LdapGroup | null {
|
|
|
+ async generateExternalUserGroupTrees(): Promise<ExternalUserGroupTreeNode[]> {
|
|
|
const groupChildGroupAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupChildGroupAttribute');
|
|
|
const groupMembershipAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupMembershipAttribute');
|
|
|
- const groupNameAttribute = configManager.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute');
|
|
|
+ const groupNameAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupNameAttribute');
|
|
|
const groupDescriptionAttribute: string = configManager.getConfig('crowi', 'external-user-group:ldap:groupDescriptionAttribute');
|
|
|
+ const groupBase: string = this.ldapService.getGroupSearchBase();
|
|
|
+
|
|
|
+ const groupEntries = await this.ldapService.searchGroupDir();
|
|
|
+
|
|
|
+ const getChildGroupDnsFromGroupEntry = (groupEntry: SearchResultEntry) => {
|
|
|
+ // groupChildGroupAttribute and groupMembershipAttribute may be the same,
|
|
|
+ // so filter values of groupChildGroupAttribute to ones that include groupBase
|
|
|
+ return this.ldapService.getArrayValFromSearchResultEntry(groupEntry, groupChildGroupAttribute).filter(attr => attr.includes(groupBase));
|
|
|
+ };
|
|
|
+ const getExternalUserIdsFromGroupEntry = (groupEntry: SearchResultEntry) => {
|
|
|
+ // groupChildGroupAttribute and groupMembershipAttribute may be the same,
|
|
|
+ // so filter values of groupMembershipAttribute to ones that does not include groupBase
|
|
|
+ return this.ldapService.getArrayValFromSearchResultEntry(groupEntry, groupMembershipAttribute).filter(attr => !attr.includes(groupBase));
|
|
|
+ };
|
|
|
|
|
|
- const childGroups = this.ldapService.getArrayValFromSearchResultEntry(groupEntry, groupChildGroupAttribute);
|
|
|
- const users = this.ldapService.getArrayValFromSearchResultEntry(groupEntry, groupMembershipAttribute);
|
|
|
+ const convert = (entry: SearchResultEntry, converted: string[]): ExternalUserGroupTreeNode | null => {
|
|
|
+ if (converted.includes(entry.objectName)) {
|
|
|
+ throw Error('There is a possible circular reference in your LDAP group tree structure');
|
|
|
+ }
|
|
|
+ converted.push(entry.objectName);
|
|
|
+
|
|
|
+ const externalUserIds = getExternalUserIdsFromGroupEntry(entry);
|
|
|
+ const name = this.ldapService.getStringValFromSearchResultEntry(entry, groupNameAttribute);
|
|
|
+ const description = this.ldapService.getStringValFromSearchResultEntry(entry, groupDescriptionAttribute);
|
|
|
+ const childGroupDNs = getChildGroupDnsFromGroupEntry(entry);
|
|
|
+
|
|
|
+ const childGroupNodes: ExternalUserGroupTreeNode[] = childGroupDNs.map((dn) => {
|
|
|
+ const childEntry = groupEntries.find(ge => ge.objectName === dn);
|
|
|
+ return childEntry != null ? convert(childEntry, converted) : null;
|
|
|
+ }).filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
|
|
|
+
|
|
|
+ return name != null ? {
|
|
|
+ id: entry.objectName,
|
|
|
+ externalUserIds,
|
|
|
+ childGroupNodes,
|
|
|
+ name,
|
|
|
+ description,
|
|
|
+ } : null;
|
|
|
+ };
|
|
|
+
|
|
|
+ // all the DNs of groups that are not a root of a tree
|
|
|
+ const allChildGroupDNs = new Set(groupEntries.flatMap((entry) => {
|
|
|
+ return getChildGroupDnsFromGroupEntry(entry);
|
|
|
+ }));
|
|
|
|
|
|
- const name = this.ldapService.getStringValFromSearchResultEntry(groupEntry, groupNameAttribute);
|
|
|
- const description = this.ldapService.getStringValFromSearchResultEntry(groupEntry, groupDescriptionAttribute);
|
|
|
+ // root of every tree
|
|
|
+ const rootEntries = groupEntries.filter((entry) => {
|
|
|
+ return !allChildGroupDNs.has(entry.objectName);
|
|
|
+ });
|
|
|
|
|
|
- return name != null ? {
|
|
|
- dn: groupEntry.objectName || '',
|
|
|
- childGroups,
|
|
|
- users,
|
|
|
- name,
|
|
|
- description,
|
|
|
- } : null;
|
|
|
+ return rootEntries.map(entry => convert(entry, [])).filter((node): node is NonNullable<ExternalUserGroupTreeNode> => node != null);
|
|
|
}
|
|
|
|
|
|
private async getExternalAccount(uid: string, userEntry: SearchResultEntry) {
|