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.Organization;
26  import org.opencastproject.security.api.OrganizationDirectoryService;
27  import org.opencastproject.security.api.RoleProvider;
28  import org.opencastproject.security.api.SecurityConstants;
29  import org.opencastproject.security.api.UserProvider;
30  import org.opencastproject.userdirectory.brightspace.client.BrightspaceClientImpl;
31  import org.opencastproject.util.NotFoundException;
32  
33  import org.apache.commons.lang3.StringUtils;
34  import org.apache.commons.lang3.math.NumberUtils;
35  import org.osgi.framework.BundleContext;
36  import org.osgi.framework.ServiceRegistration;
37  import org.osgi.service.cm.ConfigurationException;
38  import org.osgi.service.cm.ManagedServiceFactory;
39  import org.osgi.service.component.ComponentContext;
40  import org.osgi.service.component.annotations.Activate;
41  import org.osgi.service.component.annotations.Component;
42  import org.osgi.service.component.annotations.Reference;
43  import org.slf4j.Logger;
44  import org.slf4j.LoggerFactory;
45  
46  import java.lang.management.ManagementFactory;
47  import java.net.URI;
48  import java.net.URISyntaxException;
49  import java.util.Dictionary;
50  import java.util.HashSet;
51  import java.util.Map;
52  import java.util.Set;
53  import java.util.concurrent.ConcurrentHashMap;
54  
55  import javax.management.MalformedObjectNameException;
56  import javax.management.ObjectName;
57  
58  @Component(
59      immediate = true,
60      service = ManagedServiceFactory.class,
61      property = {
62          "service.pid=org.opencastproject.userdirectory.brightspace",
63          "service.description=Provides Brightspace user directory instances"
64      }
65  )
66  public class BrightspaceUserProviderFactory implements ManagedServiceFactory {
67  
68    private static final Logger logger = LoggerFactory.getLogger(BrightspaceUserProviderFactory.class);
69  
70    private static final String LTI_LEARNER_ROLE = "Learner";
71    private static final String LTI_INSTRUCTOR_ROLE = "Instructor";
72  
73    private static final String ORGANIZATION_KEY = "org.opencastproject.userdirectory.brightspace.org";
74    private static final String BRIGHTSPACE_USER_ID = "org.opencastproject.userdirectory.brightspace.systemuser.id";
75    private static final String BRIGHTSPACE_USER_KEY = "org.opencastproject.userdirectory.brightspace.systemuser.key";
76    private static final String BRIGHTSPACE_URL = "org.opencastproject.userdirectory.brightspace.url";
77    private static final String BRIGHTSPACE_APP_ID = "org.opencastproject.userdirectory.brightspace.application.id";
78    private static final String BRIGHTSPACE_APP_KEY = "org.opencastproject.userdirectory.brightspace.application.key";
79  
80    private static final String CACHE_SIZE_KEY = "org.opencastproject.userdirectory.brightspace.cache.size";
81    private static final String CACHE_EXPIRATION_KEY = "org.opencastproject.userdirectory.brightspace.cache.expiration";
82    private static final String BRIGHTSPACE_NAME = "org.opencastproject.userdirectory.brightspace";
83    private static final int DEFAULT_CACHE_SIZE_VALUE = 1000;
84    private static final int DEFAULT_CACHE_EXPIRATION_VALUE = 60;
85  
86    /** The keys to look up which roles in Brightspace should be considered as instructor roles */
87    private static final String BRIGHTSPACE_INSTRUCTOR_ROLES_KEY =
88                                  "org.opencastproject.userdirectory.brightspace.instructor.roles";
89    private static final String DEFAULT_BRIGHTSPACE_INSTRUCTOR_ROLES = "teacher,ta";
90    /** The keys to look up which users should be ignored */
91    private static final String IGNORED_USERNAMES_KEY = "org.opencastproject.userdirectory.brightspace.ignored.usernames";
92    private static final String DEFAULT_IGNORED_USERNAMES = "admin,anonymous";
93  
94    protected BundleContext bundleContext;
95    private Map<String, ServiceRegistration> providerRegistrations = new ConcurrentHashMap<>();
96    private OrganizationDirectoryService orgDirectory;
97    private int cacheSize;
98    private int cacheExpiration;
99  
100   /**
101    * Builds a JMX object name for a given PID
102    *
103    * @param pid the PID
104    * @return the object name
105    * @throws NullPointerException
106    * @throws MalformedObjectNameException
107    */
108   public static final ObjectName getObjectName(String pid) throws MalformedObjectNameException, NullPointerException {
109     return new ObjectName(pid + ":type=BrightspaceRequests");
110   }
111 
112   /**
113    * OSGi callback for setting the organization directory service.
114    */
115   @Reference
116   public void setOrgDirectory(OrganizationDirectoryService orgDirectory) {
117     this.orgDirectory = orgDirectory;
118   }
119 
120   /**
121    * Callback for the activation of this component
122    *
123    * @param cc the component context
124    */
125   @Activate
126   public void activate(ComponentContext cc) {
127     logger.debug("Activate BrightspaceUserProviderFactory");
128     this.bundleContext = cc.getBundleContext();
129   }
130 
131   /**
132    * {@inheritDoc}
133    *
134    * @see org.osgi.service.cm.ManagedServiceFactory#getName()
135    */
136   @Override
137   public String getName() {
138     return BRIGHTSPACE_NAME;
139   }
140 
141   /**
142    * {@inheritDoc}
143    *
144    * @see org.osgi.service.cm.ManagedServiceFactory#updated(java.lang.String, java.util.Dictionary)
145    */
146   @Override
147   public void updated(String pid, Dictionary properties) throws ConfigurationException {
148     logger.debug("updated BrightspaceUserProviderFactory");
149     String adminUserName = StringUtils.trimToNull(
150         bundleContext.getProperty(SecurityConstants.GLOBAL_ADMIN_USER_PROPERTY));
151     String organization = (String) properties.get(ORGANIZATION_KEY);
152     String urlStr = (String) properties.get(BRIGHTSPACE_URL);
153     String systemUserId = (String) properties.get(BRIGHTSPACE_USER_ID);
154     String systemUserKey = (String) properties.get(BRIGHTSPACE_USER_KEY);
155     final String applicationId = (String) properties.get(BRIGHTSPACE_APP_ID);
156     final String applicationKey = (String) properties.get(BRIGHTSPACE_APP_KEY);
157 
158     String cacheSizeStr = (String) properties.get(CACHE_SIZE_KEY);
159     if (StringUtils.isBlank(cacheSizeStr)) {
160       cacheSize = DEFAULT_CACHE_SIZE_VALUE;
161     } else {
162       cacheSize = NumberUtils.toInt(cacheSizeStr);
163     }
164 
165 
166     String cacheExpirationStr = (String) properties.get(CACHE_EXPIRATION_KEY);
167     if (StringUtils.isBlank(cacheExpirationStr)) {
168       cacheExpiration = DEFAULT_CACHE_EXPIRATION_VALUE;
169     } else {
170       cacheExpiration = NumberUtils.toInt(cacheExpirationStr);
171     }
172 
173     String rolesStr = (String) properties.get(BRIGHTSPACE_INSTRUCTOR_ROLES_KEY);
174     if (StringUtils.isBlank(rolesStr)) {
175       rolesStr = DEFAULT_BRIGHTSPACE_INSTRUCTOR_ROLES;
176     }
177     Set instructorRoles = parsePropertyLineAsSet(rolesStr);
178     logger.debug("Brightspace instructor roles: {}", instructorRoles);
179 
180     String ignoredUsersStr = (String) properties.get(IGNORED_USERNAMES_KEY);
181     if (StringUtils.isBlank(ignoredUsersStr)) {
182       ignoredUsersStr = DEFAULT_IGNORED_USERNAMES;
183     }
184     Set ignoredUsernames = parsePropertyLineAsSet(ignoredUsersStr);
185     logger.debug("Ignored users: {}", ignoredUsernames);
186 
187     validateUrl(urlStr);
188     validateConfigurationKey(ORGANIZATION_KEY, organization);
189     validateConfigurationKey(BRIGHTSPACE_USER_ID, systemUserId);
190     validateConfigurationKey(BRIGHTSPACE_USER_KEY, systemUserKey);
191     validateConfigurationKey(BRIGHTSPACE_APP_ID, applicationId);
192     validateConfigurationKey(BRIGHTSPACE_APP_KEY, applicationKey);
193 
194     ServiceRegistration existingRegistration = this.providerRegistrations.remove(pid);
195     if (existingRegistration != null) {
196       existingRegistration.unregister();
197     }
198 
199     Organization org;
200     try {
201       org = this.orgDirectory.getOrganization(organization);
202     } catch (NotFoundException nfe) {
203       logger.warn("Organization {} not found!", organization);
204       throw new ConfigurationException(ORGANIZATION_KEY, "not found");
205     }
206 
207     logger.debug("creating new brightspace user provider for pid={}", pid);
208 
209     BrightspaceClientImpl clientImpl
210         = new BrightspaceClientImpl(urlStr, applicationId, applicationKey, systemUserId, systemUserKey);
211     BrightspaceUserProviderInstance provider
212         = new BrightspaceUserProviderInstance(pid, clientImpl, org, cacheSize, cacheExpiration  ,
213             instructorRoles, ignoredUsernames);
214     this.providerRegistrations
215         .put(pid, this.bundleContext.registerService(UserProvider.class.getName(), provider, null));
216     this.providerRegistrations
217         .put(pid, this.bundleContext.registerService(RoleProvider.class.getName(), provider, null));
218   }
219 
220   /**
221    * {@inheritDoc}
222    *
223    * @see org.osgi.service.cm.ManagedServiceFactory#deleted(java.lang.String)
224    */
225   @Override
226   public void deleted(String pid) {
227     logger.debug("delete BrightspaceUserProviderInstance for pid={}", pid);
228     ServiceRegistration registration = providerRegistrations.remove(pid);
229     if (registration != null) {
230       registration.unregister();
231 
232       try {
233         ManagementFactory.getPlatformMBeanServer().unregisterMBean(getObjectName(pid));
234       } catch (Exception e) {
235         logger.warn("Unable to unregister mbean for pid='{}'", pid, e);
236       }
237     }
238   }
239 
240   private void validateConfigurationKey(String key, String value) throws ConfigurationException {
241     if (StringUtils.isBlank(value)) {
242       throw new ConfigurationException(key, "is not set");
243     }
244   }
245 
246   private void validateUrl(String urlStr) throws ConfigurationException {
247     if (StringUtils.isBlank(urlStr)) {
248       throw new ConfigurationException(BRIGHTSPACE_URL, "is not set");
249     } else {
250       try {
251         new URI(urlStr);
252       } catch (URISyntaxException e) {
253         throw new ConfigurationException(BRIGHTSPACE_URL, "not a URL");
254       }
255     }
256   }
257 
258   private Set<String> parsePropertyLineAsSet(String configLine) {
259     Set<String> set = new HashSet<>();
260     String[] configs = configLine.split(",");
261     for (String config: configs) {
262       set.add(config.trim());
263     }
264     return set;
265   }
266 
267 }