/**
* 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.kernel.userdirectory;
import static org.opencastproject.security.api.UserProvider.ALL_ORGANIZATIONS;
import static org.opencastproject.util.data.Tuple.tuple;
import org.opencastproject.security.api.GroupProvider;
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.Role;
import org.opencastproject.security.api.RoleDirectoryService;
import org.opencastproject.security.api.RoleProvider;
import org.opencastproject.security.api.SecurityService;
import org.opencastproject.security.api.User;
import org.opencastproject.security.api.UserDirectoryService;
import org.opencastproject.security.api.UserProvider;
import org.opencastproject.util.data.Collections;
import org.opencastproject.util.data.Function;
import org.opencastproject.util.data.Tuple;
import com.entwinemedia.fn.Stream;
import com.entwinemedia.fn.StreamOp;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.apache.commons.collections.IteratorUtils;
import org.apache.commons.lang3.StringUtils;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
/**
* Federates user and role providers, and exposes a spring UserDetailsService so user lookups can be used by spring
* security.
*/
public class UserAndRoleDirectoryServiceImpl implements UserDirectoryService, UserDetailsService, RoleDirectoryService {
/** The logger */
private static final Logger logger = LoggerFactory.getLogger(UserAndRoleDirectoryServiceImpl.class);
/** A non-obvious password to allow a Spring User to be instantiated for CAS authenticated users having no password */
private static final String DEFAULT_PASSWORD = "4b3e4b30-718c-11e2-bcfd-0800200c9a66";
/** The configuration property for the user cache size */
public static final String USER_CACHE_SIZE_KEY = "org.opencastproject.userdirectory.cache.size";
/** The configuration property for the user cache expiry time */
public static final String USER_CACHE_EXPIRY_KEY = "org.opencastproject.userdirectory.cache.expiry";
/** The list of user providers */
protected List<UserProvider> userProviders = new CopyOnWriteArrayList<UserProvider>();
/** The list of role providers */
protected List<RoleProvider> roleProviders = new CopyOnWriteArrayList<RoleProvider>();
/** The security service */
protected SecurityService securityService = null;
/** A token to store in the miss cache */
private Object nullToken = new Object();
private final CacheLoader<Tuple<String, String>, Object> userLoader = new CacheLoader<Tuple<String, String>, Object>() {
@Override
public Object load(Tuple<String, String> orgUser) {
User user = loadUser.apply(orgUser);
return user == null ? nullToken : user;
}
};
/** The user cache */
private LoadingCache<Tuple<String, String>, Object> cache;
/** Size of the user cache */
private int cacheSize = 200;
/** Expiry time for elements in the user cache */
private int cacheExpiryTimeInMinutes = 1;
/**
* Callback to activate the component.
*
* @param cc
* the declarative services component context
*/
protected void activate(ComponentContext cc) {
if (cc != null) {
String stringValue = cc.getBundleContext().getProperty(USER_CACHE_SIZE_KEY);
try {
cacheSize = Integer.parseInt(StringUtils.trimToNull(stringValue));
} catch (Exception e) {
logger.warn("Ignoring invalid value {} for user cache size", stringValue);
}
stringValue = cc.getBundleContext().getProperty(USER_CACHE_EXPIRY_KEY);
try {
cacheExpiryTimeInMinutes = Integer.parseInt(StringUtils.trimToNull(stringValue));
} catch (Exception e) {
logger.warn("Ignoring invalid value {} for user cache expiry time", stringValue);
}
}
// Create the user cache
cache = CacheBuilder.newBuilder().expireAfterWrite(cacheExpiryTimeInMinutes, TimeUnit.MINUTES).maximumSize(cacheSize).build(userLoader);
logger.info("Activated UserAndRoleDirectoryService with user cache of size {}, expiry time {} minutes",
cacheSize, cacheExpiryTimeInMinutes);
}
/**
* Adds a user provider.
*
* @param userProvider
* the user provider to add
*/
protected synchronized void addUserProvider(UserProvider userProvider) {
logger.debug("Adding {} to the list of user providers", userProvider);
if (InMemoryUserAndRoleProvider.PROVIDER_NAME.equals(userProvider.getName())) {
userProviders.add(0, userProvider);
} else {
userProviders.add(userProvider);
}
}
/**
* Remove a user provider.
*
* @param userProvider
* the user provider to remove
*/
protected synchronized void removeUserProvider(UserProvider userProvider) {
logger.debug("Removing {} from the list of user providers", userProvider);
roleProviders.remove(userProvider);
}
/**
* Adds a role provider.
*
* @param roleProvider
* the role provider to add
*/
protected synchronized void addRoleProvider(RoleProvider roleProvider) {
logger.debug("Adding {} to the list of role providers", roleProvider);
roleProviders.add(roleProvider);
}
/**
* Remove a role provider.
*
* @param roleProvider
* the role provider to remove
*/
protected synchronized void removeRoleProvider(RoleProvider roleProvider) {
logger.debug("Removing {} from the list of role providers", roleProvider);
roleProviders.remove(roleProvider);
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.security.api.UserDirectoryService#getUsers()
*/
@Override
@SuppressWarnings("unchecked")
public Iterator<User> getUsers() {
Organization org = securityService.getOrganization();
if (org == null)
throw new IllegalStateException("No organization is set");
// Find all users from the user providers
Stream<User> users = Stream.empty();
for (final UserProvider userProvider : userProviders) {
String providerOrgId = userProvider.getOrganization();
if (!ALL_ORGANIZATIONS.equals(providerOrgId) && !org.getId().equals(providerOrgId))
continue;
users = users.append(IteratorUtils.toList(userProvider.getUsers())).sort(userComparator);
}
return users.iterator();
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.security.api.RoleDirectoryService#getRoles()
*/
@Override
@SuppressWarnings("unchecked")
public Iterator<Role> getRoles() {
Organization org = securityService.getOrganization();
if (org == null)
throw new IllegalStateException("No organization is set");
Stream<Role> roles = Stream.empty();
for (RoleProvider roleProvider : roleProviders) {
String providerOrgId = roleProvider.getOrganization();
if (!ALL_ORGANIZATIONS.equals(providerOrgId) && !org.getId().equals(providerOrgId))
continue;
roles = roles.append(IteratorUtils.toList(roleProvider.getRoles())).sort(roleComparator);
}
return roles.iterator();
}
/**
* {@inheritDoc}
*
* @see org.opencastproject.security.api.UserDirectoryService#loadUser(java.lang.String)
*/
@Override
public User loadUser(String userName) throws IllegalStateException {
Organization org = securityService.getOrganization();
if (org == null) {
throw new IllegalStateException("No organization is set");
}
Object user = cache.getUnchecked(tuple(org.getId(), userName));
if (user == nullToken) {
cache.invalidate(tuple(org.getId(), userName));
return null;
} else {
return (User) user;
}
}
/** Load a user of an organization. */
private final Function<Tuple<String, String>, User> loadUser = new Function<Tuple<String, String>, User>() {
@Override
public User apply(Tuple<String, String> orgUser) {
// Collect all of the roles known from each of the user providers for this user
User user = null;
for (UserProvider userProvider : userProviders) {
String providerOrgId = userProvider.getOrganization();
if (!ALL_ORGANIZATIONS.equals(providerOrgId) && !orgUser.getA().equals(providerOrgId)) {
continue;
}
User providerUser = userProvider.loadUser(orgUser.getB());
if (providerUser == null) {
continue;
}
User tmpUser = JaxbUser.fromUser(providerUser);
if (user == null) {
user = tmpUser;
} else {
user = mergeUsers(user, tmpUser);
}
// Return super users without merging to avoid unnecessary requests to other user providers
if (InMemoryUserAndRoleProvider.PROVIDER_NAME.equals(userProvider.getName())) {
user = tmpUser;
break;
}
}
if (user == null)
return null;
// Add additional roles from role providers
Set<JaxbRole> roles = new HashSet<JaxbRole>();
for (Role role : user.getRoles()) {
roles.add(JaxbRole.fromRole(role));
}
// Consult roleProviders if this is not an internal system user
if (!InMemoryUserAndRoleProvider.PROVIDER_NAME.equals(user.getProvider())) {
for (RoleProvider roleProvider : roleProviders) {
for (Role role : roleProvider.getRolesForUser(user.getUsername())) {
roles.add(JaxbRole.fromRole(role));
}
}
}
// Resolve any transitive roles granted via group membership
Set<JaxbRole> derivedRoles = new HashSet<JaxbRole>();
for (Role role : roles) {
if (Role.Type.EXTERNAL_GROUP.equals(role.getType())) {
// Load roles granted to this group
logger.debug("Resolving transitive roles for user {} from external group {}", user.getUsername(), role.getName());
for (RoleProvider roleProvider : roleProviders) {
if (roleProvider instanceof GroupProvider) {
List<Role> groupRoles = ((GroupProvider) roleProvider).getRolesForGroup(role.getName());
if (groupRoles != null) {
for (Role groupRole : groupRoles) {
derivedRoles.add(JaxbRole.fromRole(groupRole));
}
logger.debug("Adding {} derived role(s) for user {} from internal group {}", derivedRoles.size(), user.getUsername(), role.getName());
} else {
logger.warn("Cannot resolve externallly provided group reference for user {} to internal group {}", user.getUsername(), role.getName());
}
}
}
}
}
roles.addAll(derivedRoles);
// Create and return the final user
JaxbUser mergedUser = new JaxbUser(user.getUsername(), user.getPassword(), user.getName(), user.getEmail(),
user.getProvider(), user.canLogin(), JaxbOrganization.fromOrganization(user.getOrganization()), roles);
mergedUser.setManageable(user.isManageable());
return mergedUser;
}
};
/**
* {@inheritDoc}
*
* @see org.springframework.security.core.userdetails.UserDetailsService#loadUserByUsername(java.lang.String)
*/
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException,
org.springframework.dao.DataAccessException {
User user = loadUser(userName);
if (user == null)
throw new UsernameNotFoundException(userName);
// Store the user in the security service
securityService.setUser(user);
Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
for (Role role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
// Add additional roles from role providers
if (!InMemoryUserAndRoleProvider.PROVIDER_NAME.equals(user.getProvider())) {
for (RoleProvider roleProvider : roleProviders) {
List<Role> rolesForUser = roleProvider.getRolesForUser(userName);
for (Role role : rolesForUser)
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
}
authorities.add(new SimpleGrantedAuthority(securityService.getOrganization().getAnonymousRole()));
// need a non null password to instantiate org.springframework.security.core.userdetails.User
// but CAS authenticated users have no password
String password = user.getPassword() == null ? DEFAULT_PASSWORD : user.getPassword();
return new org.springframework.security.core.userdetails.User(user.getUsername(), password, user.canLogin(), true,
true, true, authorities);
}
/**
* Merges two representations of a user, as returned by two different user providers. The set or roles from the
* provided users will be merged into one set.
*
* @param user1
* the first user to merge
* @param user2
* the second user to merge
* @return a user with a merged set of roles
*/
protected User mergeUsers(User user1, User user2) {
HashSet<JaxbRole> mergedRoles = new HashSet<JaxbRole>();
for (Role role : user1.getRoles()) {
mergedRoles.add(JaxbRole.fromRole(role));
}
for (Role role : user2.getRoles()) {
mergedRoles.add(JaxbRole.fromRole(role));
}
String name = StringUtils.isNotBlank(user1.getName()) ? user1.getName() : user2.getName();
String email = StringUtils.isNotBlank(user1.getEmail()) ? user1.getEmail() : user2.getEmail();
String password = user1.getPassword() == null ? user2.getPassword() : user1.getPassword();
boolean manageable = user1.isManageable() || user2.isManageable() ? true : false;
JaxbOrganization organization = JaxbOrganization.fromOrganization(user1.getOrganization());
String provider = StringUtils.join(Collections.nonNullList(user1.getProvider(), user2.getProvider()), ",");
JaxbUser jaxbUser = new JaxbUser(user1.getUsername(), password, name, email, provider, organization, mergedRoles);
jaxbUser.setManageable(manageable);
return jaxbUser;
}
/**
* Sets the security service
*
* @param securityService
* the securityService to set
*/
public void setSecurityService(SecurityService securityService) {
this.securityService = securityService;
}
@Override
@SuppressWarnings("unchecked")
public Iterator<User> findUsers(String query, int offset, int limit) {
if (query == null)
throw new IllegalArgumentException("Query must be set");
Organization org = securityService.getOrganization();
if (org == null)
throw new IllegalStateException("No organization is set");
// Find all users from the user providers
Stream<User> users = Stream.empty();
for (final UserProvider userProvider : userProviders) {
String providerOrgId = userProvider.getOrganization();
if (!ALL_ORGANIZATIONS.equals(providerOrgId) && !org.getId().equals(providerOrgId))
continue;
users = users.append(IteratorUtils.toList(userProvider.findUsers(query, 0, 0))).sort(userComparator);
}
return users.drop(offset).apply(limit > 0 ? StreamOp.<User> id().take(limit) : StreamOp.<User> id()).iterator();
}
@Override
@SuppressWarnings("unchecked")
public Iterator<Role> findRoles(String query, Role.Target target, int offset, int limit) {
if (query == null)
throw new IllegalArgumentException("Query must be set");
Organization org = securityService.getOrganization();
if (org == null)
throw new IllegalStateException("No organization is set");
// Find all roles from the role providers
Stream<Role> roles = Stream.empty();
for (RoleProvider roleProvider : roleProviders) {
String providerOrgId = roleProvider.getOrganization();
if (!ALL_ORGANIZATIONS.equals(providerOrgId) && !org.getId().equals(providerOrgId))
continue;
roles = roles.append(IteratorUtils.toList(roleProvider.findRoles(query, target, 0, 0))).sort(roleComparator);
}
return roles.drop(offset).apply(limit > 0 ? StreamOp.<Role> id().take(limit) : StreamOp.<Role> id()).iterator();
}
@Override
public long countUsers() {
long sum = 0;
for (UserProvider userProvider : userProviders) {
sum += userProvider.countUsers();
}
return sum;
}
@Override
public void invalidate(String userName) {
for (UserProvider userProvider : userProviders) {
userProvider.invalidate(userName);
}
Organization org = securityService.getOrganization();
if (org == null)
throw new IllegalStateException("No organization is set");
cache.invalidate(tuple(org.getId(), userName));
logger.trace("Invalidated user {} from user directories", userName);
}
private static final Comparator<Role> roleComparator = new Comparator<Role>() {
@Override
public int compare(Role role1, Role role2) {
return role1.getName().compareTo(role2.getName());
}
};
private static final Comparator<User> userComparator = new Comparator<User>() {
@Override
public int compare(User user1, User user2) {
return user1.getUsername().compareTo(user2.getUsername());
}
};
}