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