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