1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.userdirectory.moodle;
23
24 import org.opencastproject.security.api.CachingUserProviderMXBean;
25 import org.opencastproject.security.api.Group;
26 import org.opencastproject.security.api.JaxbOrganization;
27 import org.opencastproject.security.api.JaxbRole;
28 import org.opencastproject.security.api.JaxbUser;
29 import org.opencastproject.security.api.Organization;
30 import org.opencastproject.security.api.Role;
31 import org.opencastproject.security.api.RoleProvider;
32 import org.opencastproject.security.api.SecurityConstants;
33 import org.opencastproject.security.api.User;
34 import org.opencastproject.security.api.UserProvider;
35 import org.opencastproject.userdirectory.moodle.MoodleWebService.CoreUserGetUserByFieldFilters;
36
37 import com.google.common.cache.CacheBuilder;
38 import com.google.common.cache.CacheLoader;
39 import com.google.common.cache.LoadingCache;
40 import com.google.common.util.concurrent.ExecutionError;
41 import com.google.common.util.concurrent.UncheckedExecutionException;
42
43 import org.apache.commons.lang3.StringUtils;
44 import org.slf4j.Logger;
45 import org.slf4j.LoggerFactory;
46
47 import java.lang.management.ManagementFactory;
48 import java.util.ArrayList;
49 import java.util.Collections;
50 import java.util.HashSet;
51 import java.util.Iterator;
52 import java.util.LinkedList;
53 import java.util.List;
54 import java.util.Set;
55 import java.util.concurrent.TimeUnit;
56 import java.util.concurrent.atomic.AtomicLong;
57 import java.util.regex.PatternSyntaxException;
58
59 import javax.management.InstanceNotFoundException;
60 import javax.management.MBeanServer;
61 import javax.management.ObjectName;
62
63
64
65
66 public class MoodleUserProviderInstance implements UserProvider, RoleProvider, CachingUserProviderMXBean {
67
68
69
70 private static final String PROVIDER_NAME = "moodle";
71
72
73
74
75 private static final Logger logger = LoggerFactory.getLogger(MoodleUserProviderInstance.class);
76
77
78
79
80 private static final String LEARNER_ROLE_SUFFIX = "Learner";
81
82
83
84
85 private static final String INSTRUCTOR_ROLE_SUFFIX = "Instructor";
86
87
88
89
90 private static final String GROUP_ROLE_PREFIX = "G";
91
92
93
94
95 private static final String GROUP_ROLE_SUFFIX = "Learner";
96
97
98
99
100 private MoodleWebService client;
101
102
103
104
105 private Organization organization;
106
107
108
109
110 private boolean groupRoles;
111
112
113
114
115 private String coursePattern;
116
117
118
119
120 private String userPattern;
121
122
123
124
125 private String groupPattern;
126
127
128
129
130 private final String contextRolePrefix;
131
132
133
134
135 private LoadingCache<String, Object> cache;
136
137
138
139
140 private Object nullToken = new Object();
141
142
143
144
145 private AtomicLong loadUserRequests;
146
147
148
149
150 private AtomicLong moodleWebServiceRequests;
151
152
153 private final boolean lowercaseUsername;
154
155 private final List<String> ignoredUsernames;
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172 public MoodleUserProviderInstance(String pid, MoodleWebService client, Organization organization,
173 String coursePattern, String userPattern, String groupPattern, boolean groupRoles, int cacheSize,
174 int cacheExpiration, String adminUserName, final boolean lowercaseUsername, final String contextRolePrefix) {
175 this.client = client;
176 this.organization = organization;
177 this.groupRoles = groupRoles;
178 this.coursePattern = coursePattern;
179 this.userPattern = userPattern;
180 this.groupPattern = groupPattern;
181 this.lowercaseUsername = lowercaseUsername;
182 this.contextRolePrefix = contextRolePrefix;
183
184
185 this.ignoredUsernames = new ArrayList<>();
186 this.ignoredUsernames.add("");
187 this.ignoredUsernames.add(SecurityConstants.GLOBAL_ANONYMOUS_USERNAME);
188 if (StringUtils.isNoneEmpty(adminUserName)) {
189 ignoredUsernames.add(adminUserName);
190 }
191
192 logger.info("Creating new MoodleUserProviderInstance(pid={}, url={}, cacheSize={}, cacheExpiration={})", pid,
193 client.getURL(), cacheSize, cacheExpiration);
194
195
196 cache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheExpiration, TimeUnit.MINUTES)
197 .build(new CacheLoader<String, Object>() {
198 @Override
199 public Object load(String username) {
200 User user = loadUserFromMoodle(username);
201 return user == null ? nullToken : user;
202 }
203 });
204
205 registerMBean(pid);
206 }
207
208
209
210
211
212
213
214
215
216 @Override
217 public float getCacheHitRatio() {
218 if (loadUserRequests.get() == 0) {
219 return 0;
220 }
221 return (float) (loadUserRequests.get() - moodleWebServiceRequests.get()) / loadUserRequests.get();
222 }
223
224
225
226
227 private void registerMBean(String pid) {
228
229 loadUserRequests = new AtomicLong();
230 moodleWebServiceRequests = new AtomicLong();
231 try {
232 ObjectName name;
233 name = MoodleUserProviderFactory.getObjectName(pid);
234 Object mbean = this;
235 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
236 try {
237 mbs.unregisterMBean(name);
238 } catch (InstanceNotFoundException e) {
239 logger.debug("{} was not registered", name);
240 }
241 mbs.registerMBean(mbean, name);
242 } catch (Exception e) {
243 logger.error("Unable to register {} as an mbean", this, e);
244 }
245 }
246
247
248
249
250
251
252
253
254
255 @Override
256 public String getName() {
257 return PROVIDER_NAME;
258 }
259
260
261
262
263
264
265 @Override
266 public Iterator<User> getUsers() {
267
268 return Collections.emptyIterator();
269 }
270
271
272
273
274
275
276 @Override
277 public User loadUser(String userName) {
278 loadUserRequests.incrementAndGet();
279 try {
280 Object user = cache.getUnchecked(userName);
281 if (user == nullToken) {
282 logger.debug("Returning null user from cache");
283 return null;
284 } else {
285 logger.debug("Returning user {} from cache", userName);
286 return (User) user;
287 }
288 } catch (ExecutionError e) {
289 logger.warn("Exception while loading user {}", userName, e);
290 return null;
291 } catch (UncheckedExecutionException e) {
292 logger.warn("Exception while loading user {}", userName, e);
293 return null;
294 }
295 }
296
297
298
299
300
301
302 @Override
303 public long countUsers() {
304
305 return 0;
306 }
307
308
309
310
311
312
313 @Override
314 public String getOrganization() {
315 return organization.getId();
316 }
317
318
319
320
321
322
323 @Override
324 public Iterator<User> findUsers(String query, int offset, int limit) {
325 if (query == null) {
326 throw new IllegalArgumentException("Query must be set");
327 }
328
329 if (query.endsWith("%")) {
330 query = query.substring(0, query.length() - 1);
331 }
332
333 if (query.isEmpty()) {
334 return Collections.emptyIterator();
335 }
336
337
338 try {
339 if ((userPattern != null) && !query.matches(userPattern)) {
340 logger.debug("verify user {} failed regexp {}", query, userPattern);
341 return Collections.emptyIterator();
342 }
343 } catch (PatternSyntaxException e) {
344 logger.warn("Invalid regular expression for user pattern {} - disabling checks", userPattern);
345 userPattern = null;
346 }
347
348
349 List<User> users = new LinkedList<>();
350
351 User user = loadUser(query);
352 if (user != null) {
353 users.add(user);
354 }
355
356 return users.iterator();
357 }
358
359
360
361
362
363
364 @Override
365 public void invalidate(String userName) {
366 cache.invalidate(userName);
367 }
368
369
370
371
372
373
374
375
376
377 @Override
378 public List<Role> getRolesForUser(String username) {
379 List<Role> roles = new LinkedList<>();
380
381
382 if (ignoredUsernames.stream().anyMatch(u -> u.equals(username))) {
383 logger.debug("we don't answer for: {}", username);
384 return roles;
385 }
386
387 User user = loadUser(username);
388 if (user != null) {
389 logger.debug("Returning cached role set for {}", username);
390 return new ArrayList<>(user.getRoles());
391 }
392
393
394 logger.debug("Return empty role set for {} - not found in Moodle", username);
395 return new LinkedList<>();
396 }
397
398
399
400
401
402
403 @Override
404 public Iterator<Role> findRoles(String query, Role.Target target, int offset, int limit) {
405
406 if (target == Role.Target.USER) {
407 return Collections.emptyIterator();
408 }
409
410 boolean exact = true;
411 boolean ltirole = false;
412
413 if (query.endsWith("%")) {
414 exact = false;
415 query = query.substring(0, query.length() - 1);
416 }
417
418 if (query.isEmpty()) {
419 return Collections.emptyIterator();
420 }
421
422
423 if (!query.startsWith(contextRolePrefix)) {
424 return Collections.emptyIterator();
425 }
426
427
428 if (exact
429 && !query.endsWith("_" + LEARNER_ROLE_SUFFIX)
430 && !query.endsWith("_" + INSTRUCTOR_ROLE_SUFFIX)
431 && !query.endsWith("_" + GROUP_ROLE_SUFFIX)) {
432 return Collections.emptyIterator();
433 }
434
435 final String groupRolePrefix = contextRolePrefix + GROUP_ROLE_PREFIX;
436 final boolean findGroupRole = groupRoles && query.startsWith(groupRolePrefix);
437
438
439 String moodleId = findGroupRole ? query.substring(groupRolePrefix.length()) : query;
440 if (query.endsWith("_" + LEARNER_ROLE_SUFFIX)) {
441 moodleId = query.substring(contextRolePrefix.length(), query.lastIndexOf("_" + LEARNER_ROLE_SUFFIX));
442 ltirole = true;
443 } else if (query.endsWith("_" + INSTRUCTOR_ROLE_SUFFIX)) {
444 moodleId = query.substring(contextRolePrefix.length(), query.lastIndexOf("_" + INSTRUCTOR_ROLE_SUFFIX));
445 ltirole = true;
446 } else if (query.endsWith("_" + GROUP_ROLE_SUFFIX)) {
447 moodleId = query.substring(contextRolePrefix.length(), query.lastIndexOf("_" + GROUP_ROLE_SUFFIX));
448 ltirole = true;
449 }
450
451
452 String pattern = findGroupRole ? groupPattern : coursePattern;
453 try {
454 if ((pattern != null) && !moodleId.matches(pattern)) {
455 logger.debug("Verify Moodle ID {} failed regexp {}", moodleId, pattern);
456 return Collections.emptyIterator();
457 }
458 } catch (PatternSyntaxException e) {
459 logger.warn("Invalid regular expression for pattern {} - disabling checks", pattern);
460 if (findGroupRole) {
461 groupPattern = null;
462 } else {
463 coursePattern = null;
464 }
465 }
466
467
468 List<Role> roles = new LinkedList<>();
469 JaxbOrganization jaxbOrganization = JaxbOrganization.fromOrganization(organization);
470 if (ltirole) {
471
472 roles.add(new JaxbRole(query, jaxbOrganization, "Moodle Site Role", Role.Type.EXTERNAL));
473 } else if (findGroupRole) {
474
475 roles.add(new JaxbRole(contextRolePrefix + GROUP_ROLE_PREFIX + moodleId + "_" + GROUP_ROLE_SUFFIX,
476 jaxbOrganization, "Moodle Group Learner Role", Role.Type.EXTERNAL));
477 } else {
478
479 roles.add(new JaxbRole(moodleId + "_" + INSTRUCTOR_ROLE_SUFFIX, jaxbOrganization,
480 "Moodle Course Instructor Role", Role.Type.EXTERNAL));
481 roles.add(new JaxbRole(moodleId + "_" + LEARNER_ROLE_SUFFIX, jaxbOrganization, "Moodle Course Learner Role",
482 Role.Type.EXTERNAL));
483 }
484
485 return roles.iterator();
486 }
487
488
489
490
491
492
493
494
495
496
497 private User loadUserFromMoodle(String username) {
498 if (lowercaseUsername) {
499 username = username.toLowerCase();
500 }
501
502 logger.debug("loadUserFromMoodle({})", username);
503
504 if (cache == null) {
505 throw new IllegalStateException("The Moodle user detail service has not yet been configured");
506 }
507
508
509 if (ignoredUsernames.contains(username)) {
510 logger.debug("We don't answer for: " + username);
511 return null;
512 }
513
514 JaxbOrganization jaxbOrganization = JaxbOrganization.fromOrganization(organization);
515
516
517 moodleWebServiceRequests.incrementAndGet();
518
519 Thread currentThread = Thread.currentThread();
520 ClassLoader originalClassloader = currentThread.getContextClassLoader();
521
522 try {
523
524 List<MoodleUser> moodleUsers = client
525 .coreUserGetUsersByField(CoreUserGetUserByFieldFilters.username, Collections.singletonList(username));
526
527 if (moodleUsers.isEmpty()) {
528 logger.debug("User {} not found in Moodle system", username);
529 return null;
530 }
531 MoodleUser moodleUser = moodleUsers.get(0);
532
533
534 List<String> courseIdsInstructor = client.toolOpencastGetCoursesForInstructor(username);
535 List<String> courseIdsLearner = client.toolOpencastGetCoursesForLearner(username);
536 List<String> groupIdsLearner = groupRoles
537 ? client.toolOpencastGetGroupsForLearner(username)
538 : Collections.emptyList();
539
540
541 Set<JaxbRole> roles = new HashSet<>();
542 roles.add(new JaxbRole(Group.ROLE_PREFIX + contextRolePrefix + "MOODLE", jaxbOrganization, "Moodle Users",
543 Role.Type.EXTERNAL_GROUP));
544 for (final String courseId : courseIdsInstructor) {
545 roles.add(contextRole(courseId, INSTRUCTOR_ROLE_SUFFIX, jaxbOrganization));
546 }
547 for (final String courseId : courseIdsLearner) {
548 roles.add(contextRole(courseId, LEARNER_ROLE_SUFFIX, jaxbOrganization));
549 }
550 for (final String groupId : groupIdsLearner) {
551 roles.add(contextRole(GROUP_ROLE_PREFIX + groupId, GROUP_ROLE_SUFFIX, jaxbOrganization));
552 }
553
554 return new JaxbUser(moodleUser.getUsername(), null, moodleUser.getFullname(), moodleUser.getEmail(),
555 this.getName(), jaxbOrganization, roles);
556 } catch (Exception e) {
557 logger.warn("Exception loading Moodle user {} at {}", username, client.getURL());
558 } finally {
559 currentThread.setContextClassLoader(originalClassloader);
560 }
561
562 return null;
563 }
564
565
566
567
568
569
570
571
572
573 private JaxbRole contextRole(final String context, final String contextRole, final JaxbOrganization organization) {
574 final String name = contextRolePrefix + context + "_" + contextRole;
575 final String description = "Moodle Course " + contextRole + " Role";
576 return new JaxbRole(name, organization, description, Role.Type.EXTERNAL);
577 }
578 }