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.studip;
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.User;
33 import org.opencastproject.security.api.UserProvider;
34
35 import com.google.common.cache.CacheBuilder;
36 import com.google.common.cache.CacheLoader;
37 import com.google.common.cache.LoadingCache;
38 import com.google.common.util.concurrent.ExecutionError;
39 import com.google.common.util.concurrent.UncheckedExecutionException;
40
41 import org.apache.http.client.config.RequestConfig;
42 import org.apache.http.client.methods.CloseableHttpResponse;
43 import org.apache.http.client.methods.HttpGet;
44 import org.apache.http.client.utils.URIBuilder;
45 import org.apache.http.impl.client.CloseableHttpClient;
46 import org.apache.http.impl.client.HttpClientBuilder;
47 import org.json.simple.JSONArray;
48 import org.json.simple.JSONObject;
49 import org.json.simple.parser.JSONParser;
50 import org.json.simple.parser.ParseException;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
53
54 import java.io.BufferedReader;
55 import java.io.IOException;
56 import java.io.InputStreamReader;
57 import java.lang.management.ManagementFactory;
58 import java.net.URI;
59 import java.net.URISyntaxException;
60 import java.util.ArrayList;
61 import java.util.Collections;
62 import java.util.HashSet;
63 import java.util.Iterator;
64 import java.util.LinkedList;
65 import java.util.List;
66 import java.util.Objects;
67 import java.util.Set;
68 import java.util.concurrent.TimeUnit;
69 import java.util.concurrent.atomic.AtomicLong;
70
71 import javax.management.InstanceNotFoundException;
72 import javax.management.MBeanServer;
73 import javax.management.ObjectName;
74
75
76
77
78 public class StudipUserProviderInstance implements UserProvider, RoleProvider, CachingUserProviderMXBean {
79
80 public static final String PROVIDER_NAME = "studip";
81
82 private static final String OC_USERAGENT = "Opencast";
83 private static final String STUDIP_GROUP = Group.ROLE_PREFIX + "STUDIP";
84
85
86 private static final Logger logger = LoggerFactory.getLogger(StudipUserProviderInstance.class);
87
88
89 private Organization organization = null;
90
91
92 private AtomicLong requests = null;
93
94
95 private AtomicLong studipLoads = null;
96
97
98 private LoadingCache<String, Object> cache = null;
99
100
101 protected Object nullToken = new Object();
102
103
104 private URI studipUrl;
105
106
107 private String studipToken = null;
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125 public StudipUserProviderInstance(
126 String pid,
127 Organization organization,
128 URI url,
129 String token,
130
131 int cacheSize,
132 int cacheExpiration
133 ) {
134
135 this.organization = organization;
136 this.studipUrl = url;
137 this.studipToken = token;
138
139 logger.info("Creating new StudipUserProviderInstance(pid={}, url={}, cacheSize={}, cacheExpiration={})",
140 pid, url, cacheSize, cacheExpiration);
141
142
143 cache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheExpiration, TimeUnit.MINUTES)
144 .build(new CacheLoader<String, Object>() {
145 @Override
146 public Object load(String id) throws Exception {
147 User user = loadUserFromStudip(id);
148 return user == null ? nullToken : user;
149 }
150 });
151
152 registerMBean(pid);
153 }
154
155 @Override
156 public String getName() {
157 return PROVIDER_NAME;
158 }
159
160
161
162
163 protected void registerMBean(String pid) {
164
165 requests = new AtomicLong();
166 studipLoads = new AtomicLong();
167 try {
168 ObjectName name;
169 name = StudipUserProviderFactory.getObjectName(pid);
170 Object mbean = this;
171 MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
172 try {
173 mbs.unregisterMBean(name);
174 } catch (InstanceNotFoundException e) {
175 logger.debug("{} was not registered before", name);
176 }
177 mbs.registerMBean(mbean, name);
178 } catch (Exception e) {
179 logger.error("Unable to register {} as an mbean", this, e);
180 }
181 }
182
183
184
185
186
187
188
189
190 @Override
191 public String getOrganization() {
192 return organization.getId();
193 }
194
195
196
197
198
199
200 @Override
201 public User loadUser(String userName) {
202 logger.debug("loaduser({})", userName);
203
204 requests.incrementAndGet();
205 try {
206 Object user = cache.getUnchecked(userName);
207 if (user == nullToken) {
208 logger.debug("Returning null user from cache");
209 return null;
210 } else {
211 logger.debug("Returning user {} from cache", userName);
212 return (JaxbUser) user;
213 }
214 } catch (ExecutionError | UncheckedExecutionException e) {
215 logger.warn("Exception while loading user {}", userName, e);
216 return null;
217 }
218 }
219
220
221
222
223
224
225
226
227 protected User loadUserFromStudip(String userName) {
228 if (cache == null) {
229 throw new IllegalStateException("The Stud.IP user detail service has not yet been configured");
230 }
231
232
233 if ("admin".equals(userName) || "".equals(userName) || "anonymous".equals(userName)) {
234 cache.put(userName, nullToken);
235 logger.debug("we don't answer for {}", userName);
236 return null;
237 }
238
239 logger.debug("In loadUserFromStudip, currently processing user : {}", userName);
240
241 JaxbOrganization jaxbOrganization = JaxbOrganization.fromOrganization(organization);
242
243
244 studipLoads.incrementAndGet();
245
246 Thread currentThread = Thread.currentThread();
247 ClassLoader originalClassloader = currentThread.getContextClassLoader();
248 try {
249
250 JSONObject userJsonObj = getStudipUser(userName);
251 if (userJsonObj == null) {
252 return null;
253 }
254
255 Set<JaxbRole> roles = new HashSet<>();
256 if (userJsonObj.containsKey("roles")) {
257 JSONArray rolesArray = (JSONArray) userJsonObj.get("roles");
258 for (Object r : rolesArray) {
259 roles.add(new JaxbRole(r.toString(), jaxbOrganization, "Studip external role", Role.Type.EXTERNAL));
260 }
261 }
262
263
264 roles.add(new JaxbRole(STUDIP_GROUP, jaxbOrganization, "Studip Users", Role.Type.EXTERNAL_GROUP));
265 logger.debug("Returning JaxbRoles: {}", roles);
266
267
268 var email = Objects.toString(userJsonObj.get("email"), null);
269 var name = Objects.toString(userJsonObj.get("fullname"), null);
270
271 User user = new JaxbUser(userName, null, name, email, PROVIDER_NAME, jaxbOrganization, roles);
272
273 cache.put(userName, user);
274 logger.debug("Returning user {}", userName);
275
276 return user;
277
278 } catch (ParseException e) {
279 logger.error("Exception while parsing response from provider for user {}", userName, e);
280 return null;
281 } catch (IOException e) {
282 logger.error("Error requesting user data for user `{}`: {}", userName, e.getMessage());
283 return null;
284 } catch (URISyntaxException e) {
285 logger.error("Misspelled URI", e);
286 return null;
287 } finally {
288 currentThread.setContextClassLoader(originalClassloader);
289 }
290 }
291
292
293
294
295
296
297
298 private JSONObject getStudipUser(String uid) throws URISyntaxException, IOException, ParseException {
299
300 var apiPath = new URIBuilder().setPathSegments("opencast", "user", uid).getPath();
301 var url = new URIBuilder(studipUrl)
302 .setPath(studipUrl.getPath().replaceAll("/*$", "") + apiPath)
303 .addParameter("token", studipToken)
304 .build();
305
306
307 HttpGet get = new HttpGet(url);
308 get.setHeader("User-Agent", OC_USERAGENT);
309
310
311 RequestConfig config = RequestConfig.custom()
312 .setConnectTimeout(5000)
313 .setSocketTimeout(10000).build();
314
315 try (CloseableHttpClient client = HttpClientBuilder.create().setDefaultRequestConfig(config).build();) {
316 try (CloseableHttpResponse resp = client.execute(get)) {
317 var statusCode = resp.getStatusLine().getStatusCode();
318 if (statusCode == 404) {
319
320 return null;
321 } else if (statusCode / 100 != 2) {
322 throw new IOException("HttpRequest unsuccessful, reason: " + resp.getStatusLine().getReasonPhrase());
323 }
324
325
326 BufferedReader reader = new BufferedReader(new InputStreamReader(resp.getEntity().getContent()));
327 JSONParser parser = new JSONParser();
328 Object obj = parser.parse(reader);
329
330
331 if (!(obj instanceof JSONObject)) {
332 throw new IOException("StudIP responded in unexpected format");
333 }
334
335 JSONObject jObj = (JSONObject) obj;
336 if (jObj.containsKey("errors")) {
337 throw new IOException("Stud.IP returned an error: " + jObj.toJSONString());
338 }
339
340 return jObj;
341 }
342 }
343 }
344
345
346
347
348
349
350 @Override
351 public float getCacheHitRatio() {
352 if (requests.get() == 0) {
353 return 0;
354 }
355 return (float) (requests.get() - studipLoads.get()) / requests.get();
356 }
357
358 @Override
359 public Iterator<User> findUsers(String query, int offset, int limit) {
360
361 if (query == null) {
362 throw new IllegalArgumentException("Query must be set");
363 }
364
365 if (query.endsWith("%")) {
366 query = query.substring(0, query.length() - 1);
367 }
368
369 if (query.isEmpty()) {
370 return Collections.emptyIterator();
371 }
372
373 List<User> users = new LinkedList<User>();
374 JaxbOrganization jaxbOrganization = JaxbOrganization.fromOrganization(organization);
375 JaxbUser queryUser = new JaxbUser(query, PROVIDER_NAME, jaxbOrganization, new HashSet<JaxbRole>());
376 users.add(queryUser);
377
378 return users.iterator();
379 }
380
381 @Override
382 public Iterator<User> getUsers() {
383
384 return Collections.emptyIterator();
385 }
386
387 @Override
388 public void invalidate(String userName) {
389 cache.invalidate(userName);
390 }
391
392 @Override
393 public long countUsers() {
394
395 return 0;
396 }
397
398
399
400 @Override
401 public List<Role> getRolesForUser(String userName) {
402
403 List<Role> roles = new LinkedList<Role>();
404
405
406 if ("admin".equals(userName) || "".equals(userName) || "anonymous".equals(userName)) {
407 logger.debug("we don't answer for {}", userName);
408 return roles;
409 }
410
411 logger.debug("getRolesForUser({})", userName);
412
413 User user = loadUser(userName);
414 if (user != null) {
415 logger.debug("Returning cached role set for {}", userName);
416 return new ArrayList<Role>(user.getRoles());
417 }
418
419
420 logger.debug("Return empty role set for {} - not found on Stud.IP", userName);
421 return new LinkedList<>();
422 }
423
424 @Override
425 public Iterator<Role> findRoles(String query, Role.Target target, int offset, int limit) {
426 logger.debug("findRoles(query={} offset={} limit={})", query, offset, limit);
427
428
429 if (target == Role.Target.USER) {
430 return Collections.emptyIterator();
431 }
432
433 if (query.endsWith("%")) {
434 query = query.substring(0, query.length() - 1);
435 }
436
437 if (query.isEmpty()) {
438 return Collections.emptyIterator();
439 }
440
441
442 List<Role> roles = new LinkedList<>();
443
444 return roles.iterator();
445 }
446
447 }