/*
* Copyright 2012-2013, CMM, University of Queensland.
*
* This file is part of Eccles.
*
* Eccles 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.
*
* Eccles 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 Eccles. If not, see <http://www.gnu.org/licenses/>.
*/
package au.edu.uq.cmm.eccles;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.NoResultException;
import javax.persistence.PersistenceException;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import au.edu.uq.cmm.aclslib.authenticator.AclsLoginDetails;
import au.edu.uq.cmm.aclslib.config.FacilityConfig;
import au.edu.uq.cmm.aclslib.message.Certification;
public class EcclesUserDetailsManager implements UserDetailsManager {
private static final Logger LOG =
LoggerFactory.getLogger(EcclesUserDetailsManager.class);
private Random random = new Random();
private EntityManagerFactory emf;
private EcclesFallbackMode fallbackMode;
public EcclesUserDetailsManager(EntityManagerFactory emf,
EcclesFallbackMode fallbackMode) {
this.emf = Objects.requireNonNull(emf);
this.fallbackMode = Objects.requireNonNull(fallbackMode);
}
@Override
public UserDetails lookupUser(String userName, boolean fetchCollections)
throws UserDetailsException {
EntityManager em = emf.createEntityManager();
try {
TypedQuery<UserDetails> query = em.createQuery(
"from UserDetails u where u.userName = :userName",
UserDetails.class);
query.setParameter("userName", userName);
UserDetails userDetails = query.getSingleResult();
if (fetchCollections) {
userDetails.getAccounts().size();
userDetails.getCertifications().size();
}
return userDetails;
} catch (NoResultException ex) {
throw new UserDetailsException("User '" + userName + "' not found");
} finally {
emClose(em);
}
}
@Override
public List<String> getUserNames() {
EntityManager em = emf.createEntityManager();
try {
TypedQuery<String> query = em.createQuery(
"select u.userName from UserDetails u", String.class);
return query.getResultList();
} finally {
emClose(em);
}
}
@Override
public List<UserDetails> getUsers() {
EntityManager em = emf.createEntityManager();
try {
TypedQuery<UserDetails> query = em.createQuery(
"from UserDetails u", UserDetails.class);
return query.getResultList();
} finally {
emClose(em);
}
}
@Override
public void addUser(UserDetails user) throws UserDetailsException {
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
em.persist(user);
em.getTransaction().commit();
} catch (PersistenceException ex) {
throw new UserDetailsException(
"User '" + user.getUserName() + "' already exists");
} finally {
emClose(em);
}
}
@Override
public void removeUser(String userName) throws UserDetailsException {
EntityManager em = emf.createEntityManager();
try {
em.getTransaction().begin();
TypedQuery<UserDetails> query = em.createQuery(
"from UserDetails u where u.userName = :userName",
UserDetails.class);
query.setParameter("userName", userName);
UserDetails userDetails = query.getSingleResult();
em.remove(userDetails);
em.getTransaction().commit();
} catch (NoResultException ex) {
throw new UserDetailsException("User '" + userName + "' not found");
} finally {
emClose(em);
}
}
@Override
public void refreshUserDetails(EntityManager em, String userName, String email,
AclsLoginDetails loginDetails) {
try {
TypedQuery<UserDetails> query = em.createQuery(
"from UserDetails u where u.userName = :userName",
UserDetails.class);
query.setParameter("userName", userName);
UserDetails userDetails = query.getSingleResult();
LOG.debug("Refreshing cached user details for " + userName);
String digest = createDigest(loginDetails.getPassword().toLowerCase(), userDetails.getSeed());
userDetails.setAccounts(new HashSet<String>(loginDetails.getAccounts()));
userDetails.setDigest(digest);
userDetails.setOrgName(loginDetails.getOrgName());
userDetails.setHumanReadableName(loginDetails.getHumanReadableName());
userDetails.setOnsiteAssist(loginDetails.isOnsiteAssist());
userDetails.getCertifications().put(
loginDetails.getFacilityName(),
loginDetails.getCertification().toString());
} catch (NoResultException ex) {
LOG.debug("Caching user details for " + userName);
long seed = random.nextLong();
String digest = createDigest(loginDetails.getPassword().toLowerCase(), seed);
UserDetails newDetails = new UserDetails(
userName, email, loginDetails, seed, digest);
em.persist(newDetails);
}
}
@Override
/**
* Perform fallback authentication against cached user details.
* The actual behavior depends on the current 'fallbackMode'
* setting, as described by the type. If non-null, the resulting
* AclsLoginDetails object will give the user's cached certification
* for the Facility if available, defaulting to VALID if there
* is no cached certification information for the Facility.
*/
public AclsLoginDetails authenticate(
String userName, String password, FacilityConfig facility) {
LOG.debug("Called fallback authenticator (" + fallbackMode + ") for " + userName);
if (fallbackMode == EcclesFallbackMode.NO_FALLBACK) {
return null;
}
try {
UserDetails userDetails = lookupUser(userName, true);
if (fallbackMode == EcclesFallbackMode.USER_ONLY) {
LOG.debug("Skipping the password check for " + userName);
return buildDetails(userDetails, facility);
}
String savedDigest = userDetails.getDigest();
if (savedDigest == null) {
if (fallbackMode == EcclesFallbackMode.USER_PASSWORD_OPTIONAL) {
LOG.debug("Skipping the optional password check for " + userName);
return buildDetails(userDetails, facility);
} else {
LOG.debug("User " + userName + " has no cached password");
return null;
}
}
LOG.debug("Doing the password check for " + userName);
// (Backwards compatibility hack. Passwords are case-insensitive (ACLS ... sigh)
// but we used to treat them as case-sensitive when we cached them ...)
for (String p : new String[]{password.toLowerCase(), password}) {
String myDigest = createDigest(p, userDetails.getSeed());
LOG.debug("Comparing " + myDigest + " with " + savedDigest);
if (myDigest.equals(savedDigest)) {
return buildDetails(userDetails, facility);
}
}
return null;
} catch (UserDetailsException ex) {
LOG.debug("Unknown user " + userName);
return null;
}
}
private AclsLoginDetails buildDetails(
UserDetails userDetails, FacilityConfig facility) {
// The default certification is 'VALID'
String certString = userDetails.getCertifications().get(facility.getFacilityName());
Certification cert = (certString == null) ?
Certification.VALID : Certification.parse(certString);
// The default account is 'unknown account'
List<String> accounts;
if (userDetails.getAccounts().isEmpty()) {
accounts = Collections.singletonList("unknown account");
} else {
accounts = new ArrayList<>(userDetails.getAccounts());
}
String hrName = userDetails.getHumanReadableName();
if (hrName == null) {
hrName = "";
}
String orgName = userDetails.getOrgName();
if (orgName == null) {
orgName = "";
}
return new AclsLoginDetails(userDetails.getUserName(),
hrName, orgName, null, facility.getFacilityName(),
accounts, cert, userDetails.isOnsiteAssist(), true);
}
public static String createDigest(String password, long seed) {
LOG.debug("Creating digest for password using seed " + seed);
try {
MessageDigest digester = MessageDigest.getInstance("MD5");
for (int i = 0; i < 8; i++) {
byte b = (byte) ((seed >> (i * 8)) & 0xff);
digester.update(b);
}
digester.update(password.getBytes("UTF-8"));
String res = toString(digester.digest());
LOG.debug("Created digest - " + res);
return res;
} catch (NoSuchAlgorithmException ex) {
throw new AssertionError("Don't understand MD5?", ex);
} catch (UnsupportedEncodingException ex) {
throw new AssertionError("Don't understand UTF-8?", ex);
}
}
private static String toString(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2 + 6);
sb.append(bytes.length).append(":");
for (byte b : bytes) {
sb.append("0123456789ABCDEF".charAt((b >> 4) & 0xf));
sb.append("0123456789ABCDEF".charAt(b & 0xf));
}
return sb.toString();
}
private void emClose(EntityManager em) {
if (em.getTransaction().isActive()) {
em.getTransaction().rollback();
}
em.close();
}
}