-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathldap-sync-service.ts
225 lines (188 loc) · 7.86 KB
/
ldap-sync-service.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/**
* SudoSOS back-end API service.
* Copyright (C) 2024 Study association GEWIS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @license
*/
/**
* This is the module page of the ldap-sync-service.
*
* @module internal/ldap-sync-service
*/
import User, { TermsOfServiceStatus, UserType } from '../../../entity/user/user';
import { Client } from 'ldapts';
import ADService from '../../ad-service';
import LDAPAuthenticator from '../../../entity/authenticator/ldap-authenticator';
import RoleManager from '../../../rbac/role-manager';
import { EntityManager } from 'typeorm';
import { getLDAPConnection, LDAPGroup, LDAPUser } from '../../../helpers/ad';
import RBACService from '../../rbac-service';
import log4js, { Logger } from 'log4js';
import { UserSyncService } from './user-sync-service';
export default class LdapSyncService extends UserSyncService {
// We only sync organs, members and integrations.
targets = [UserType.ORGAN, UserType.MEMBER, UserType.INTEGRATION];
// Is set in the `pre` function.
private ldapClient: Client;
private readonly adService: ADService;
private readonly roleManager: RoleManager;
private logger: Logger = log4js.getLogger('AdSyncService');
constructor(roleManager: RoleManager, adService?: ADService, manager?: EntityManager) {
// Sanity check, since we already have a ldapClient
if (!process.env.ENABLE_LDAP) throw new Error('LDAP is not enabled');
super(manager);
this.logger.level = process.env.LOG_LEVEL;
this.roleManager = roleManager;
this.adService = adService ?? new ADService(this.manager);
}
async guard(user: User): Promise<boolean> {
if (!await super.guard(user)) return false;
// For members, we only sync if we have an LDAPAuthenticator
if (user.type === UserType.MEMBER) {
const ldapAuth = await this.manager.findOne(LDAPAuthenticator, { where: { user: { id: user.id } } });
return !!ldapAuth;
}
return true;
}
/**
* Sync user based on LDAPAuthenticator.
* Only organs are actually updated.
* @param user
*/
async sync(user: User): Promise<boolean> {
const ldapAuth = await this.manager.findOne(LDAPAuthenticator, { where: { user: { id: user.id } } });
if (!ldapAuth) return false;
const ldapUser = await this.adService.getLDAPResponseFromGUID(this.ldapClient, ldapAuth.UUID);
if (!ldapUser) return false;
// For members, we fetch user info from the GEWISDB
// Therefore we do not need to update the user
// But we do return true to indicate that the user is "bound" to the LDAP
if (user.type === UserType.MEMBER) return true;
this.logger.trace(`Updating user ${user} from LDAP.`);
user.firstName = ldapUser.displayName;
user.lastName = '';
user.canGoIntoDebt = false;
user.acceptedToS = TermsOfServiceStatus.NOT_REQUIRED;
user.active = true;
await this.manager.save(user);
return true;
}
/**
* Removes the LDAPAuthenticator for the given user.
* @param user
*/
async down(user: User): Promise<void> {
this.logger.trace('Running down for user', user);
const ldapAuth = await this.manager.findOne(LDAPAuthenticator, { where: { user: { id: user.id } } });
if (ldapAuth) await this.manager.delete(LDAPAuthenticator, { userId: user.id });
// For members, we only remove the authenticator.
if (user.type === UserType.MEMBER) return;
// For organs and integrations, we set the user to deleted and inactive.
// TODO: closing organ active with non-zero balance?
user.deleted = true;
user.active = false;
await this.manager.save(user);
}
/**
* Fetches all shared accounts from AD and creates them in SudoSOS.
* Also updates the membership of the shared accounts.
* @private
*/
private async fetchSharedAccounts(): Promise<void> {
this.logger.debug('Fetching shared accounts from LDAP');
const sharedAccounts = await this.adService.getLDAPGroups<LDAPGroup>(
this.ldapClient, process.env.LDAP_SHARED_ACCOUNT_FILTER);
// If there are new shared accounts, we create them.
const newSharedAccounts = (await this.adService.filterUnboundGUID(sharedAccounts)) as LDAPGroup[];
this.logger.trace(`Found ${newSharedAccounts.length} new shared accounts`);
for (const sharedAccount of newSharedAccounts) {
await this.adService.toSharedUser(sharedAccount);
}
for (const sharedAccount of sharedAccounts) {
await this.adService.updateSharedAccountMembership(this.ldapClient, sharedAccount);
}
}
/**
* Adds local users to roles based on AD membership.
* Roles are matched using the CN of the AD group.
*
* If an AD user has a role but no account yet, the account is created.
*
* @private
*/
private async fetchUserRoles(): Promise<void> {
this.logger.debug('Fetching user roles from LDAP');
const roles = await this.adService.getLDAPGroups<LDAPGroup>(
this.ldapClient, process.env.LDAP_ROLE_FILTER);
if (!roles) return;
const [dbRoles] = await RBACService.getRoles();
const dbRoleNames = new Set(dbRoles.map((r) => r.name));
const nonLocalRoles = roles.filter(ldapRole => !dbRoleNames.has(ldapRole.cn));
nonLocalRoles.forEach(ldapRole => {
this.logger.warn(`LDAP role ${ldapRole.cn} does not exist locally.`);
});
const localRoles = roles.filter(ldapRole => dbRoleNames.has(ldapRole.cn));
this.logger.trace(`Found ${localRoles.length} local roles`);
for (const ldapRole of localRoles) {
await this.adService.updateRoleMembership(this.ldapClient, ldapRole, this.roleManager);
}
}
/**
* Fetches all service accounts from LDAP and creates them locally.
*
* @private
*/
private async fetchServiceAccounts(): Promise<void> {
this.logger.debug('Fetching service accounts from LDAP');
const serviceAccounts = (await this.adService.getLDAPGroupMembers(
this.ldapClient, process.env.LDAP_SERVICE_ACCOUNT_FILTER)).searchEntries;
const newServiceAccounts = await this.adService.filterUnboundGUID(serviceAccounts);
this.logger.trace(`Found ${newServiceAccounts.length} new service accounts`);
for (const serviceAccount of newServiceAccounts) {
await this.adService.toServiceAccount(serviceAccount as LDAPUser);
}
}
/**
* LDAP fetch retrieves organs, service accounts, and user roles from AD.
*/
async fetch(): Promise<void> {
this.logger.trace('Fetching LDAP data');
if (!process.env.LDAP_SHARED_ACCOUNT_FILTER) {
this.logger.warn('LDAP_SHARED_ACCOUNT_FILTER is not set, skipping shared accounts');
} else {
await this.fetchSharedAccounts();
}
if (!process.env.LDAP_ROLE_FILTER) {
this.logger.warn('LDAP_ROLE_FILTER is not set, skipping user roles');
} else {
await this.fetchUserRoles();
}
if (!process.env.LDAP_SERVICE_ACCOUNT_FILTER) {
this.logger.warn('LDAP_SERVICE_ACCOUNT_FILTER is not set, skipping service accounts');
} else {
await this.fetchServiceAccounts();
}
}
// TODO: dependency injection of Client instead?
// i.e. add a Client to the constructor
// this would require us to make a wrapper constructor to be able to bind the client on call
async pre(): Promise<void> {
this.ldapClient = await getLDAPConnection();
}
async post(): Promise<void> {
await this.ldapClient.unbind();
}
}