View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
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  //import org.apache.commons.lang3.StringUtils;
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     * Constructs a Brighspace user provider with the needed settings
83     *
84     * @param pid             The PID of this service.
85     * @param client          The Brightspace rest service client.
86     * @param organization    The organisation.
87     * @param cacheSize       The number of users to cache.
88     * @param cacheExpiration The number of minutes to cache users.
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    * {@inheritDoc}
153    *
154    * @see org.opencastproject.security.api.UserProvider#getName()
155    */
156   @Override
157   public String getName() {
158     return pid;
159   }
160 
161   /**
162    * {@inheritDoc}
163    *
164    * @see org.opencastproject.security.api.UserProvider#getUsers()
165    */
166   @Override
167   public Iterator<User> getUsers() {
168     return Collections.emptyIterator();
169   }
170 
171   /**
172    * {@inheritDoc}
173    *
174    * @see org.opencastproject.security.api.UserProvider#loadUser(java.lang.String)
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    * {@inheritDoc}
198    *
199    * @see org.opencastproject.security.api.UserProvider#countUsers()
200    */
201   @Override
202   public long countUsers() {
203     return 0L;
204   }
205 
206   /**
207    * {@inheritDoc}
208    *
209    * @see org.opencastproject.security.api.UserProvider#getOrganization()
210    */
211   @Override
212   public String getOrganization() {
213     return this.organization.getId();
214   }
215 
216   /**
217    * {@inheritDoc}
218    *
219    * @see org.opencastproject.security.api.UserProvider#findUsers(java.lang.String, int, int)
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 }