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.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  //import org.apache.commons.lang3.StringUtils;
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     * Constructs a Brighspace user provider with the needed settings
77     *
78     * @param pid             The PID of this service.
79     * @param client          The Brightspace rest service client.
80     * @param organization    The organisation.
81     * @param cacheSize       The number of users to cache.
82     * @param cacheExpiration The number of minutes to cache users.
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    * {@inheritDoc}
116    *
117    * @see org.opencastproject.security.api.UserProvider#getName()
118    */
119   @Override
120   public String getName() {
121     return pid;
122   }
123 
124   /**
125    * {@inheritDoc}
126    *
127    * @see org.opencastproject.security.api.UserProvider#getUsers()
128    */
129   @Override
130   public Iterator<User> getUsers() {
131     return Collections.emptyIterator();
132   }
133 
134   /**
135    * {@inheritDoc}
136    *
137    * @see org.opencastproject.security.api.UserProvider#loadUser(java.lang.String)
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    * {@inheritDoc}
161    *
162    * @see org.opencastproject.security.api.UserProvider#countUsers()
163    */
164   @Override
165   public long countUsers() {
166     return 0L;
167   }
168 
169   /**
170    * {@inheritDoc}
171    *
172    * @see org.opencastproject.security.api.UserProvider#getOrganization()
173    */
174   @Override
175   public String getOrganization() {
176     return this.organization.getId();
177   }
178 
179   /**
180    * {@inheritDoc}
181    *
182    * @see org.opencastproject.security.api.UserProvider#findUsers(java.lang.String, int, int)
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 }