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.ldap;
23  
24  import org.opencastproject.security.api.Organization;
25  import org.opencastproject.security.api.OrganizationDirectoryService;
26  import org.opencastproject.security.api.SecurityService;
27  import org.opencastproject.security.api.UserProvider;
28  import org.opencastproject.userdirectory.JpaGroupRoleProvider;
29  import org.opencastproject.util.NotFoundException;
30  
31  import org.apache.commons.lang3.ArrayUtils;
32  import org.apache.commons.lang3.BooleanUtils;
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  import org.springframework.security.ldap.userdetails.LdapAuthoritiesPopulator;
46  
47  import java.lang.management.ManagementFactory;
48  import java.util.Arrays;
49  import java.util.Dictionary;
50  import java.util.Enumeration;
51  import java.util.HashMap;
52  import java.util.HashSet;
53  import java.util.Hashtable;
54  import java.util.Map;
55  import java.util.Objects;
56  import java.util.Set;
57  import java.util.concurrent.ConcurrentHashMap;
58  
59  import javax.management.MalformedObjectNameException;
60  import javax.management.ObjectName;
61  
62  /**
63   * LDAP implementation of the spring UserDetailsService, taking configuration information from the component context.
64   */
65  @Component(
66      immediate = true,
67      service = ManagedServiceFactory.class,
68      property = {
69          "service.pid=org.opencastproject.userdirectory.ldap",
70          "service.description=Provides ldap user directory instances"
71      }
72  )
73  public class LdapUserProviderFactory implements ManagedServiceFactory {
74  
75    /** The logger */
76    private static final Logger logger = LoggerFactory.getLogger(LdapUserProviderFactory.class);
77  
78    /** This service factory's PID */
79    private static final String PID = "org.opencastproject.userdirectory.ldap";
80  
81    /** The key to look up the ldap search filter in the service configuration properties */
82    private static final String SEARCH_FILTER_KEY = "org.opencastproject.userdirectory.ldap.searchfilter";
83  
84    /** The key to look up the ldap search base in the service configuration properties */
85    private static final String SEARCH_BASE_KEY = "org.opencastproject.userdirectory.ldap.searchbase";
86  
87    /** The key to look up the ldap server URL in the service configuration properties */
88    private static final String LDAP_URL_KEY = "org.opencastproject.userdirectory.ldap.url";
89  
90    /** The key to look up the role attributes in the service configuration properties */
91    private static final String ROLE_ATTRIBUTES_KEY = "org.opencastproject.userdirectory.ldap.roleattributes";
92  
93    /** The key to look up the users name attributes **/
94    private static final String USER_NAME_ATTRIBUTES_KEY = "org.opencastproject.userdirectory.ldap.userattributes.name";
95  
96    /** The key to look up the users attribute to set its mail address**/
97    private static final String USER_MAIL_ATTRIBUTE_KEY = "org.opencastproject.userdirectory.ldap.userattributes.mail";
98  
99    /** The key to look up the organization identifier in the service configuration properties */
100   private static final String ORGANIZATION_KEY = "org.opencastproject.userdirectory.ldap.org";
101 
102   /** The key to look up the user DN to use for performing searches. */
103   private static final String SEARCH_USER_DN = "org.opencastproject.userdirectory.ldap.userDn";
104 
105   /** The key to look up the password to use for performing searches */
106   private static final String SEARCH_PASSWORD = "org.opencastproject.userdirectory.ldap.password";
107 
108   /** The key to look up the number of user records to cache */
109   private static final String CACHE_SIZE = "org.opencastproject.userdirectory.ldap.cache.size";
110 
111   /** The key to look up the number of minutes to cache users */
112   private static final String CACHE_EXPIRATION = "org.opencastproject.userdirectory.ldap.cache.expiration";
113 
114   /** The key to indicate a prefix that will be added to every role read from the LDAP */
115   private static final String ROLE_PREFIX_KEY = "org.opencastproject.userdirectory.ldap.roleprefix";
116 
117   /**
118    * The key to indicate a comma-separated list of prefixes.
119    * The "role prefix" defined with the ROLE_PREFIX_KEY will not be prepended to the roles starting with any of these
120    */
121   private static final String EXCLUDE_PREFIXES_KEY = "org.opencastproject.userdirectory.ldap.exclude.prefixes";
122 
123   /**
124    * The key to indicate a prefix,
125    * which is used to check whether a roleattribute value shall be added as a group to the user
126    */
127   private static final String GROUP_CHECK_PREFIX_KEY = "org.opencastproject.userdirectory.ldap.groupcheckprefix";
128 
129   /** Specifies, whether the roleattributes should be added as a role */
130   private static final String APPLY_ROLEATTRIBUTES_AS_ROLES_KEY
131       = "org.opencastproject.userdirectory.ldap.roleattributes.applyasroles";
132 
133   /** Specifies, whether the roleattributes should be added as a group */
134   private static final String APPLY_ROLEATTRIBUTES_AS_GROUPS_KEY
135       = "org.opencastproject.userdirectory.ldap.roleattributes.applyasgroups";
136 
137   /** The prefix of the keys, which map a ldap attribute to opencast roles */
138   private static final String ATTRIBUTE_MAPPING_KEY_PREFIX = "org.opencastproject.userdirectory.ldap.map.";
139 
140   /** The postfix of the attribute maps, which specifiy the value to map */
141   private static final String ATTRIBUTE_MAPPING_KEY_POSTFIX_VALUE = "value";
142 
143   /** The postfix of the attribute maps, which map a ldap attribute to opencast roles */
144   private static final String ATTRIBUTE_MAPPING_KEY_POSTFIX_ROLES = "roles";
145 
146   /** The postfix of the attribute maps, which map a ldap attribute to opencast groups */
147   private static final String ATTRIBUTE_MAPPING_KEY_POSTFIX_GROUPS = "groups";
148 
149   /** The key to indicate whether or not the roles should be converted to uppercase */
150   private static final String UPPERCASE_KEY = "org.opencastproject.userdirectory.ldap.uppercase";
151 
152   /** The key to indicate a unique identifier for each LDAP connection */
153   private static final String INSTANCE_ID_KEY = "org.opencastproject.userdirectory.ldap.id";
154 
155   /** The key to indicate a comma-separated list of extra roles to add to the authenticated user */
156   private static final String EXTRA_ROLES_KEY = "org.opencastproject.userdirectory.ldap.extra.roles";
157 
158   /** The key to setup an LDAP connection ID as an OSGI service property */
159   private static final String INSTANCE_ID_SERVICE_PROPERTY_KEY = "instanceId";
160 
161   /** A map of pid to ldap user provider instance */
162   private Map<String, ServiceRegistration> providerRegistrations = new ConcurrentHashMap<>();
163 
164   /** A map of pid to ldap authorities populator instance */
165   private Map<String, ServiceRegistration> authoritiesPopulatorRegistrations = new ConcurrentHashMap<>();
166 
167   /** The OSGI bundle context */
168   private BundleContext bundleContext = null;
169 
170   /** The organization directory service */
171   private OrganizationDirectoryService orgDirectory;
172 
173   /** The group role provider service */
174   private JpaGroupRoleProvider groupRoleProvider;
175 
176   /** A reference to Opencast's security service */
177   private SecurityService securityService;
178 
179   /** OSGi callback for setting the organization directory service. */
180   @Reference
181   public void setOrgDirectory(OrganizationDirectoryService orgDirectory) {
182     this.orgDirectory = orgDirectory;
183   }
184 
185   /** OSGi callback for setting the role group service. */
186   @Reference
187   public void setGroupRoleProvider(JpaGroupRoleProvider groupRoleProvider) {
188     this.groupRoleProvider = groupRoleProvider;
189   }
190 
191   /** OSGi callback for setting the security service. */
192   @Reference
193   public void setSecurityService(SecurityService securityService) {
194     this.securityService = securityService;
195   }
196 
197   /**
198    * Callback for activation of this component.
199    *
200    * @param cc
201    *          the component context
202    */
203   @Activate
204   public void activate(ComponentContext cc) {
205     logger.debug("Activate LdapUserProviderFactory");
206     bundleContext = cc.getBundleContext();
207   }
208 
209   /**
210    * {@inheritDoc}
211    *
212    * @see org.osgi.service.cm.ManagedServiceFactory#getName()
213    */
214   @Override
215   public String getName() {
216     return PID;
217   }
218 
219   /**
220    * Retrieve configuration values and check for a proper value.
221    *
222    * @param properties
223    *      Configuration dictionary
224    * @param key
225    *      Configuration key to check for
226    * @return
227    *      The configuration value
228    * @throws ConfigurationException
229    *      Thrown if the configuration value is blank
230    */
231   private String getRequiredProperty(final Dictionary properties, final String key) throws ConfigurationException {
232     final String value = (String) properties.get(key);
233     if (StringUtils.isBlank(value)) {
234       throw new ConfigurationException(key, "missing configuration value");
235     }
236     return value;
237   }
238 
239   /**
240    * {@inheritDoc}
241    *
242    * @see org.osgi.service.cm.ManagedServiceFactory#updated(java.lang.String, java.util.Dictionary)
243    */
244   @Override
245   public void updated(String pid, Dictionary properties) throws ConfigurationException {
246     logger.debug("Updating LdapUserProviderFactory");
247 
248     // required settings
249     String searchBase = getRequiredProperty(properties, SEARCH_BASE_KEY);
250     String searchFilter = getRequiredProperty(properties, SEARCH_FILTER_KEY);
251     String url = getRequiredProperty(properties, LDAP_URL_KEY);
252     String instanceId = getRequiredProperty(properties, INSTANCE_ID_KEY);
253     String roleAttributes = getRequiredProperty(properties, ROLE_ATTRIBUTES_KEY);
254 
255     // optional settings
256     String organization = (String) properties.get(ORGANIZATION_KEY);
257     String userDn = (String) properties.get(SEARCH_USER_DN);
258     String password = (String) properties.get(SEARCH_PASSWORD);
259     String[] userAttributeName = StringUtils.split((String) properties.get(USER_NAME_ATTRIBUTES_KEY),',');
260     String userAttributeMail = (String) properties.get(USER_MAIL_ATTRIBUTE_KEY);
261 
262     // optional with default values
263     String rolePrefix = Objects.toString(properties.get(ROLE_PREFIX_KEY), "ROLE_");
264     String[] excludePrefixes = StringUtils.split((String) properties.get(EXCLUDE_PREFIXES_KEY), ",");
265     String groupCheckPrefix = Objects.toString(properties.get(GROUP_CHECK_PREFIX_KEY), "ROLE_GROUP_");
266     boolean convertToUppercase = BooleanUtils.toBoolean(Objects.toString(properties.get(UPPERCASE_KEY), "true"));
267     int cacheSize = NumberUtils.toInt((String) properties.get(CACHE_SIZE), 1000);
268     int cacheExpiration = NumberUtils.toInt((String) properties.get(CACHE_EXPIRATION), 5);
269     boolean applyRoleattributesAsRoles = BooleanUtils.toBoolean(Objects.toString(
270             properties.get(APPLY_ROLEATTRIBUTES_AS_ROLES_KEY), "true"));
271     boolean applyRoleattributesAsGroups = BooleanUtils.toBoolean(Objects.toString(
272             properties.get(APPLY_ROLEATTRIBUTES_AS_GROUPS_KEY), "true"));
273     if (applyRoleattributesAsGroups && !applyRoleattributesAsRoles) {
274       throw new ConfigurationException(APPLY_ROLEATTRIBUTES_AS_GROUPS_KEY,
275               "'" + APPLY_ROLEATTRIBUTES_AS_ROLES_KEY + "' needs to be 'true' to enable this option");
276     }
277 
278     // extra roles
279     String[] extraRoles =  StringUtils.split(Objects.toString(properties.get(EXTRA_ROLES_KEY), ""), ",");
280     Set<String> extraRoleSet = new HashSet<>(Arrays.asList(extraRoles));
281     extraRoleSet.addAll(Arrays.asList("ROLE_ANONYMOUS", "ROLE_USER"));
282     extraRoles = extraRoleSet.toArray(new String[extraRoles.length]);
283 
284     // maps
285     HashMap<String, HashMap<String, String>> ldapAssignmentMappingsPreparation = new HashMap();
286     for (Enumeration<String> e = properties.keys(); e.hasMoreElements();) {
287       String key = e.nextElement();
288 
289       if (key.startsWith(ATTRIBUTE_MAPPING_KEY_PREFIX)) {
290         final String[] postfix = key.substring(ATTRIBUTE_MAPPING_KEY_PREFIX.length()).split("\\.");
291 
292         if (postfix.length != 2) {
293           throw new ConfigurationException(key,
294                   "Invalid Configkey format, the following format is needed: "
295                   + ATTRIBUTE_MAPPING_KEY_PREFIX + "<identifier>.<key>");
296         }
297 
298         final String mappingIdentifier = postfix[0];
299         final String mappingKey = postfix[1];
300 
301         HashMap keyValueMap = ldapAssignmentMappingsPreparation.getOrDefault(mappingIdentifier, new HashMap());
302 
303         keyValueMap.put(mappingKey, (String) properties.get(key));
304 
305         ldapAssignmentMappingsPreparation.put(mappingIdentifier, keyValueMap);
306       }
307     }
308     HashMap<String, String[]> ldapAssignmentRoleMap = new HashMap();
309     HashMap<String, String[]> ldapAssignmentGroupMap = new HashMap();
310     for (HashMap.Entry<String, HashMap<String, String>> entry : ldapAssignmentMappingsPreparation.entrySet()) {
311       HashMap<String, String> mappingConf = entry.getValue();
312       String value = StringUtils.trimToNull(mappingConf.get(ATTRIBUTE_MAPPING_KEY_POSTFIX_VALUE));
313       String roles = StringUtils.trimToNull(mappingConf.get(ATTRIBUTE_MAPPING_KEY_POSTFIX_ROLES));
314       String groups = StringUtils.trimToNull(mappingConf.get(ATTRIBUTE_MAPPING_KEY_POSTFIX_GROUPS));
315 
316       if (value == null) {
317         throw new ConfigurationException(ATTRIBUTE_MAPPING_KEY_PREFIX + entry.getKey() + ".*",
318                 "LDAP mapping incomplete, the key 'value' is needed");
319       }
320       if (roles == null && groups == null) {
321         throw new ConfigurationException(ATTRIBUTE_MAPPING_KEY_PREFIX + entry.getKey() + ".*",
322                 "LDAP mapping incomplete, one of the keys 'roles' or 'groups' is needed");
323       }
324 
325       if (convertToUppercase) {
326         value = value.toUpperCase();
327       }
328 
329       if (roles != null) {
330         if (convertToUppercase) {
331           roles = roles.toUpperCase();
332         }
333         ldapAssignmentRoleMap.put(value,
334                 ArrayUtils.addAll(
335                         ldapAssignmentRoleMap.getOrDefault(value, new String[0]),
336                         Arrays.stream(roles.split(","))
337                                 .map(r -> StringUtils.trimToNull(r))
338                                 .filter(r -> r != null)
339                                 .toArray(String[]::new)
340                 )
341         );
342       }
343 
344       if (groups != null) {
345         if (convertToUppercase) {
346           groups = groups.toUpperCase();
347         }
348         ldapAssignmentGroupMap.put(value,
349                 ArrayUtils.addAll(
350                         ldapAssignmentGroupMap.getOrDefault(value, new String[0]),
351                         Arrays.stream(groups.split(","))
352                                 .map(r -> StringUtils.trimToNull(r))
353                                 .filter(r -> r != null)
354                                 .toArray(String[]::new)
355                 )
356         );
357       }
358     }
359 
360     // Now that we have everything we need, go ahead and activate a new provider, removing an old one if necessary
361     ServiceRegistration existingRegistration = providerRegistrations.remove(pid);
362     if (existingRegistration != null) {
363       existingRegistration.unregister();
364     }
365 
366     // Defaults to first available organization
367     Organization org;
368     try {
369       if (StringUtils.isNoneBlank(organization)) {
370         org = orgDirectory.getOrganization(organization);
371       } else {
372         if (orgDirectory.getOrganizations().size() != 1) {
373           throw new NotFoundException("Multiple organizations exist but none is specified");
374         }
375         org = orgDirectory.getOrganizations().get(0);
376       }
377     } catch (NotFoundException e) {
378       throw new ConfigurationException(ORGANIZATION_KEY, "no organization with configured id", e);
379     }
380 
381     // Dictionary to include a property to identify this LDAP instance in the security.xml file
382     Hashtable<String, String> dict = new Hashtable<>();
383     dict.put(INSTANCE_ID_SERVICE_PROPERTY_KEY, instanceId);
384 
385     // Instantiate this LDAP instance and register it as such
386 
387     OpencastLdapAuthoritiesPopulator authoritiesPopulator = new OpencastLdapAuthoritiesPopulator(roleAttributes,
388             rolePrefix, excludePrefixes, groupCheckPrefix, applyRoleattributesAsRoles, applyRoleattributesAsGroups,
389             ldapAssignmentRoleMap, ldapAssignmentGroupMap, convertToUppercase, org, securityService,
390             groupRoleProvider, extraRoles);
391 
392     // Also, register this instance as LdapAuthoritiesPopulator so that it can be used within the security.xml file
393     authoritiesPopulatorRegistrations.put(pid,
394             bundleContext.registerService(LdapAuthoritiesPopulator.class.getName(), authoritiesPopulator, dict));
395 
396     OpencastUserDetailsContextMapper mapper = null;
397     if (userAttributeName != null && userAttributeMail != null) {
398       mapper = new OpencastUserDetailsContextMapper(userAttributeName, userAttributeMail);
399     }
400 
401     LdapUserProviderInstance provider = new LdapUserProviderInstance(pid, org, searchBase, searchFilter, url, userDn,
402         password, roleAttributes, cacheSize,
403         cacheExpiration, securityService, authoritiesPopulator, mapper);
404 
405     providerRegistrations.put(pid, bundleContext.registerService(UserProvider.class.getName(), provider, null));
406 
407   }
408 
409   /**
410    * {@inheritDoc}
411    *
412    * @see org.osgi.service.cm.ManagedServiceFactory#deleted(java.lang.String)
413    */
414   @Override
415   public void deleted(String pid) {
416     ServiceRegistration providerRegistration = null;
417     ServiceRegistration authoritiesPopulatorRegistration = null;
418 
419     try {
420       providerRegistration = providerRegistrations.remove(pid);
421       authoritiesPopulatorRegistration = authoritiesPopulatorRegistrations.remove(pid);
422       if ((providerRegistration != null) || (authoritiesPopulatorRegistration != null)) {
423         try {
424           ManagementFactory.getPlatformMBeanServer().unregisterMBean(LdapUserProviderFactory.getObjectName(pid));
425         } catch (Exception e) {
426           logger.warn("Unable to unregister mbean for pid='{}': {}", pid, e.getMessage());
427         }
428       }
429     } finally {
430       if (providerRegistration != null) {
431         providerRegistration.unregister();
432       }
433       if (authoritiesPopulatorRegistration != null) {
434         authoritiesPopulatorRegistration.unregister();
435       }
436     }
437   }
438 
439   /**
440    * Builds a JMX object name for a given PID
441    *
442    * @param pid
443    *          the PID
444    * @return the object name
445    * @throws NullPointerException
446    * @throws MalformedObjectNameException
447    */
448   public static final ObjectName getObjectName(String pid) throws MalformedObjectNameException, NullPointerException {
449     return new ObjectName(pid + ":type=LDAPRequests");
450   }
451 
452 }