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.CachingUserProviderMXBean;
25  import org.opencastproject.security.api.JaxbOrganization;
26  import org.opencastproject.security.api.JaxbRole;
27  import org.opencastproject.security.api.JaxbUser;
28  import org.opencastproject.security.api.Organization;
29  import org.opencastproject.security.api.SecurityService;
30  import org.opencastproject.security.api.User;
31  import org.opencastproject.security.api.UserProvider;
32  
33  import com.google.common.cache.CacheBuilder;
34  import com.google.common.cache.CacheLoader;
35  import com.google.common.cache.LoadingCache;
36  import com.google.common.util.concurrent.UncheckedExecutionException;
37  
38  import org.apache.commons.lang3.StringUtils;
39  import org.slf4j.Logger;
40  import org.slf4j.LoggerFactory;
41  import org.springframework.security.core.userdetails.UserDetails;
42  import org.springframework.security.core.userdetails.UsernameNotFoundException;
43  import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
44  import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
45  import org.springframework.security.ldap.userdetails.LdapUserDetailsService;
46  
47  import java.lang.management.ManagementFactory;
48  import java.util.ArrayList;
49  import java.util.Collections;
50  import java.util.Iterator;
51  import java.util.List;
52  import java.util.Set;
53  import java.util.concurrent.TimeUnit;
54  import java.util.concurrent.atomic.AtomicLong;
55  import java.util.stream.Collectors;
56  import java.util.stream.Stream;
57  
58  import javax.management.InstanceNotFoundException;
59  import javax.management.MBeanServer;
60  import javax.management.ObjectName;
61  
62  /**
63   * A UserProvider that reads user roles from LDAP entries.
64   */
65  public class LdapUserProviderInstance implements UserProvider, CachingUserProviderMXBean {
66  
67    /** The logger */
68    private static final Logger logger = LoggerFactory.getLogger(LdapUserProviderInstance.class);
69  
70    public static final String PROVIDER_NAME = "ldap";
71  
72    /** The spring ldap userdetails service delegate */
73    private final LdapUserDetailsService delegate;
74  
75    /** The organization id */
76    private Organization organization = null;
77  
78    /** Total number of requests made to load users */
79    private AtomicLong requests = null;
80  
81    /** The number of requests made to ldap */
82    private AtomicLong ldapLoads = null;
83  
84    /** A cache of users, which lightens the load on the LDAP server */
85    private LoadingCache<String, Object> cache = null;
86  
87    /** A token to store in the miss cache */
88    protected Object nullToken = new Object();
89  
90    /** Opencast's security service */
91    private final SecurityService securityService;
92  
93    /**
94     * Constructs an ldap user provider with the needed settings.
95     *
96     * @param pid
97     *          the pid of this service
98     * @param organization
99     *          the organization
100    * @param searchBase
101    *          the ldap search base
102    * @param searchFilter
103    *          the ldap search filter
104    * @param url
105    *          the url of the ldap server
106    * @param userDn
107    *          the user to authenticate as
108    * @param password
109    *          the user credentials
110    * @param roleAttributesGlob
111    *          the comma separate list of ldap attributes to treat as roles or to consider for the ldapAssignmentRoleMap
112    * @param cacheSize
113    *          the number of users to cache
114    * @param cacheExpiration
115    *          the number of minutes to cache users
116    * @param securityService
117    *          a reference to Opencast's security service
118    * @param authoritiesPopulator
119    *          a reference to Opencast's authorities populator
120    * @param userDetailsContextMapper
121    *          a reference to Opencast's user details mapper
122    */
123   // CHECKSTYLE:OFF
124   LdapUserProviderInstance(
125       String pid,
126       Organization organization,
127       String searchBase,
128       String searchFilter,
129       String url,
130       String userDn,
131       String password,
132       String roleAttributesGlob,
133       int cacheSize,
134       int cacheExpiration,
135       SecurityService securityService,
136       OpencastLdapAuthoritiesPopulator authoritiesPopulator,
137       OpencastUserDetailsContextMapper userDetailsContextMapper
138   ) {
139     // CHECKSTYLE:ON
140     this.organization = organization;
141     this.securityService = securityService;
142     logger.debug("Creating LdapUserProvider instance with pid=" + pid + ", and organization=" + organization
143             + ", to LDAP server at url:  " + url);
144 
145     DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(url);
146     if (StringUtils.isNotBlank(userDn)) {
147       contextSource.setPassword(password);
148       contextSource.setUserDn(userDn);
149       // Required so that authentication will actually be used
150       contextSource.setAnonymousReadOnly(false);
151     } else {
152       // No password set so try to connect anonymously.
153       contextSource.setAnonymousReadOnly(true);
154     }
155 
156     try {
157       contextSource.afterPropertiesSet();
158     } catch (Exception e) {
159       throw new org.opencastproject.util.ConfigurationException("Unable to create a spring context source", e);
160     }
161     FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(searchBase, searchFilter, contextSource);
162     userSearch.setReturningAttributes(roleAttributesGlob.split(","));
163 
164     delegate = new LdapUserDetailsService(userSearch, authoritiesPopulator);
165 
166     if (userDetailsContextMapper != null) {
167       userSearch.setReturningAttributes(
168           Stream.of(roleAttributesGlob.split(","), userDetailsContextMapper.getAttributes())
169               .flatMap(Stream::of)
170               .collect(Collectors.toList()).toArray(new String[] { })
171       );
172       delegate.setUserDetailsMapper(userDetailsContextMapper);
173     }
174 
175     // Setup the caches
176     cache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheExpiration, TimeUnit.MINUTES)
177             .build(new CacheLoader<String, Object>() {
178               @Override
179               public Object load(String id) throws Exception {
180                 User user = loadUserFromLdap(id);
181                 return user == null ? nullToken : user;
182               }
183             });
184 
185     registerMBean(pid);
186   }
187 
188   @Override
189   public String getName() {
190     return PROVIDER_NAME;
191   }
192 
193   /**
194    * Registers an MXBean.
195    */
196   protected void registerMBean(String pid) {
197     // register with jmx
198     requests = new AtomicLong();
199     ldapLoads = new AtomicLong();
200     try {
201       ObjectName name;
202       name = LdapUserProviderFactory.getObjectName(pid);
203       Object mbean = this;
204       MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
205       try {
206         mbs.unregisterMBean(name);
207       } catch (InstanceNotFoundException e) {
208         logger.debug(name + " was not registered");
209       }
210       mbs.registerMBean(mbean, name);
211     } catch (Exception e) {
212       logger.warn("Unable to register {} as an mbean", this, e);
213     }
214   }
215 
216   /**
217    * {@inheritDoc}
218    *
219    * @see org.opencastproject.security.api.UserProvider#getOrganization()
220    */
221   @Override
222   public String getOrganization() {
223     return organization.getId();
224   }
225 
226   /**
227    * {@inheritDoc}
228    *
229    * @see org.opencastproject.security.api.UserProvider#loadUser(java.lang.String)
230    */
231   @Override
232   public User loadUser(String userName) {
233     logger.debug("LdapUserProvider is loading user " + userName);
234     requests.incrementAndGet();
235     try {
236       // use #getUnchecked since the loader does not throw any checked exceptions
237       Object user = cache.getUnchecked(userName);
238       if (user == nullToken) {
239         return null;
240       } else {
241         return (JaxbUser) user;
242       }
243     } catch (UncheckedExecutionException e) {
244       logger.warn("Exception while loading user " + userName, e);
245       return null;
246     }
247   }
248 
249   /**
250    * Loads a user from LDAP.
251    *
252    * @param userName
253    *          the username
254    * @return the user
255    */
256   protected User loadUserFromLdap(String userName) {
257     if (delegate == null || cache == null) {
258       throw new IllegalStateException("The LDAP user detail service has not yet been configured");
259     }
260     ldapLoads.incrementAndGet();
261     UserDetails userDetails = null;
262 
263     Thread currentThread = Thread.currentThread();
264     ClassLoader originalClassloader = currentThread.getContextClassLoader();
265     try {
266       currentThread.setContextClassLoader(LdapUserProviderFactory.class.getClassLoader());
267       try {
268         userDetails = delegate.loadUserByUsername(userName);
269       } catch (UsernameNotFoundException e) {
270         cache.put(userName, nullToken);
271         return null;
272       }
273       JaxbOrganization jaxbOrganization = JaxbOrganization.fromOrganization(organization);
274 
275       Set<JaxbRole> roles = userDetails.getAuthorities()
276           .stream()
277           .map(a -> new JaxbRole(a.getAuthority(), jaxbOrganization))
278           .collect(Collectors.toUnmodifiableSet());
279 
280       User user;
281       if (userDetails instanceof OpencastUserDetails) {
282         user = new JaxbUser(userDetails.getUsername(),null,
283             ((OpencastUserDetails) userDetails).getName(),
284             ((OpencastUserDetails) userDetails).getMail(), PROVIDER_NAME, jaxbOrganization, roles);
285       } else {
286         user = new JaxbUser(userDetails.getUsername(), PROVIDER_NAME, jaxbOrganization, roles);
287       }
288 
289       cache.put(userName, user);
290       return user;
291     } finally {
292       currentThread.setContextClassLoader(originalClassloader);
293     }
294   }
295 
296   /**
297    * {@inheritDoc}
298    *
299    * @see org.opencastproject.security.api.CachingUserProviderMXBean#getCacheHitRatio()
300    */
301   @Override
302   public float getCacheHitRatio() {
303     if (requests.get() == 0) {
304       return 0;
305     }
306     return (float) (requests.get() - ldapLoads.get()) / requests.get();
307   }
308 
309   @Override
310   public Iterator<User> findUsers(String query, int offset, int limit) {
311     if (query == null) {
312       throw new IllegalArgumentException("Query must be set");
313     }
314     // TODO implement a LDAP wildcard search
315     // FIXME We return the current user, rather than an empty list, to make sure the current user's role is displayed in
316     // the admin UI (MH-12526).
317     User currentUser = securityService.getUser();
318     if (loadUser(currentUser.getUsername()) != null) {
319       List<User> retVal = new ArrayList<>();
320       retVal.add(securityService.getUser());
321       return retVal.iterator();
322     }
323     return Collections.emptyIterator();
324   }
325 
326   @Override
327   public Iterator<User> getUsers() {
328     // TODO implement LDAP get all users
329     // FIXME We return the current user, rather than an empty list,
330     // to make sure the current user's role is displayed in
331     // the admin UI (MH-12526).
332     User currentUser = securityService.getUser();
333     if (loadUser(currentUser.getUsername()) != null) {
334       List<User> retVal = new ArrayList<>();
335       retVal.add(securityService.getUser());
336       return retVal.iterator();
337     }
338     return Collections.emptyIterator();
339   }
340 
341   @Override
342   public long countUsers() {
343     // TODO implement LDAP count users
344     // FIXME Because of MH-12526, we return conditionally 1 when the previous methods return the current user
345     if (loadUser(securityService.getUser().getUsername()) != null) {
346       return 1;
347     }
348     return 0;
349   }
350 
351   @Override
352   public void invalidate(String userName) {
353     cache.invalidate(userName);
354   }
355 }