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  package org.opencastproject.userdirectory.moodle;
23  
24  import org.opencastproject.security.api.Organization;
25  import org.opencastproject.security.api.OrganizationDirectoryService;
26  import org.opencastproject.security.api.RoleProvider;
27  import org.opencastproject.security.api.SecurityConstants;
28  import org.opencastproject.security.api.UserProvider;
29  import org.opencastproject.util.NotFoundException;
30  
31  import org.apache.commons.lang3.BooleanUtils;
32  import org.apache.commons.lang3.StringUtils;
33  import org.osgi.framework.BundleContext;
34  import org.osgi.framework.ServiceRegistration;
35  import org.osgi.service.cm.ConfigurationException;
36  import org.osgi.service.cm.ManagedServiceFactory;
37  import org.osgi.service.component.ComponentContext;
38  import org.osgi.service.component.annotations.Activate;
39  import org.osgi.service.component.annotations.Component;
40  import org.osgi.service.component.annotations.Reference;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  import java.lang.management.ManagementFactory;
45  import java.net.URI;
46  import java.net.URISyntaxException;
47  import java.util.Dictionary;
48  import java.util.Map;
49  import java.util.Objects;
50  import java.util.concurrent.ConcurrentHashMap;
51  
52  import javax.management.MalformedObjectNameException;
53  import javax.management.ObjectName;
54  
55  /**
56   * Moodle implementation of the spring UserDetailsService, taking configuration information from the component context.
57   */
58  @Component(
59      immediate = true,
60      service = ManagedServiceFactory.class,
61      property = {
62          "service.pid=org.opencastproject.userdirectory.moodle",
63          "service.description=Provides Moodle user directory instances"
64      }
65  )
66  public class MoodleUserProviderFactory implements ManagedServiceFactory {
67    /**
68     * This service factory's PID
69     */
70    public static final String PID = "org.opencastproject.userdirectory.moodle";
71  
72    /**
73     * The logger
74     */
75    private static final Logger logger = LoggerFactory.getLogger(MoodleUserProviderFactory.class);
76  
77    /**
78     * The key to look up the organization identifier in the service configuration properties
79     */
80    private static final String ORGANIZATION_KEY = "org.opencastproject.userdirectory.moodle.org";
81  
82    /**
83     * The key to look up the REST webservice URL of the Moodle instance
84     */
85    private static final String MOODLE_URL_KEY = "org.opencastproject.userdirectory.moodle.url";
86  
87    /**
88     * The key to look up the user token to use for performing searches.
89     */
90    private static final String MOODLE_TOKEN_KEY = "org.opencastproject.userdirectory.moodle.token";
91  
92    /**
93     * The key to look up the number of user records to cache
94     */
95    private static final String CACHE_SIZE = "org.opencastproject.userdirectory.moodle.cache.size";
96  
97    /**
98     * The key to look up the number of minutes to cache users
99     */
100   private static final String CACHE_EXPIRATION = "org.opencastproject.userdirectory.moodle.cache.expiration";
101 
102   /**
103    * The key to look up whether to activate group roles
104    */
105   private static final String GROUP_ROLES_KEY = "org.opencastproject.userdirectory.moodle.group.roles.enabled";
106 
107   /**
108    * The key to look up the regular expression used to validate courses
109    */
110   private static final String COURSE_PATTERN_KEY = "org.opencastproject.userdirectory.moodle.course.pattern";
111 
112   /**
113    * The key to look up the regular expression used to validate users
114    */
115   private static final String USER_PATTERN_KEY = "org.opencastproject.userdirectory.moodle.user.pattern";
116 
117   /**
118    * The key to look up the regular expression used to validate groups
119    */
120   private static final String GROUP_PATTERN_KEY = "org.opencastproject.userdirectory.moodle.group.pattern";
121 
122   /** Key specifying if usernames should be converted to lowercase */
123   private static final String LOWERCASE_USERNAME = "org.opencastproject.userdirectory.moodle.user.lowercase.conversion";
124 
125   /**
126    * Key for configuring a context role prefix
127    */
128   private static final String CONTEXT_ROLE_PREFIX = "org.opencastproject.userdirectory.moodle.context.role.prefix";
129 
130   /**
131    * The OSGI bundle context
132    */
133   protected BundleContext bundleContext = null;
134 
135   /**
136    * A map of pid to moodle user provider instance
137    */
138   private Map<String, ServiceRegistration> providerRegistrations = new ConcurrentHashMap<String, ServiceRegistration>();
139 
140   /**
141    * The organization directory service
142    */
143   private OrganizationDirectoryService orgDirectory;
144 
145   /**
146    * Builds a JMX object name for a given PID
147    *
148    * @param pid the PID
149    * @return the object name
150    * @throws NullPointerException
151    * @throws MalformedObjectNameException
152    */
153   public static ObjectName getObjectName(String pid) throws MalformedObjectNameException, NullPointerException {
154     return new ObjectName(pid + ":type=MoodleRequests");
155   }
156 
157   /**
158    * OSGi callback for setting the organization directory service.
159    */
160   @Reference
161   public void setOrgDirectory(OrganizationDirectoryService orgDirectory) {
162     this.orgDirectory = orgDirectory;
163   }
164 
165   /**
166    * Callback for the activation of this component
167    *
168    * @param cc the component context
169    */
170   @Activate
171   public void activate(ComponentContext cc) {
172     logger.debug("Activate MoodleUserProviderFactory");
173     this.bundleContext = cc.getBundleContext();
174   }
175 
176   /**
177    * {@inheritDoc}
178    *
179    * @see org.osgi.service.cm.ManagedServiceFactory#getName()
180    */
181   @Override
182   public String getName() {
183     return PID;
184   }
185 
186   /**
187    * {@inheritDoc}
188    *
189    * @see org.osgi.service.cm.ManagedServiceFactory#updated(java.lang.String, java.util.Dictionary)
190    */
191   @Override
192   public void updated(String pid, Dictionary properties) throws ConfigurationException {
193     logger.debug("updated MoodleUserProviderFactory");
194 
195     String adminUserName = StringUtils.trimToNull(
196         bundleContext.getProperty(SecurityConstants.GLOBAL_ADMIN_USER_PROPERTY)
197     );
198 
199     String organization = (String) properties.get(ORGANIZATION_KEY);
200     if (StringUtils.isBlank(organization)) {
201       throw new ConfigurationException(ORGANIZATION_KEY, "is not set");
202     }
203 
204     String urlStr = (String) properties.get(MOODLE_URL_KEY);
205     URI url;
206     if (StringUtils.isBlank(urlStr)) {
207       throw new ConfigurationException(MOODLE_URL_KEY, "is not set");
208     }
209     try {
210       url = new URI(urlStr);
211     } catch (URISyntaxException e) {
212       throw new ConfigurationException(MOODLE_URL_KEY, "not a URL");
213     }
214 
215     String token = (String) properties.get(MOODLE_TOKEN_KEY);
216     if (StringUtils.isBlank(token)) {
217       throw new ConfigurationException(MOODLE_TOKEN_KEY, "is not set");
218     }
219 
220     final String groupRolesStr = (String) properties.get(GROUP_ROLES_KEY);
221     final boolean groupRoles = BooleanUtils.toBoolean(groupRolesStr);
222 
223     String coursePattern = (String) properties.get(COURSE_PATTERN_KEY);
224     String userPattern = (String) properties.get(USER_PATTERN_KEY);
225     String groupPattern = (String) properties.get(GROUP_PATTERN_KEY);
226     final boolean lowercaseUsername = BooleanUtils.toBoolean((String) properties.get(LOWERCASE_USERNAME));
227 
228     final String contextRolePrefix = Objects.toString(properties.get(CONTEXT_ROLE_PREFIX), "");
229 
230     int cacheSize = 1000;
231     try {
232       if (properties.get(CACHE_SIZE) != null) {
233         cacheSize = Integer.parseInt(properties.get(CACHE_SIZE).toString());
234       }
235     } catch (NumberFormatException e) {
236       logger.warn("{} could not be loaded, default value is used: {}", CACHE_SIZE, cacheSize);
237     }
238 
239     int cacheExpiration = 60;
240     try {
241       if (properties.get(CACHE_EXPIRATION) != null) {
242         cacheExpiration = Integer.parseInt(properties.get(CACHE_EXPIRATION).toString());
243       }
244     } catch (NumberFormatException e) {
245       logger.warn("{} could not be loaded, default value is used: {}", CACHE_EXPIRATION, cacheExpiration);
246     }
247 
248     // Now that we have everything we need, go ahead and activate a new provider, removing an old one if necessary
249     ServiceRegistration existingRegistration = providerRegistrations.remove(pid);
250     if (existingRegistration != null) {
251       existingRegistration.unregister();
252     }
253 
254     Organization org;
255     try {
256       org = orgDirectory.getOrganization(organization);
257     } catch (NotFoundException e) {
258       logger.warn("Organization {} not found!", organization);
259       throw new ConfigurationException(ORGANIZATION_KEY, "not found");
260     }
261 
262     logger.debug("creating new MoodleUserProviderInstance for pid=" + pid);
263     MoodleUserProviderInstance provider = new MoodleUserProviderInstance(pid, new MoodleWebServiceImpl(url, token), org,
264         coursePattern, userPattern, groupPattern, groupRoles, cacheSize, cacheExpiration, adminUserName,
265         lowercaseUsername, contextRolePrefix);
266 
267     providerRegistrations.put(pid, bundleContext.registerService(UserProvider.class.getName(), provider, null));
268     providerRegistrations.put(pid, bundleContext.registerService(RoleProvider.class.getName(), provider, null));
269   }
270 
271   /**
272    * {@inheritDoc}
273    *
274    * @see org.osgi.service.cm.ManagedServiceFactory#deleted(java.lang.String)
275    */
276   @Override
277   public void deleted(String pid) {
278     logger.debug("delete MoodleUserProviderInstance for pid=" + pid);
279     ServiceRegistration registration = providerRegistrations.remove(pid);
280     if (registration != null) {
281       registration.unregister();
282       try {
283         ManagementFactory.getPlatformMBeanServer().unregisterMBean(MoodleUserProviderFactory.getObjectName(pid));
284       } catch (Exception e) {
285         logger.warn("Unable to unregister mbean for pid='{}': {}", pid, e.getMessage());
286       }
287     }
288   }
289 }