/** * Licensed to The Apereo Foundation under one or more contributor license * agreements. See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * * The Apereo Foundation licenses this file to you under the Educational * Community License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License * at: * * http://opensource.org/licenses/ecl2.txt * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * */ package org.opencastproject.userdirectory.ldap; import org.opencastproject.security.api.CachingUserProviderMXBean; import org.opencastproject.security.api.JaxbOrganization; import org.opencastproject.security.api.JaxbRole; import org.opencastproject.security.api.JaxbUser; import org.opencastproject.security.api.Organization; import org.opencastproject.security.api.User; import org.opencastproject.security.api.UserProvider; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.util.concurrent.UncheckedExecutionException; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.ldap.DefaultSpringSecurityContextSource; import org.springframework.security.ldap.search.FilterBasedLdapUserSearch; import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper; import org.springframework.security.ldap.userdetails.LdapUserDetailsService; import java.lang.management.ManagementFactory; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicLong; import javax.management.InstanceNotFoundException; import javax.management.MBeanServer; import javax.management.ObjectName; /** * A UserProvider that reads user roles from LDAP entries. */ public class LdapUserProviderInstance implements UserProvider, CachingUserProviderMXBean { /** The logger */ private static final Logger logger = LoggerFactory.getLogger(LdapUserProviderInstance.class); public static final String PROVIDER_NAME = "ldap"; /** The spring ldap userdetails service delegate */ private LdapUserDetailsService delegate = null; /** The organization id */ private Organization organization = null; /** Total number of requests made to load users */ private AtomicLong requests = null; /** The number of requests made to ldap */ private AtomicLong ldapLoads = null; /** A cache of users, which lightens the load on the LDAP server */ private LoadingCache<String, Object> cache = null; /** A token to store in the miss cache */ protected Object nullToken = new Object(); /** * Constructs an ldap user provider with the needed settings. * * @param pid * the pid of this service * @param organization * the organization * @param searchBase * the ldap search base * @param searchFilter * the ldap search filter * @param url * the url of the ldap server * @param userDn * the user to authenticate as * @param password * the user credentials * @param roleAttributesGlob * the comma separate list of ldap attributes to treat as roles * @param rolePrefix * a prefix to be appended to all the roles read from the LDAP server * @param cacheSize * the number of users to cache * @param cacheExpiration * the number of minutes to cache users */ // CHECKSTYLE:OFF LdapUserProviderInstance(String pid, Organization organization, String searchBase, String searchFilter, String url, String userDn, String password, String roleAttributesGlob, String rolePrefix, int cacheSize, int cacheExpiration) { // CHECKSTYLE:ON this.organization = organization; logger.debug("Creating LdapUserProvider instance with pid=" + pid + ", and organization=" + organization + ", to LDAP server at url: " + url); DefaultSpringSecurityContextSource contextSource = new DefaultSpringSecurityContextSource(url); if (StringUtils.isNotBlank(userDn)) { contextSource.setPassword(password); contextSource.setUserDn(userDn); // Required so that authentication will actually be used contextSource.setAnonymousReadOnly(false); } else { // No password set so try to connect anonymously. contextSource.setAnonymousReadOnly(true); } try { contextSource.afterPropertiesSet(); } catch (Exception e) { throw new org.opencastproject.util.ConfigurationException("Unable to create a spring context source", e); } FilterBasedLdapUserSearch userSearch = new FilterBasedLdapUserSearch(searchBase, searchFilter, contextSource); userSearch.setReturningAttributes(roleAttributesGlob.split(",")); this.delegate = new LdapUserDetailsService(userSearch); if (StringUtils.isNotBlank(roleAttributesGlob)) { LdapUserDetailsMapper mapper = new LdapUserDetailsMapper(); if (rolePrefix != null) { mapper.setRolePrefix(rolePrefix); logger.debug("Role prefix set to: \"{}\"", rolePrefix); } else { logger.debug("Using default role prefix (\"ROLE_\")"); } mapper.setRoleAttributes(roleAttributesGlob.split(",")); this.delegate.setUserDetailsMapper(mapper); } // Setup the caches cache = CacheBuilder.newBuilder().maximumSize(cacheSize).expireAfterWrite(cacheExpiration, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String id) throws Exception { User user = loadUserFromLdap(id); return user == null ? nullToken : user; } }); registerMBean(pid); } @Override public String getName() { return PROVIDER_NAME; } /** * Registers an MXBean. */ protected void registerMBean(String pid) { // register with jmx requests = new AtomicLong(); ldapLoads = new AtomicLong(); try { ObjectName name; name = LdapUserProviderFactory.getObjectName(pid); Object mbean = this; MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); try { mbs.unregisterMBean(name); } catch (InstanceNotFoundException e) { logger.debug(name + " was not registered"); } mbs.registerMBean(mbean, name); } catch (Exception e) { logger.warn("Unable to register {} as an mbean: {}", this, e); } } /** * {@inheritDoc} * * @see org.opencastproject.security.api.UserProvider#getOrganization() */ @Override public String getOrganization() { return organization.getId(); } /** * {@inheritDoc} * * @see org.opencastproject.security.api.UserProvider#loadUser(java.lang.String) */ @Override public User loadUser(String userName) { logger.debug("LdapUserProvider is loading user " + userName); requests.incrementAndGet(); try { // use #getUnchecked since the loader does not throw any checked exceptions Object user = cache.getUnchecked(userName); if (user == nullToken) { return null; } else { return (JaxbUser) user; } } catch (UncheckedExecutionException e) { logger.warn("Exception while loading user " + userName, e); return null; } } /** * Loads a user from LDAP. * * @param userName * the username * @return the user */ protected User loadUserFromLdap(String userName) { if (delegate == null || cache == null) { throw new IllegalStateException("The LDAP user detail service has not yet been configured"); } ldapLoads.incrementAndGet(); UserDetails userDetails = null; Thread currentThread = Thread.currentThread(); ClassLoader originalClassloader = currentThread.getContextClassLoader(); try { currentThread.setContextClassLoader(LdapUserProviderFactory.class.getClassLoader()); try { userDetails = delegate.loadUserByUsername(userName); } catch (UsernameNotFoundException e) { cache.put(userName, nullToken); return null; } JaxbOrganization jaxbOrganization = JaxbOrganization.fromOrganization(organization); Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities(); Set<JaxbRole> roles = new HashSet<JaxbRole>(); if (authorities != null) { for (GrantedAuthority authority : authorities) { roles.add(new JaxbRole(authority.getAuthority(), jaxbOrganization)); } } User user = new JaxbUser(userDetails.getUsername(), PROVIDER_NAME, jaxbOrganization, roles); cache.put(userName, user); return user; } finally { currentThread.setContextClassLoader(originalClassloader); } } /** * {@inheritDoc} * * @see org.opencastproject.security.api.CachingUserProviderMXBean#getCacheHitRatio() */ @Override public float getCacheHitRatio() { if (requests.get() == 0) { return 0; } return (float) (requests.get() - ldapLoads.get()) / requests.get(); } @Override public Iterator<User> findUsers(String query, int offset, int limit) { if (query == null) throw new IllegalArgumentException("Query must be set"); // TODO implement a LDAP wildcard search return Collections.<User> emptyList().iterator(); } @Override public Iterator<User> getUsers() { // TODO implement LDAP get all users return Collections.<User> emptyList().iterator(); } @Override public long countUsers() { // TODO implement LDAP count users return 0; } @Override public void invalidate(String userName) { cache.invalidate(userName); } }