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