/*
* Copyright (C) 2010 - 2012 Interactive Media Management
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package dk.i2m.converge.ejb.services;
import dk.i2m.converge.core.DataNotFoundException;
import dk.i2m.converge.core.ConfigurationKey;
import dk.i2m.converge.core.security.*;
import dk.i2m.converge.core.utils.BeanComparator;
import dk.i2m.converge.core.workflow.Department;
import dk.i2m.converge.core.workflow.Outlet;
import dk.i2m.jndi.ldap.LdapUtils;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.PostConstruct;
import javax.ejb.EJB;
import javax.ejb.Stateless;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import org.apache.commons.lang.StringUtils;
/**
* Stateless session bean providing a service for managing users.
*
* @author Allan Lykke Christensen
*/
@Stateless(name = "UserServiceBean", mappedName = "ejb/UserServiceBean")
public class UserServiceBean implements UserServiceLocal {
private static final Logger LOG = Logger.getLogger(UserServiceBean.class.getName());
private static final String EXCEPTION_MSG_COULD_NOT_CONNECT_TO_DIRECTORY = "Could not connect to directory";
@EJB private DaoServiceLocal daoService;
@EJB private ConfigurationServiceLocal cfgService;
/** Identifier (user id) of an anonymous user. */
private final static String ANONYMOUS_USER = "ANONYMOUS";
private final SearchControls sc = new SearchControls();
private String groupOfUsers = "";
private String groupOfAdministrators = "";
private final Map<String, String> fieldMapping = new HashMap<String, String>();
@PostConstruct
public void startup() {
setupUserMapping();
}
/** {@inheritDoc} */
@Override
public List<UserAccount> getMembers(Long departmentId) {
List<UserAccount> members = new ArrayList<UserAccount>();
try {
Department department = daoService.findById(Department.class, departmentId);
members = department.getUserAccounts();
} catch (DataNotFoundException ex) {
LOG.log(Level.FINE, "Invalid Department. {0}" , ex.getMessage());
}
return members;
}
/** {@inheritDoc} */
@Override
public List<UserAccount> getMembers(Long outletId, SystemPrivilege privilege) {
List<UserAccount> members = new ArrayList<UserAccount>();
try {
Outlet outlet = daoService.findById(Outlet.class, outletId);
for (UserRole userRole : outlet.getRoles()) {
for (Privilege p : userRole.getPrivileges()) {
if (p.getId().equals(privilege)) {
members.addAll(userRole.getUserAccounts());
}
}
}
} catch (DataNotFoundException ex) {
LOG.log(Level.FINE, "Invalid Outlet", ex);
}
// Remove duplicates
Set set = new HashSet(members);
members = new ArrayList(set);
return members;
}
/** {@inheritDoc } */
@Override
public List<UserAccount> getRoleMembers(Long roleId) {
try {
UserRole userRole = daoService.findById(UserRole.class, roleId);
return userRole.getUserAccounts();
} catch (DataNotFoundException ex) {
LOG.log(Level.FINE, "", ex);
return Collections.EMPTY_LIST;
}
}
@Override
public List<UserAccount> getDirectoryMembers() throws NamingException {
return getMembers(
cfgService.getString(ConfigurationKey.LDAP_GROUP_USERS));
}
/** {@inheritDoc } */
@Override
public List<UserAccount> getMembers(String groupDn) throws NamingException {
// Field containing the user unique identifier
String uid = getFieldMapping(LdapFieldMapping.USER_MAPPING_USERNAME);
// Field containinf the "member of" a group
String memberOf = getFieldMapping(
LdapFieldMapping.GROUP_MAPPING_MEMBEROF);
List<UserAccount> members = new LinkedList<UserAccount>();
// Set up attributes to search for
String[] searchAttributes = new String[1];
searchAttributes[0] = memberOf;
DirContext dirCtx = getDirectoryConnection();
// Get all the memberOf attributes of the "group of users"
Attributes groupAttrs = dirCtx.getAttributes(groupDn, searchAttributes);
if (groupAttrs != null) {
Attribute memberAttrs = groupAttrs.get(memberOf);
if (memberAttrs != null) {
NamingEnumeration vals = memberAttrs.getAll();
while (vals.hasMoreElements()) {
UserAccount ua;
// Get the DN of the next group member
String userDn = (String) vals.nextElement();
// Get all the attributes of the given user
try {
Attributes attrs = dirCtx.getAttributes(userDn);
// Get the UID of the user
String username = (String) attrs.get(uid).get();
// Look up user by UID in local database
Map<String, Object> params = QueryBuilder.with(
"username", username).parameters();
List<UserAccount> result =
daoService.findWithNamedQuery(
UserAccount.FIND_BY_UID, params);
if (result.isEmpty()) {
LOG.log(Level.FINE,
"User {0} has not been setup in the local database, using information from directory",
username);
ua = new UserAccount();
} else {
ua = result.iterator().next();
}
// Add LDAP directory attributes to user
updateUser(ua, attrs);
ua.setDistinguishedName(userDn);
// Add to results list
members.add(ua);
} catch (NamingException ex) {
LOG.log(Level.WARNING, "User {0} does not exist in LDAP",
userDn);
}
}
}
} else {
LOG.log(Level.SEVERE, "Couldn't find search attributes ({0}) in {1}",
new Object[]{memberOf, groupOfUsers});
}
closeDirectoryConnection(dirCtx);
Collections.sort(members, new BeanComparator("fullName"));
return members;
}
/** {@inheritDoc} */
@Override
public List<UserAccount> findAll() {
return daoService.findAll(UserAccount.class);
}
/**
* Determines if a given user exists in the LDAP directory.
*
* @param id Unique identifier of the user
* @return {@code true} if the {@link UserAccount} exists in the LDAP
* directory, otherwise {@code false}
*/
@Override
public boolean exists(String id) {
this.sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
String uid = getFieldMapping(LdapFieldMapping.USER_MAPPING_USERNAME);
String filter = "(" + uid + "=" + id + ")";
boolean exists = false;
try {
DirContext dirCtx = getDirectoryConnection();
NamingEnumeration results = dirCtx.search(this.groupOfUsers, filter,
this.sc);
exists = results.hasMore();
closeDirectoryConnection(dirCtx);
} catch (NamingException e) {
LOG.log(Level.WARNING, "", e);
}
return exists;
}
/** {@inheritDoc} */
@Override
public UserAccount syncWithDirectory(UserAccount userAccount) throws
UserNotFoundException, DirectoryException {
if (userAccount == null) {
throw new UserNotFoundException(
"null user account passed to synchronisation");
}
LOG.log(Level.INFO, "Synchronising [{0}/{1}/{2}] with directory",
new Object[]{userAccount.getUsername(), userAccount.
getDistinguishedName(), userAccount.getId()});
boolean found = false;
this.sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
String id = userAccount.getUsername();
String uid = getFieldMapping(LdapFieldMapping.USER_MAPPING_USERNAME);
DirContext dirCtx = null;
try {
String base = cfgService.getString(ConfigurationKey.LDAP_BASE);
dirCtx = getDirectoryConnection();
NamingEnumeration results = dirCtx.search(base, "({0}={1})",
new Object[]{uid, id}, this.sc);
if (results.hasMoreElements() && !found) {
SearchResult sr = (SearchResult) results.next();
updateUser(userAccount, sr.getAttributes());
userAccount.setDistinguishedName(sr.getNameInNamespace());
found = true;
}
closeDirectoryConnection(dirCtx);
} catch (CommunicationException e) {
closeDirectoryConnection(dirCtx);
throw new DirectoryException(EXCEPTION_MSG_COULD_NOT_CONNECT_TO_DIRECTORY, e);
} catch (NamingException e) {
closeDirectoryConnection(dirCtx);
throw new DirectoryException(EXCEPTION_MSG_COULD_NOT_CONNECT_TO_DIRECTORY, e);
}
if (!found) {
throw new UserNotFoundException("User [" + id
+ "] was not found in directory");
}
return userAccount;
}
/**
* Finds a {@link UserAccount} by its unique identifier.
*
* @param id Unique identifier of the {@link UserAccount}
* @return {@link UserAccount} matching the unique identifier
* @throws UserNotFoundException If a {@link UserAccount} could not found with the given
* <code>id</code>
* @throws DirectoryException If communication with the user directory failed
*/
@Override
public UserAccount findById(String id) throws UserNotFoundException,
DirectoryException {
if (ANONYMOUS_USER.equalsIgnoreCase(id)) {
throw new UserNotFoundException(id + " is not a valid user");
}
UserAccount ua;
List<UserAccount> matches = daoService.findWithNamedQuery(
UserAccount.FIND_BY_UID, QueryBuilder.with("username", id).
parameters());
if (matches.size() == 1) {
return ua = matches.iterator().next();
} else {
LOG.log(Level.FINE, "No user account found for {0} in database", id);
ua = new UserAccount();
ua.setUsername(id);
ua.setTimeZoneAsString(cfgService.getString(
ConfigurationKey.TIME_ZONE));
try {
ua = syncWithDirectory(ua);
LOG.log(Level.FINE, "Creating user account in database for {0}",
id);
return daoService.create(ua);
} catch (Exception ex) {
throw new UserNotFoundException("User [" + id
+ "] was not found in directory nor database", ex);
}
}
}
/** {@inheritDoc } */
@Override
public UserAccount update(UserAccount user) {
return daoService.update(user);
}
/**
* Sets the properties of a {@link UserAccount} based on a
* {@link SearchResult} from the directory. The process will validate each
* {@link Attribute} to ensure no
* <code>null</code> values or exceptions
* occur.
*
* @param user {@link UserAccount} to update
* @param sr {@link SearchResult} containing the user information
*/
private void updateUser(UserAccount user, final Attributes attrs) {
String fieldUid =
getFieldMapping(LdapFieldMapping.USER_MAPPING_USERNAME);
String fieldMail = getFieldMapping(LdapFieldMapping.USER_MAPPING_EMAIL);
String fieldCommonName = getFieldMapping(
LdapFieldMapping.USER_MAPPING_COMMON_NAME);
String fieldGivenName = getFieldMapping(
LdapFieldMapping.USER_MAPPING_FIRST_NAME);
String fieldSurname = getFieldMapping(
LdapFieldMapping.USER_MAPPING_LAST_NAME);
String fieldJobTitle = getFieldMapping(
LdapFieldMapping.USER_MAPPING_JOB_TITLE);
String fieldMobile = getFieldMapping(
LdapFieldMapping.USER_MAPPING_MOBILE);
String fieldOrganisation = getFieldMapping(
LdapFieldMapping.USER_MAPPING_ORGANISATION);
String fieldPhone = getFieldMapping(LdapFieldMapping.USER_MAPPING_PHONE);
String fieldLanguage = getFieldMapping(
LdapFieldMapping.USER_MAPPING_LANGUAGE);
String fieldPhoto = getFieldMapping(
LdapFieldMapping.USER_MAPPING_JPEG_PHOTO);
String fieldEmploymentType = getFieldMapping(
LdapFieldMapping.USER_MAPPING_EMPLOYMENT_TYPE);
String employmentTypeFreelance = getFieldMapping(
LdapFieldMapping.EMPLOYMENT_TYPE_MAPPING_FREELANCE);
String employmentTypePermanent = getFieldMapping(
LdapFieldMapping.EMPLOYMENT_TYPE_MAPPING_PERMANENT);
String fieldFeeType = getFieldMapping(
LdapFieldMapping.USER_MAPPING_FEE_TYPE);
String feeTypeStory = getFieldMapping(
LdapFieldMapping.FEE_TYPE_MAPPING_STORY);
String feeTypeFixed = getFieldMapping(
LdapFieldMapping.FEE_TYPE_MAPPING_FIXED);
String feeTypeWord = getFieldMapping(
LdapFieldMapping.FEE_TYPE_MAPPING_WORD);
user.setUsername(LdapUtils.validateAttribute(attrs.get(fieldUid)));
user.setEmail(LdapUtils.validateAttribute(attrs.get(fieldMail)));
user.setFullName(LdapUtils.validateAttribute(attrs.get(fieldCommonName)));
user.setGivenName(LdapUtils.validateAttribute(attrs.get(fieldGivenName)));
user.setJobTitle(LdapUtils.validateAttribute(attrs.get(fieldJobTitle)));
user.setMobile(LdapUtils.validateAttribute(attrs.get(fieldMobile)));
user.setOrganisation(LdapUtils.validateAttribute(attrs.get(
fieldOrganisation)));
user.setPhone(LdapUtils.validateAttribute(attrs.get(fieldPhone)));
// If the user does not have a preferred language, get the one specified
// in the LDAP directory
if (StringUtils.isBlank(user.getPreferredLanguage())) {
user.setPreferredLanguage(LdapUtils.validateAttribute(attrs.get(
fieldLanguage)));
}
// If no preferred language was specified in the profile nor in the LDAP
// use the default language specified for the application
if (StringUtils.isBlank(user.getPreferredLanguage())) {
String language = cfgService.getString(ConfigurationKey.LANGUAGE);
user.setPreferredLanguage(language);
}
user.setSurname(LdapUtils.validateAttribute(attrs.get(fieldSurname)));
String employmentType = LdapUtils.validateAttribute(attrs.get(
fieldEmploymentType));
String feeType = LdapUtils.validateAttribute(attrs.get(fieldFeeType));
if (employmentType.equalsIgnoreCase(employmentTypePermanent)) {
user.setEmploymentType(EmploymentType.PERMANENT);
} else if (employmentType.equalsIgnoreCase(employmentTypeFreelance)) {
user.setEmploymentType(EmploymentType.FREELANCE);
} else {
user.setEmploymentType(EmploymentType.UNKNOWN);
}
if (feeType.equalsIgnoreCase(feeTypeStory)) {
user.setFeeType(FeeType.ARTICLE);
} else if (feeType.equalsIgnoreCase(feeTypeFixed)) {
user.setFeeType(FeeType.FIXED);
} else if (feeType.equalsIgnoreCase(feeTypeWord)) {
user.setFeeType(FeeType.WORD);
} else {
user.setFeeType(FeeType.UNKNOWN);
}
}
/**
* Setup field mapping for the directory service.
*/
private void setupUserMapping() {
groupOfUsers = cfgService.getString(ConfigurationKey.LDAP_GROUP_USERS);
groupOfAdministrators = cfgService.getString(
ConfigurationKey.LDAP_GROUP_ADMINISTRATORS);
addMapping(LdapFieldMapping.USER_MAPPING_USERNAME,
cfgService.getString(ConfigurationKey.LDAP_USER_MAPPING_USERNAME));
addMapping(LdapFieldMapping.USER_MAPPING_EMAIL,
cfgService.getString(ConfigurationKey.LDAP_USER_MAPPING_EMAIL));
addMapping(LdapFieldMapping.USER_MAPPING_COMMON_NAME, cfgService.
getString(ConfigurationKey.LDAP_USER_MAPPING_COMMON_NAME));
addMapping(LdapFieldMapping.USER_MAPPING_FIRST_NAME, cfgService.
getString(ConfigurationKey.LDAP_USER_MAPPING_FIRST_NAME));
addMapping(LdapFieldMapping.USER_MAPPING_LAST_NAME,
cfgService.getString(
ConfigurationKey.LDAP_USER_MAPPING_LAST_NAME));
addMapping(LdapFieldMapping.USER_MAPPING_JOB_TITLE,
cfgService.getString(
ConfigurationKey.LDAP_USER_MAPPING_JOB_TITLE));
addMapping(LdapFieldMapping.USER_MAPPING_MOBILE,
cfgService.getString(ConfigurationKey.LDAP_USER_MAPPING_MOBILE));
addMapping(LdapFieldMapping.USER_MAPPING_ORGANISATION, cfgService.
getString(ConfigurationKey.LDAP_USER_MAPPING_ORGANISATION));
addMapping(LdapFieldMapping.USER_MAPPING_PHONE,
cfgService.getString(ConfigurationKey.LDAP_USER_MAPPING_PHONE));
addMapping(LdapFieldMapping.USER_MAPPING_LANGUAGE,
cfgService.getString(ConfigurationKey.LDAP_USER_MAPPING_LANGUAGE));
addMapping(LdapFieldMapping.USER_MAPPING_JPEG_PHOTO, cfgService.
getString(ConfigurationKey.LDAP_USER_MAPPING_JPEG_PHOTO));
addMapping(LdapFieldMapping.USER_MAPPING_EMPLOYMENT_TYPE, cfgService.
getString(ConfigurationKey.LDAP_USER_MAPPING_EMPLOYEE_TYPE));
addMapping(LdapFieldMapping.EMPLOYMENT_TYPE_MAPPING_FREELANCE,
cfgService.getString(
ConfigurationKey.LDAP_EMPLOYMENT_TYPE_MAPPING_FREELANCE));
addMapping(LdapFieldMapping.EMPLOYMENT_TYPE_MAPPING_PERMANENT,
cfgService.getString(
ConfigurationKey.LDAP_EMPLOYMENT_TYPE_MAPPING_PERMANENT));
addMapping(LdapFieldMapping.USER_MAPPING_FEE_TYPE,
cfgService.getString(ConfigurationKey.LDAP_USER_MAPPING_FEE_TYPE));
addMapping(LdapFieldMapping.FEE_TYPE_MAPPING_STORY,
cfgService.getString(
ConfigurationKey.LDAP_FEE_TYPE_MAPPING_STORY));
addMapping(LdapFieldMapping.FEE_TYPE_MAPPING_FIXED,
cfgService.getString(
ConfigurationKey.LDAP_FEE_TYPE_MAPPING_FIXED));
addMapping(LdapFieldMapping.FEE_TYPE_MAPPING_WORD,
cfgService.getString(ConfigurationKey.LDAP_FEE_TYPE_MAPPING_WORD));
addMapping(LdapFieldMapping.GROUP_MAPPING_NAME,
cfgService.getString(ConfigurationKey.LDAP_GROUP_MAPPING_NAME));
addMapping(LdapFieldMapping.GROUP_MAPPING_MEMBEROF,
cfgService.getString(
ConfigurationKey.LDAP_GROUP_MAPPING_MEMBEROF));
}
/**
* Adds a field mapping to the mapping table.
*
* @param fieldIdentifier Field identifier
* @param fieldName Real name of the field
*/
private void addMapping(String fieldIdentifier, String fieldName) {
this.fieldMapping.put(fieldIdentifier, fieldName);
}
/**
* Gets a field mapping from the mapping table.
*
* @param fieldIdentifier Field identifier
* @return Real name of the field given
*/
private String getFieldMapping(String fieldIdentifier) {
if (this.fieldMapping.containsKey(fieldIdentifier)) {
return this.fieldMapping.get(fieldIdentifier);
} else {
return "";
}
}
private void addMapping(LdapFieldMapping fieldIdentifier, String fieldName) {
addMapping(fieldIdentifier.name(), fieldName);
}
private String getFieldMapping(LdapFieldMapping fieldMapping) {
return getFieldMapping(fieldMapping.name());
}
/**
* Obtains the connection to the LDAP directory used for storing users and
* user groups.
*
* @return Established connection to the used LDAP directory
* @throws NamingException If the connection could not be established
*/
private DirContext getDirectoryConnection() throws NamingException {
LOG.log(Level.FINE, "Opening directory connection");
Properties p = new Properties();
p.put(Context.INITIAL_CONTEXT_FACTORY,
cfgService.getString(ConfigurationKey.LDAP_CONNECTION_FACTORY));
p.put("com.sun.jndi.ldap.connect.pool", "true");
p.put("com.sun.jndi.ldap.read.timeout",
cfgService.getString(ConfigurationKey.LDAP_READ_TIMEOUT));
p.put("com.sun.jndi.ldap.connect.timeout",
cfgService.getString(ConfigurationKey.LDAP_CONNECT_TIMEOUT));
p.put(Context.PROVIDER_URL, cfgService.getString(
ConfigurationKey.LDAP_PROVIDER_URL));
p.put(Context.SECURITY_AUTHENTICATION,
cfgService.getString(
ConfigurationKey.LDAP_SECURITY_AUTHENTICATION));
p.put(Context.SECURITY_PRINCIPAL, cfgService.getString(
ConfigurationKey.LDAP_SECURITY_PRINCIPAL));
p.put(Context.SECURITY_CREDENTIALS,
cfgService.getString(ConfigurationKey.LDAP_SECURITY_CREDENTIALS));
if (LOG.isLoggable(Level.FINE)) {
LOG.log(Level.FINE, "INITIAL_CONTEXT_FACTORY: {0}",
p.getProperty(Context.INITIAL_CONTEXT_FACTORY));
LOG.log(Level.FINE, "PROVIDER_URL: {0}", p.getProperty(
Context.PROVIDER_URL));
LOG.log(Level.FINE, "SECURITY_AUTHENTICATION: {0}",
p.getProperty(Context.SECURITY_AUTHENTICATION));
LOG.log(Level.FINE, "SECURITY_PRINCIPAL: {0}",
p.getProperty(Context.SECURITY_PRINCIPAL));
LOG.log(Level.FINE, "SECURITY_CREDENTIALS: {0}",
p.getProperty(Context.SECURITY_CREDENTIALS));
LOG.log(Level.FINE, "LDAP_BASE: {0}",
cfgService.getString(ConfigurationKey.LDAP_BASE));
LOG.log(Level.FINE, "LDAP_CONNECTION_TIMEOUT: {0}", cfgService.
getString(ConfigurationKey.LDAP_CONNECT_TIMEOUT));
LOG.log(Level.FINE, "LDAP_READ_TIMEOUT: {0}",
cfgService.getString(ConfigurationKey.LDAP_READ_TIMEOUT));
}
return new InitialDirContext(p);
}
private void closeDirectoryConnection(DirContext dirContext) {
LOG.log(Level.FINE, "Closing directory connection");
if (dirContext == null) {
LOG.log(Level.WARNING, "Could not close DirContext as it is null");
return;
}
try {
dirContext.close();
} catch (NamingException ex) {
LOG.log(Level.SEVERE, "Could not close DirContext. ", ex);
}
}
/** {@inheritDoc } */
@Override
public List<UserRole> getUserRoles() {
return daoService.findAll(UserRole.class);
}
/** {@inheritDoc } */
@Override
public UserRole findUserRoleById(Long id) throws DataNotFoundException {
return daoService.findById(UserRole.class, id);
}
/** {@inheritDoc } */
@Override
public List<UserAccount> findUserAccountsByUserRoleName(String name) {
Map<String, Object> params = QueryBuilder.with("roleName", name).
parameters();
return daoService.findWithNamedQuery(UserAccount.FIND_BY_USER_ROLE,
params);
}
/** {@inheritDoc } */
@Override
public void update(UserRole userRole) {
daoService.update(userRole);
}
/** {@inheritDoc } */
@Override
public UserRole create(UserRole selected) {
return daoService.create(selected);
}
/** {@inheritDoc } */
@Override
public void delete(UserRole userRole) {
daoService.delete(UserRole.class, userRole.getId());
}
/** {@inheritDoc} */
@Override
public Privilege findPrivilegeById(String id) throws DataNotFoundException {
SystemPrivilege sp = null;
try {
sp = SystemPrivilege.valueOf(id);
return daoService.findById(Privilege.class, sp);
} catch (IllegalArgumentException ex) {
throw new DataNotFoundException(ex);
} catch (DataNotFoundException ex) {
Privilege p = new Privilege(sp);
return daoService.create(p);
}
}
}