package edu.harvard.iq.dataverse.authorization; import edu.harvard.iq.dataverse.UserNotification; import edu.harvard.iq.dataverse.UserNotificationServiceBean; import edu.harvard.iq.dataverse.search.IndexServiceBean; import edu.harvard.iq.dataverse.actionlogging.ActionLogRecord; import edu.harvard.iq.dataverse.actionlogging.ActionLogServiceBean; import edu.harvard.iq.dataverse.authorization.exceptions.AuthenticationFailedException; import edu.harvard.iq.dataverse.authorization.exceptions.AuthenticationProviderFactoryNotFoundException; import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationSetupException; import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderFactory; import edu.harvard.iq.dataverse.authorization.providers.AuthenticationProviderRow; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinAuthenticationProviderFactory; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUser; import edu.harvard.iq.dataverse.authorization.providers.builtin.BuiltinUserServiceBean; import edu.harvard.iq.dataverse.authorization.providers.echo.EchoAuthenticationProviderFactory; import edu.harvard.iq.dataverse.authorization.providers.shib.ShibAuthenticationProvider; import edu.harvard.iq.dataverse.authorization.users.ApiToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailData; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; import edu.harvard.iq.dataverse.passwordreset.PasswordResetData; import edu.harvard.iq.dataverse.passwordreset.PasswordResetServiceBean; import java.sql.SQLException; import java.sql.Timestamp; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import java.util.logging.Level; import java.util.logging.Logger; import javax.annotation.PostConstruct; import javax.ejb.EJB; import javax.ejb.Singleton; import javax.persistence.EntityManager; import javax.persistence.NoResultException; import javax.persistence.NonUniqueResultException; import javax.persistence.PersistenceContext; import javax.persistence.TypedQuery; import javax.validation.ConstraintViolation; import javax.validation.Validation; import javax.validation.Validator; import javax.validation.ValidatorFactory; /** * The AuthenticationManager is responsible for registering and listing * AuthenticationProviders. There's a single instance per application. * * Register the providers in the {@link #startup()} method. */ @Singleton public class AuthenticationServiceBean { private static final Logger logger = Logger.getLogger(AuthenticationServiceBean.class.getName()); /** * Where all registered authentication providers live. */ final Map<String, AuthenticationProvider> authenticationProviders = new HashMap<>(); final Map<String, AuthenticationProviderFactory> providerFactories = new HashMap<>(); @EJB BuiltinUserServiceBean builtinUserServiceBean; @EJB IndexServiceBean indexService; @EJB protected ActionLogServiceBean actionLogSvc; @EJB UserNotificationServiceBean userNotificationService; @EJB ConfirmEmailServiceBean confirmEmailService; @EJB PasswordResetServiceBean passwordResetServiceBean; @PersistenceContext(unitName = "VDCNet-ejbPU") private EntityManager em; @PostConstruct public void startup() { // First, set up the factories try { registerProviderFactory( new BuiltinAuthenticationProviderFactory(builtinUserServiceBean) ); registerProviderFactory( new EchoAuthenticationProviderFactory() ); /** * Register shib provider factory here. Test enable/disable via Admin API, etc. */ new ShibAuthenticationProvider(); } catch (AuthorizationSetupException ex) { logger.log(Level.SEVERE, "Exception setting up the authentication provider factories: " + ex.getMessage(), ex); } // Now, load the providers. for ( AuthenticationProviderRow row : em.createNamedQuery("AuthenticationProviderRow.findAllEnabled", AuthenticationProviderRow.class) .getResultList() ) { try { registerProvider( loadProvider(row) ); } catch ( AuthenticationProviderFactoryNotFoundException e ) { logger.log(Level.SEVERE, "Cannot find authentication provider factory with alias '" + e.getFactoryAlias() + "'",e); } catch (AuthorizationSetupException ex) { logger.log(Level.SEVERE, "Exception setting up the authentication provider '" + row.getId() + "': " + ex.getMessage(), ex); } } } public void registerProviderFactory(AuthenticationProviderFactory aFactory) throws AuthorizationSetupException { if ( providerFactories.containsKey(aFactory.getAlias()) ) { throw new AuthorizationSetupException( "Duplicate alias " + aFactory.getAlias() + " for authentication provider factory."); } providerFactories.put( aFactory.getAlias(), aFactory); logger.log( Level.FINE, "Registered Authentication Provider Factory {0} as {1}", new Object[]{aFactory.getInfo(), aFactory.getAlias()}); } /** * Tries to load and {@link AuthenticationProvider} using the passed {@link AuthenticationProviderRow}. * @param aRow The row to load the provider from. * @return The provider, if successful * @throws AuthenticationProviderFactoryNotFoundException If the row specifies a non-existent factory * @throws AuthorizationSetupException If the factory failed to instantiate a provider from the row. */ public AuthenticationProvider loadProvider( AuthenticationProviderRow aRow ) throws AuthenticationProviderFactoryNotFoundException, AuthorizationSetupException { AuthenticationProviderFactory fact = getProviderFactory(aRow.getFactoryAlias()); if ( fact == null ) throw new AuthenticationProviderFactoryNotFoundException(aRow.getFactoryAlias()); return fact.buildProvider(aRow); } public void registerProvider(AuthenticationProvider aProvider) throws AuthorizationSetupException { if ( authenticationProviders.containsKey(aProvider.getId()) ) { throw new AuthorizationSetupException( "Duplicate id " + aProvider.getId() + " for authentication provider."); } authenticationProviders.put( aProvider.getId(), aProvider); actionLogSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.Auth, "registerProvider") .setInfo(aProvider.getId() + ":" + aProvider.getInfo().getTitle())); } public void deregisterProvider( String id ) { authenticationProviders.remove( id ); actionLogSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.Auth, "deregisterProvider") .setInfo(id)); logger.log(Level.INFO,"Deregistered provider {0}", new Object[]{id}); logger.log(Level.INFO,"Providers left {0}", new Object[]{getAuthenticationProviderIds()}); } public Set<String> getAuthenticationProviderIds() { return authenticationProviders.keySet(); } public <T extends AuthenticationProvider> Set<String> getAuthenticationProviderIdsOfType( Class<T> aClass ) { Set<String> retVal = new TreeSet<>(); for ( Map.Entry<String, AuthenticationProvider> p : authenticationProviders.entrySet() ) { if ( aClass.isAssignableFrom( p.getValue().getClass() ) ) { retVal.add( p.getKey() ); } } return retVal; } public AuthenticationProviderFactory getProviderFactory( String alias ) { return providerFactories.get(alias); } public AuthenticationProvider getAuthenticationProvider( String id ) { return authenticationProviders.get( id ); } public AuthenticatedUser findByID(Object pk){ if (pk==null){ return null; } return em.find(AuthenticatedUser.class, pk); } public void removeApiToken(AuthenticatedUser user){ if (user!=null) { ApiToken apiToken = findApiTokenByUser(user); if (apiToken != null) { em.remove(apiToken); } } } /** * Use with care! This method was written primarily for developers * interested in API testing who want to: * * 1. Create a temporary user and get an API token. * * 2. Do some work with that API token. * * 3. Delete all the stuff that was created with the API token. * * 4. Delete the temporary user. * * Before calling this method, make sure you've deleted all the stuff tied * to the user, including stuff they've created, role assignments, group * assignments, etc. * * Longer term, the intention is to have a "disableAuthenticatedUser" * method/command. See https://github.com/IQSS/dataverse/issues/2419 */ public void deleteAuthenticatedUser(Object pk) { AuthenticatedUser user = em.find(AuthenticatedUser.class, pk); if (user != null) { ApiToken apiToken = findApiTokenByUser(user); if (apiToken != null) { em.remove(apiToken); } ConfirmEmailData confirmEmailData = confirmEmailService.findSingleConfirmEmailDataByUser(user); if (confirmEmailData != null) { /** * @todo This could probably be a cascade delete instead. */ em.remove(confirmEmailData); } for (UserNotification notification : userNotificationService.findByUser(user.getId())) { userNotificationService.delete(notification); } if (user.isBuiltInUser()) { BuiltinUser builtin = builtinUserServiceBean.findByUserName(user.getUserIdentifier()); em.remove(builtin); } actionLogSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.Auth, "deleteUser") .setInfo(user.getUserIdentifier())); em.remove(user.getAuthenticatedUserLookup()); em.remove(user); } } public AuthenticatedUser getAuthenticatedUser( String identifier ) { try { return em.createNamedQuery("AuthenticatedUser.findByIdentifier", AuthenticatedUser.class) .setParameter("identifier", identifier) .getSingleResult(); } catch ( NoResultException nre ) { return null; } } public AuthenticatedUser getAdminUser() { try { return em.createNamedQuery("AuthenticatedUser.findAdminUser", AuthenticatedUser.class) .setMaxResults(1) .getSingleResult(); } catch (Exception ex) { return null; } } public AuthenticatedUser getAuthenticatedUserByEmail( String email ) { try { return em.createNamedQuery("AuthenticatedUser.findByEmail", AuthenticatedUser.class) .setParameter("email", email) .getSingleResult(); } catch ( NoResultException ex ) { logger.log(Level.INFO, "no user found using {0}", email); return null; } catch ( NonUniqueResultException ex ) { logger.log(Level.INFO, "multiple users found using {0}: {1}", new Object[]{email, ex}); return null; } } public AuthenticatedUser authenticate( String authenticationProviderId, AuthenticationRequest req ) throws AuthenticationFailedException { AuthenticationProvider prv = getAuthenticationProvider(authenticationProviderId); if ( prv == null ) throw new IllegalArgumentException("No authentication provider listed under id " + authenticationProviderId ); AuthenticationResponse resp = prv.authenticate(req); if ( resp.getStatus() == AuthenticationResponse.Status.SUCCESS ) { // yay! see if we already have this user. AuthenticatedUser user = lookupUser(authenticationProviderId, resp.getUserId()); /** * @todo Why does a method called "authenticate" have the potential * to call "createAuthenticatedUser"? Isn't the creation of a user a * different action than authenticating? */ return ( user == null ) ? AuthenticationServiceBean.this.createAuthenticatedUser( new UserRecordIdentifier(authenticationProviderId, resp.getUserId()), resp.getUserId(), resp.getUserDisplayInfo(), true ) : updateAuthenticatedUser( user, resp.getUserDisplayInfo() ); } else { throw new AuthenticationFailedException(resp, "Authentication Failed: " + resp.getMessage()); } } public AuthenticatedUser lookupUser(String authPrvId, String userPersistentId) { TypedQuery<AuthenticatedUserLookup> typedQuery = em.createNamedQuery("AuthenticatedUserLookup.findByAuthPrvID_PersUserId", AuthenticatedUserLookup.class); typedQuery.setParameter("authPrvId", authPrvId); typedQuery.setParameter("persUserId", userPersistentId); try { AuthenticatedUserLookup au = typedQuery.getSingleResult(); return au.getAuthenticatedUser(); } catch (NoResultException | NonUniqueResultException ex) { return null; } } public ApiToken findApiToken(String token) { try { return em.createNamedQuery("ApiToken.findByTokenString", ApiToken.class) .setParameter("tokenString", token) .getSingleResult(); } catch (NoResultException ex) { return null; } } public ApiToken findApiTokenByUser(AuthenticatedUser au) { if (au == null) { return null; } TypedQuery<ApiToken> typedQuery = em.createNamedQuery("ApiToken.findByUser", ApiToken.class); typedQuery.setParameter("user", au); try { return typedQuery.getSingleResult(); } catch (NoResultException | NonUniqueResultException ex) { logger.log(Level.INFO, "When looking up API token for {0} caught {1}", new Object[]{au, ex}); return null; } } // A method for generating a new API token; // TODO: this is a simple, one-size-fits-all solution; we'll need // to expand this system, to be able to generate tokens with different // lifecycles/valid for specific actions only, etc. // -- L.A. 4.0 beta12 public ApiToken generateApiTokenForUser(AuthenticatedUser au) { if (au == null) { return null; } ApiToken apiToken = new ApiToken(); apiToken.setTokenString(java.util.UUID.randomUUID().toString()); apiToken.setAuthenticatedUser(au); Calendar c = Calendar.getInstance(); apiToken.setCreateTime(new Timestamp(c.getTimeInMillis())); c.roll(Calendar.YEAR, 1); apiToken.setExpireTime(new Timestamp(c.getTimeInMillis())); save(apiToken); actionLogSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.Auth, "generateApiToken") .setInfo("user:" + au.getIdentifier() + " token:" + apiToken.getTokenString())); return apiToken; } public AuthenticatedUser lookupUser( String apiToken ) { ApiToken tkn = findApiToken(apiToken); if ( tkn == null ) return null; if ( tkn.isDisabled() ) return null; if ( tkn.getExpireTime() != null ) { if ( tkn.getExpireTime().before( new Timestamp(new Date().getTime())) ) { em.remove(tkn); return null; } } return tkn.getAuthenticatedUser(); } public AuthenticatedUser save( AuthenticatedUser user ) { user.setModificationTime(getCurrentTimestamp()); em.persist(user); em.flush(); return user; } public AuthenticatedUser update( AuthenticatedUser user ) { user.setModificationTime(getCurrentTimestamp()); return em.merge(user); } public ApiToken save( ApiToken aToken ) { if ( aToken.getId() == null ) { em.persist(aToken); return aToken; } else { return em.merge( aToken ); } } /** * Creates an authenticated user based on the passed * {@code userDisplayInfo}, a lookup entry for them based * UserIdentifier.getLookupStringPerAuthProvider (within the supplied * authentication provider), and internal user identifier (used for role * assignments, etc.) based on UserIdentifier.getInternalUserIdentifer. * * @param userRecordId * @param proposedAuthenticatedUserIdentifier * @param userDisplayInfo * @param generateUniqueIdentifier if {@code true}, create a new, unique user identifier for the created user, if the suggested one exists. * @return the newly created user, or {@code null} if the proposed identifier exists and {@code generateUniqueIdentifier} was {@code false}. * @throws EJBException which may wrap an ConstraintViolationException if the proposed user does not pass bean validation. */ public AuthenticatedUser createAuthenticatedUser(UserRecordIdentifier userRecordId, String proposedAuthenticatedUserIdentifier, AuthenticatedUserDisplayInfo userDisplayInfo, boolean generateUniqueIdentifier) { AuthenticatedUser authenticatedUser = new AuthenticatedUser(); authenticatedUser.applyDisplayInfo(userDisplayInfo); // we have no desire for leading or trailing whitespace in identifiers if (proposedAuthenticatedUserIdentifier != null) { proposedAuthenticatedUserIdentifier = proposedAuthenticatedUserIdentifier.trim(); } // we now select a username for the generated AuthenticatedUser, or give up String internalUserIdentifer = proposedAuthenticatedUserIdentifier; // TODO should lock table authenticated users for write here if ( identifierExists(internalUserIdentifer) ) { if ( ! generateUniqueIdentifier ) { return null; } int i=1; String identifier = internalUserIdentifer + i; while ( identifierExists(identifier) ) { i += 1; } authenticatedUser.setUserIdentifier(identifier); } else { authenticatedUser.setUserIdentifier(internalUserIdentifer); } authenticatedUser = save( authenticatedUser ); // TODO should unlock table authenticated users for write here AuthenticatedUserLookup auusLookup = userRecordId.createAuthenticatedUserLookup(authenticatedUser); em.persist( auusLookup ); authenticatedUser.setAuthenticatedUserLookup(auusLookup); if (ShibAuthenticationProvider.PROVIDER_ID.equals(auusLookup.getAuthenticationProviderId())) { Timestamp emailConfirmedNow = new Timestamp(new Date().getTime()); // Email addresses for Shib users are confirmed by the Identity Provider. authenticatedUser.setEmailConfirmed(emailConfirmedNow); authenticatedUser = save(authenticatedUser); } else { /** * @todo Rather than creating a token directly here it might be * better to do something like "startConfirmEmailProcessForNewUser". */ confirmEmailService.createToken(authenticatedUser); } actionLogSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.Auth, "createUser") .setInfo(authenticatedUser.getIdentifier())); return authenticatedUser; } public boolean identifierExists( String idtf ) { return em.createNamedQuery("AuthenticatedUser.countOfIdentifier", Number.class) .setParameter("identifier", idtf) .getSingleResult().intValue() > 0; } public AuthenticatedUser updateAuthenticatedUser(AuthenticatedUser user, AuthenticatedUserDisplayInfo userDisplayInfo) { user.applyDisplayInfo(userDisplayInfo); actionLogSvc.log( new ActionLogRecord(ActionLogRecord.ActionType.Auth, "updateUser") .setInfo(user.getIdentifier())); return update(user); } public List<AuthenticatedUser> findAllAuthenticatedUsers() { return em.createNamedQuery("AuthenticatedUser.findAll", AuthenticatedUser.class).getResultList(); } public List<AuthenticatedUser> findSuperUsers() { return em.createNamedQuery("AuthenticatedUser.findSuperUsers", AuthenticatedUser.class).getResultList(); } public Set<AuthenticationProviderFactory> listProviderFactories() { return new HashSet<>( providerFactories.values() ); } public Timestamp getCurrentTimestamp() { return new Timestamp(new Date().getTime()); } // TODO should probably be moved to the Shib provider - this is a classic Shib-specific // use case. This class should deal with general autnetications. public AuthenticatedUser convertBuiltInToShib(AuthenticatedUser builtInUserToConvert, String shibProviderId, UserIdentifier newUserIdentifierInLookupTable) { logger.info("converting user " + builtInUserToConvert.getId() + " from builtin to shib"); String builtInUserIdentifier = builtInUserToConvert.getIdentifier(); logger.info("builtin user identifier: " + builtInUserIdentifier); TypedQuery<AuthenticatedUserLookup> typedQuery = em.createQuery("SELECT OBJECT(o) FROM AuthenticatedUserLookup AS o WHERE o.authenticatedUser = :auid", AuthenticatedUserLookup.class); typedQuery.setParameter("auid", builtInUserToConvert); AuthenticatedUserLookup authuserLookup; try { authuserLookup = typedQuery.getSingleResult(); } catch (NoResultException | NonUniqueResultException ex) { logger.info("exception caught: " + ex); return null; } if (authuserLookup == null) { return null; } String oldProviderId = authuserLookup.getAuthenticationProviderId(); logger.info("we expect this to be 'builtin': " + oldProviderId); authuserLookup.setAuthenticationProviderId(shibProviderId); String oldUserLookupIdentifier = authuserLookup.getPersistentUserId(); logger.info("this should be 'pete' or whatever the old builtin username was: " + oldUserLookupIdentifier); String perUserShibIdentifier = newUserIdentifierInLookupTable.getLookupStringPerAuthProvider(); authuserLookup.setPersistentUserId(perUserShibIdentifier); /** * @todo this should be a transaction of some kind. We want to update * the authenticateduserlookup and also delete the row from the * builtinuser table in a single transaction. */ em.persist(authuserLookup); String builtinUsername = builtInUserIdentifier.replaceFirst(AuthenticatedUser.IDENTIFIER_PREFIX, ""); BuiltinUser builtin = builtinUserServiceBean.findByUserName(builtinUsername); if (builtin != null) { // These were created by AuthenticationResponse.Status.BREAKOUT in ShibServiceBean.canLogInAsBuiltinUser List<PasswordResetData> oldTokens = passwordResetServiceBean.findPasswordResetDataByDataverseUser(builtin); for (PasswordResetData oldToken : oldTokens) { em.remove(oldToken); } em.remove(builtin); } else { logger.info("Couldn't delete builtin user because could find it based on username " + builtinUsername); } AuthenticatedUser shibUser = lookupUser(shibProviderId, perUserShibIdentifier); if (shibUser != null) { return shibUser; } return null; } /** * @param idOfAuthUserToConvert The id of the AuthenticatedUser (Shibboleth * user) to convert to a BuiltinUser. * @param newEmailAddress The new email address that will be used instead of * the user's old email address from the institution that they have left. * @return BuiltinUser * @throws java.lang.Exception You must catch and report back to the user (a * superuser) any Exceptions. */ public BuiltinUser convertShibToBuiltIn(Long idOfAuthUserToConvert, String newEmailAddress) throws Exception { AuthenticatedUser authenticatedUser = findByID(idOfAuthUserToConvert); if (authenticatedUser == null) { throw new Exception("User id " + idOfAuthUserToConvert + " not found."); } AuthenticatedUser existingUserWithSameEmail = getAuthenticatedUserByEmail(newEmailAddress); if (existingUserWithSameEmail != null) { throw new Exception("User id " + idOfAuthUserToConvert + " (" + authenticatedUser.getIdentifier() + ") cannot be converted from Shibboleth to BuiltIn because the email address " + newEmailAddress + " is already in use by user id " + existingUserWithSameEmail.getId() + " (" + existingUserWithSameEmail.getIdentifier() + ")."); } BuiltinUser builtinUser = new BuiltinUser(); builtinUser.setUserName(authenticatedUser.getUserIdentifier()); builtinUser.setFirstName(authenticatedUser.getFirstName()); builtinUser.setLastName(authenticatedUser.getLastName()); // Bean Validation will check for null and invalid email addresses builtinUser.setEmail(newEmailAddress); ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); Validator validator = factory.getValidator(); Set<ConstraintViolation<BuiltinUser>> violations = validator.validate(builtinUser); int numViolations = violations.size(); if (numViolations > 0) { StringBuilder logMsg = new StringBuilder(); for (ConstraintViolation<?> violation : violations) { logMsg.append(" Invalid value: <<<").append(violation.getInvalidValue()).append(">>> for ").append(violation.getPropertyPath()).append(" at ").append(violation.getLeafBean()).append(" - ").append(violation.getMessage()); } throw new Exception("User id " + idOfAuthUserToConvert + " cannot be converted from Shibboleth to BuiltIn because of constraint violations on the BuiltIn user that would be created: " + numViolations + ". Details: " + logMsg); } try { builtinUser = builtinUserServiceBean.save(builtinUser); } catch (IllegalArgumentException ex) { throw new Exception("User id " + idOfAuthUserToConvert + " cannot be converted from Shibboleth to BuiltIn because of an IllegalArgumentException creating the row in the builtinuser table: " + ex); } AuthenticatedUserLookup lookup = authenticatedUser.getAuthenticatedUserLookup(); if (lookup == null) { throw new Exception("User id " + idOfAuthUserToConvert + " does not have an 'authenticateduserlookup' row"); } String providerId = lookup.getAuthenticationProviderId(); if (providerId == null) { throw new Exception("User id " + idOfAuthUserToConvert + " provider id is null."); } String shibProviderId = ShibAuthenticationProvider.PROVIDER_ID; if (!providerId.equals(shibProviderId)) { throw new Exception("User id " + idOfAuthUserToConvert + " cannot be converted from Shibboleth to BuiltIn because current provider id is '" + providerId + "' rather than '" + shibProviderId + "'."); } lookup.setAuthenticationProviderId(BuiltinAuthenticationProvider.PROVIDER_ID); lookup.setPersistentUserId(authenticatedUser.getUserIdentifier()); em.persist(lookup); authenticatedUser.setEmail(newEmailAddress); em.persist(authenticatedUser); em.flush(); return builtinUser; } }