1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 package org.opencastproject.userdirectory.brightspace;
24
25 import org.opencastproject.security.api.CachingUserProviderMXBean;
26 import org.opencastproject.security.api.Group;
27 import org.opencastproject.security.api.JaxbOrganization;
28 import org.opencastproject.security.api.JaxbRole;
29 import org.opencastproject.security.api.JaxbUser;
30 import org.opencastproject.security.api.Organization;
31 import org.opencastproject.security.api.Role;
32 import org.opencastproject.security.api.Role.Target;
33 import org.opencastproject.security.api.RoleProvider;
34 import org.opencastproject.security.api.User;
35 import org.opencastproject.security.api.UserProvider;
36 import org.opencastproject.userdirectory.brightspace.client.BrightspaceClient;
37 import org.opencastproject.userdirectory.brightspace.client.BrightspaceClientException;
38 import org.opencastproject.userdirectory.brightspace.client.api.BrightspaceUser;
39
40 import com.google.common.cache.CacheBuilder;
41 import com.google.common.cache.CacheLoader;
42 import com.google.common.cache.LoadingCache;
43 import com.google.common.util.concurrent.ExecutionError;
44 import com.google.common.util.concurrent.UncheckedExecutionException;
45
46
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import java.lang.management.ManagementFactory;
51 import java.util.ArrayList;
52 import java.util.Collections;
53 import java.util.HashSet;
54 import java.util.Iterator;
55 import java.util.List;
56 import java.util.Set;
57 import java.util.concurrent.TimeUnit;
58 import java.util.concurrent.atomic.AtomicLong;
59
60 import javax.management.InstanceNotFoundException;
61 import javax.management.MBeanServer;
62 import javax.management.ObjectName;
63
64 public class BrightspaceUserProviderInstance implements UserProvider, RoleProvider, CachingUserProviderMXBean {
65
66 private static final Logger logger = LoggerFactory.getLogger(BrightspaceUserProviderInstance.class);
67
68 private static final String LTI_LEARNER_ROLE = "Learner";
69 private static final String LTI_INSTRUCTOR_ROLE = "Instructor";
70
71 private String pid;
72 private BrightspaceClient client;
73 private Organization organization;
74 private LoadingCache<String, Object> cache;
75 private Object nullToken = new Object();
76 private AtomicLong loadUserRequests;
77 private AtomicLong brightspaceWebServiceRequests;
78 private final Set<String> instructorRoles;
79 private final Set<String> ignoredUsernames;
80
81
82
83
84
85
86
87
88
89
90 public BrightspaceUserProviderInstance(
91 String pid,
92 BrightspaceClient client,
93 Organization organization,
94 int cacheSize,
95 int cacheExpiration,
96 Set instructorRoles,
97 Set ignoredUsernames
98 ) {
99
100 this.pid = pid;
101 this.client = client;
102 this.organization = organization;
103 this.instructorRoles = instructorRoles;
104 this.ignoredUsernames = ignoredUsernames;
105
106 logger.info("Creating new BrightspaceUserProviderInstance(pid={}, url={}, cacheSize={}, cacheExpiration={}, "
107 + "InstructorRoles={}, ignoredUserNames={})", pid, client.getURL(), cacheSize, cacheExpiration,
108 instructorRoles, ignoredUsernames);
109
110 cache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheExpiration, TimeUnit.MINUTES)
111 .build(new CacheLoader<String, Object>() {
112 @Override
113 public Object load(String username) {
114 User user = loadUserFromBrightspace(username);
115 return user == null ? nullToken : user;
116 }
117 });
118
119 this.registerMBean(pid);
120 }
121
122 @Override
123 public float getCacheHitRatio() {
124 if (loadUserRequests.get() == 0) {
125 return 0;
126 }
127 return (float) (loadUserRequests.get() - brightspaceWebServiceRequests.get()) / loadUserRequests.get();
128 }
129
130 private void registerMBean(String pid) {
131 this.loadUserRequests = new AtomicLong();
132 this.brightspaceWebServiceRequests = new AtomicLong();
133
134 try {
135 ObjectName name = BrightspaceUserProviderFactory.getObjectName(pid);
136 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
137
138 try {
139 mbs.unregisterMBean(name);
140 } catch (InstanceNotFoundException ife) {
141 logger.debug("{} was not registered", name);
142 }
143
144 mbs.registerMBean(this, name);
145 } catch (Exception e) {
146 logger.error("Unable to register {} as an mbean", this, e);
147 }
148
149 }
150
151
152
153
154
155
156 @Override
157 public String getName() {
158 return pid;
159 }
160
161
162
163
164
165
166 @Override
167 public Iterator<User> getUsers() {
168 return Collections.emptyIterator();
169 }
170
171
172
173
174
175
176 @Override
177 public User loadUser(String userName) {
178 this.loadUserRequests.incrementAndGet();
179 logger.debug("getting user from cache");
180
181 try {
182 Object user = this.cache.getUnchecked(userName);
183 if (user != this.nullToken) {
184 logger.debug("Returning user {} from cache", userName);
185 return (User) user;
186 } else {
187 logger.debug("Returning null user from cache");
188 return null;
189 }
190 } catch (ExecutionError | UncheckedExecutionException ee) {
191 logger.warn("Exception while loading user {}", userName, ee);
192 return null;
193 }
194 }
195
196
197
198
199
200
201 @Override
202 public long countUsers() {
203 return 0L;
204 }
205
206
207
208
209
210
211 @Override
212 public String getOrganization() {
213 return this.organization.getId();
214 }
215
216
217
218
219
220
221 @Override
222 public Iterator<User> findUsers(String query, int offset, int limit) {
223 return Collections.emptyIterator();
224 }
225
226 @Override
227 public void invalidate(String userName) {
228 this.cache.invalidate(userName);
229 }
230
231 @Override
232 public List<Role> getRolesForUser(String username) {
233 User user = this.loadUser(username);
234 if (user != null) {
235 logger.debug("Returning cached role set for {}", username);
236 return new ArrayList<>(user.getRoles());
237 }
238 logger.debug("Return empty role set for {} - not found in Brightspace", username);
239 return Collections.emptyList();
240 }
241
242 @Override
243 public Iterator<Role> findRoles(String query, Target target, int offset, int limit) {
244 return Collections.emptyIterator();
245 }
246
247 private User loadUserFromBrightspace(String username) {
248 if (this.cache == null) {
249 throw new IllegalStateException("The Brightspace user detail service has not yet been configured");
250 } else if (ignoredUsernames.stream().anyMatch(u -> u.equals(username))) {
251 logger.debug("We don't answer for: " + username);
252 return null;
253 } else {
254
255 logger.debug("In loadUserFromBrightspace, currently processing user: {}", username);
256 JaxbOrganization jaxbOrganization = JaxbOrganization.fromOrganization(organization);
257
258 this.brightspaceWebServiceRequests.incrementAndGet();
259 Thread currentThread = Thread.currentThread();
260 ClassLoader originalClassloader = currentThread.getContextClassLoader();
261 BrightspaceUser brightspaceUser;
262
263 try {
264 brightspaceUser = this.client.findUser(username);
265
266 if (brightspaceUser != null) {
267 logger.info("Retrieved user {}", brightspaceUser.getUserId());
268 String brightspaceUserId = brightspaceUser.getUserId();
269
270 List<String> roleList = client.getRolesFromBrightspace(brightspaceUserId, instructorRoles);
271 logger.debug("Brightspace user {} with id {} with roles: {}", username, brightspaceUserId, roleList);
272
273 Set<JaxbRole> roles = new HashSet<>();
274 boolean isInstructor = false;
275 for (String roleStr: roleList) {
276 roles.add(new JaxbRole(roleStr, jaxbOrganization, "Brightspace external role", Role.Type.EXTERNAL));
277 if (roleStr.endsWith(LTI_INSTRUCTOR_ROLE)) {
278 isInstructor = true;
279 }
280 }
281 roles.add(new JaxbRole(Group.ROLE_PREFIX + "BRIGHTSPACE", jaxbOrganization, "Brightspace User",
282 Role.Type.EXTERNAL_GROUP));
283 if (isInstructor) {
284 roles.add(new JaxbRole(Group.ROLE_PREFIX + "BRIGHTSPACE_INSTRUCTOR", jaxbOrganization,
285 "Brightspace Instructor", Role.Type.EXTERNAL_GROUP));
286 }
287 logger.debug("Returning JaxbRoles: {}", roles);
288
289 User user = new JaxbUser(username, null, brightspaceUser.getDisplayName(),
290 brightspaceUser.getExternalEmail(), this.getName(), jaxbOrganization, roles);
291 cache.put(username, user);
292 logger.debug("Returning user {}", user);
293 return user;
294 } else {
295 cache.put(username, nullToken);
296 logger.debug("User {} not found in Brightspace system", username);
297 return null;
298 }
299 } catch (BrightspaceClientException e) {
300 logger.error("A Brightspace API error ( {} ) occurred, user {} could not be retrieved", e, username);
301 return null;
302 } finally {
303 currentThread.setContextClassLoader(originalClassloader);
304 }
305 }
306 }
307 }