
com.helger.photon.security.user.UserManager Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of ph-oton-security Show documentation
Show all versions of ph-oton-security Show documentation
ph-oton security components
/**
* Copyright (C) 2014-2016 Philip Helger (www.helger.com)
* philip[at]helger[dot]com
*
* Licensed under the Apache 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://www.apache.org/licenses/LICENSE-2.0
*
* 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 com.helger.photon.security.user;
import java.util.Locale;
import java.util.Map;
import java.util.function.Predicate;
import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.annotation.concurrent.ThreadSafe;
import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.annotation.ReturnsMutableCopy;
import com.helger.commons.annotation.ReturnsMutableObject;
import com.helger.commons.callback.CallbackList;
import com.helger.commons.collection.ext.ICommonsList;
import com.helger.commons.state.EChange;
import com.helger.commons.string.StringHelper;
import com.helger.photon.basic.app.dao.IReloadableDAO;
import com.helger.photon.basic.app.dao.impl.AbstractMapBasedWALDAO;
import com.helger.photon.basic.app.dao.impl.DAOException;
import com.helger.photon.basic.audit.AuditHelper;
import com.helger.photon.security.CSecurity;
import com.helger.photon.security.object.ObjectHelper;
import com.helger.photon.security.password.GlobalPasswordSettings;
import com.helger.security.password.hash.PasswordHash;
import com.helger.security.password.salt.IPasswordSalt;
import com.helger.security.password.salt.PasswordSalt;
/**
* This class manages the available users.
*
* @author Philip Helger
*/
@ThreadSafe
public class UserManager extends AbstractMapBasedWALDAO implements IReloadableDAO
{
private final CallbackList m_aCallbacks = new CallbackList<> ();
public UserManager (@Nonnull @Nonempty final String sFilename) throws DAOException
{
super (User.class, sFilename);
}
public void reload () throws DAOException
{
m_aRWLock.writeLockedThrowing ( () -> {
internalRemoveAllItemsNoCallback ();
initialRead ();
});
}
public void createDefaults ()
{
// Create Administrator
if (!containsWithID (CSecurity.USER_ADMINISTRATOR_ID))
m_aRWLock.writeLocked ( () -> internalCreateItem (new User (CSecurity.USER_ADMINISTRATOR_ID,
CSecurity.USER_ADMINISTRATOR_LOGIN,
CSecurity.USER_ADMINISTRATOR_EMAIL,
GlobalPasswordSettings.createUserDefaultPasswordHash (new PasswordSalt (),
CSecurity.USER_ADMINISTRATOR_PASSWORD),
CSecurity.USER_ADMINISTRATOR_NAME,
(String) null,
(String) null,
(Locale) null,
(Map ) null,
false)));
// Create regular user
if (!containsWithID (CSecurity.USER_USER_ID))
m_aRWLock.writeLocked ( () -> internalCreateItem (new User (CSecurity.USER_USER_ID,
CSecurity.USER_USER_LOGIN,
CSecurity.USER_USER_EMAIL,
GlobalPasswordSettings.createUserDefaultPasswordHash (new PasswordSalt (),
CSecurity.USER_USER_PASSWORD),
CSecurity.USER_USER_NAME,
(String) null,
(String) null,
(Locale) null,
(Map ) null,
false)));
// Create guest user
if (!containsWithID (CSecurity.USER_GUEST_ID))
m_aRWLock.writeLocked ( () -> internalCreateItem (new User (CSecurity.USER_GUEST_ID,
CSecurity.USER_GUEST_LOGIN,
CSecurity.USER_GUEST_EMAIL,
GlobalPasswordSettings.createUserDefaultPasswordHash (new PasswordSalt (),
CSecurity.USER_GUEST_PASSWORD),
CSecurity.USER_GUEST_NAME,
(String) null,
(String) null,
(Locale) null,
(Map ) null,
false)));
}
/**
* @return The user callback list. Never null
.
*/
@Nonnull
@ReturnsMutableObject ("design")
public CallbackList getUserModificationCallbacks ()
{
return m_aCallbacks;
}
/**
* Create a new user.
*
* @param sLoginName
* Login name of the user. May neither be null
nor
* empty.This login name must be unique over all existing users.
* @param sEmailAddress
* The email address. May be null
.
* @param sPlainTextPassword
* The plain text password to be used. May neither be null
* nor empty.
* @param sFirstName
* The users first name. May be null
.
* @param sLastName
* The users last name. May be null
.
* @param sDescription
* Optional description for the user. May be null
.
* @param aDesiredLocale
* The users default locale. May be null
.
* @param aCustomAttrs
* Custom attributes. May be null
.
* @return The created user or null
if another user with the same
* email address is already present.
* @param bDisabled
* true
if the user is disabled
*/
@Nullable
public IUser createNewUser (@Nonnull @Nonempty final String sLoginName,
@Nullable final String sEmailAddress,
@Nonnull final String sPlainTextPassword,
@Nullable final String sFirstName,
@Nullable final String sLastName,
@Nullable final String sDescription,
@Nullable final Locale aDesiredLocale,
@Nullable final Map aCustomAttrs,
final boolean bDisabled)
{
ValueEnforcer.notEmpty (sLoginName, "LoginName");
ValueEnforcer.notNull (sPlainTextPassword, "PlainTextPassword");
if (getUserOfLoginName (sLoginName) != null)
{
// Another user with this login name already exists
AuditHelper.onAuditCreateFailure (User.OT, "login-name-already-in-use", sLoginName);
return null;
}
// Create user
final User aUser = new User (sLoginName,
sEmailAddress,
GlobalPasswordSettings.createUserDefaultPasswordHash (new PasswordSalt (),
sPlainTextPassword),
sFirstName,
sLastName,
sDescription,
aDesiredLocale,
aCustomAttrs,
bDisabled);
m_aRWLock.writeLocked ( () -> {
internalCreateItem (aUser);
});
AuditHelper.onAuditCreateSuccess (User.OT,
aUser.getID (),
sLoginName,
sEmailAddress,
sFirstName,
sLastName,
sDescription,
aDesiredLocale,
aCustomAttrs,
Boolean.valueOf (bDisabled));
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserCreated (aUser, false));
return aUser;
}
/**
* Create a predefined user.
*
* @param sID
* The ID to use
* @param sLoginName
* Login name of the user. May neither be null
nor empty.
* This login name must be unique over all existing users.
* @param sEmailAddress
* The email address. May be null
.
* @param sPlainTextPassword
* The plain text password to be used. May neither be null
* nor empty.
* @param sFirstName
* The users first name. May be null
.
* @param sLastName
* The users last name. May be null
.
* @param sDescription
* Optional description for the user. May be null
.
* @param aDesiredLocale
* The users default locale. May be null
.
* @param aCustomAttrs
* Custom attributes. May be null
.
* @return The created user or null
if another user with the same
* email address is already present.
* @param bDisabled
* true
if the user is disabled
*/
@Nullable
public IUser createPredefinedUser (@Nonnull @Nonempty final String sID,
@Nonnull @Nonempty final String sLoginName,
@Nullable final String sEmailAddress,
@Nonnull final String sPlainTextPassword,
@Nullable final String sFirstName,
@Nullable final String sLastName,
@Nullable final String sDescription,
@Nullable final Locale aDesiredLocale,
@Nullable final Map aCustomAttrs,
final boolean bDisabled)
{
ValueEnforcer.notEmpty (sLoginName, "LoginName");
ValueEnforcer.notNull (sPlainTextPassword, "PlainTextPassword");
if (getUserOfLoginName (sLoginName) != null)
{
// Another user with this login name already exists
AuditHelper.onAuditCreateFailure (User.OT, "login-name-already-in-use", sLoginName, "predefined-user");
return null;
}
// Create user
final User aUser = new User (sID,
sLoginName,
sEmailAddress,
GlobalPasswordSettings.createUserDefaultPasswordHash (new PasswordSalt (),
sPlainTextPassword),
sFirstName,
sLastName,
sDescription,
aDesiredLocale,
aCustomAttrs,
bDisabled);
m_aRWLock.writeLocked ( () -> {
internalCreateItem (aUser);
});
AuditHelper.onAuditCreateSuccess (User.OT,
aUser.getID (),
"predefined-user",
sLoginName,
sEmailAddress,
sFirstName,
sLastName,
sDescription,
aDesiredLocale,
aCustomAttrs,
Boolean.valueOf (bDisabled));
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserCreated (aUser, true));
return aUser;
}
/**
* Get the user with the specified ID.
*
* @param sUserID
* The user ID to resolve. May be null
.
* @return null
if no such user exists
*/
@Nullable
public IUser getUserOfID (@Nullable final String sUserID)
{
return getOfID (sUserID);
}
@Nullable
public IUser getActiveUserOfID (@Nullable final String sUserID)
{
final IUser aUser = getOfID (sUserID);
return aUser == null || aUser.isDeleted () || aUser.isDisabled () ? null : aUser;
}
/**
* Get the user with the specified login name
*
* @param sLoginName
* The login name to be checked. May be null
.
* @return null
if no such user exists
*/
@Nullable
public IUser getUserOfLoginName (@Nullable final String sLoginName)
{
if (StringHelper.hasNoText (sLoginName))
return null;
return findFirst (x -> x.getLoginName ().equals (sLoginName));
}
/**
* Get the user with the specified email address
*
* @param sEmailAddress
* The email address to be checked. May be null
.
* @return null
if no such user exists
*/
@Nullable
public IUser getUserOfEmailAddress (@Nullable final String sEmailAddress)
{
if (StringHelper.hasNoText (sEmailAddress))
return null;
return findFirst (x -> sEmailAddress.equals (x.getEmailAddress ()));
}
/**
* @return A non-null
collection of all contained users
* (enabled+disabled and deleted+not-deleted)
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsList extends IUser> getAllUsers ()
{
return getAll ();
}
@Nonnull
@ReturnsMutableCopy
public ICommonsList extends IUser> getAllUsers (@Nullable final Predicate super IUser> aFilter)
{
return getAll (aFilter);
}
@Nonnegative
public int getUserCount (@Nullable final Predicate super IUser> aFilter)
{
return getCount (aFilter);
}
/**
* @return A non-null
collection of all contained enabled and
* not-deleted users
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsList extends IUser> getAllActiveUsers ()
{
return getAll (x -> !x.isDeleted () && x.isEnabled ());
}
/**
* @return The number of all contained enabled and not-deleted users
*/
@Nonnegative
public int getActiveUserCount ()
{
return getCount (x -> !x.isDeleted () && x.isEnabled ());
}
/**
* @return A non-null
collection of all contained disabled and
* not-deleted users
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsList extends IUser> getAllDisabledUsers ()
{
return getAll (x -> !x.isDeleted () && x.isDisabled ());
}
/**
* @return A non-null
collection of all contained not deleted
* users
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsList extends IUser> getAllNotDeletedUsers ()
{
return getAll (x -> !x.isDeleted ());
}
/**
* @return A non-null
collection of all contained deleted users
*/
@Nonnull
@ReturnsMutableCopy
public ICommonsList extends IUser> getAllDeletedUsers ()
{
return getAll (x -> x.isDeleted ());
}
/**
* @return true
if any non-deleted, enabled user exists.
*/
public boolean containsAnyActiveUser ()
{
return containsAny (x -> !x.isDeleted () && x.isEnabled ());
}
/**
* Change the modifiable data of a user
*
* @param sUserID
* The ID of the user to be modified. May be null
.
* @param sNewLoginName
* The new login name. May not be null
.
* @param sNewEmailAddress
* The new email address. May be null
.
* @param sNewFirstName
* The new first name. May be null
.
* @param sNewLastName
* The new last name. May be null
.
* @param sNewDescription
* Optional new description for the user. May be null
.
* @param aNewDesiredLocale
* The new desired locale. May be null
.
* @param aNewCustomAttrs
* Custom attributes. May be null
.
* @param bNewDisabled
* true
if the user is disabled
* @return {@link EChange}
*/
@Nonnull
public EChange setUserData (@Nullable final String sUserID,
@Nonnull @Nonempty final String sNewLoginName,
@Nullable final String sNewEmailAddress,
@Nullable final String sNewFirstName,
@Nullable final String sNewLastName,
@Nullable final String sNewDescription,
@Nullable final Locale aNewDesiredLocale,
@Nullable final Map aNewCustomAttrs,
final boolean bNewDisabled)
{
// Resolve user
final User aUser = getOfID (sUserID);
if (aUser == null)
{
AuditHelper.onAuditModifyFailure (User.OT, sUserID, "no-such-user-id");
return EChange.UNCHANGED;
}
m_aRWLock.writeLock ().lock ();
try
{
EChange eChange = aUser.setLoginName (sNewLoginName);
eChange = eChange.or (aUser.setEmailAddress (sNewEmailAddress));
eChange = eChange.or (aUser.setFirstName (sNewFirstName));
eChange = eChange.or (aUser.setLastName (sNewLastName));
eChange = eChange.or (aUser.setDescription (sNewDescription));
eChange = eChange.or (aUser.setDesiredLocale (aNewDesiredLocale));
eChange = eChange.or (aUser.setDisabled (bNewDisabled));
eChange = eChange.or (aUser.getMutableAttributes ().clear ());
eChange = eChange.or (aUser.getMutableAttributes ().setAttributes (aNewCustomAttrs));
if (eChange.isUnchanged ())
return EChange.UNCHANGED;
ObjectHelper.setLastModificationNow (aUser);
internalUpdateItem (aUser);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
AuditHelper.onAuditModifySuccess (User.OT,
"all",
aUser.getID (),
sNewLoginName,
sNewEmailAddress,
sNewFirstName,
sNewLastName,
sNewDescription,
aNewDesiredLocale,
aNewCustomAttrs,
Boolean.valueOf (bNewDisabled));
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserUpdated (aUser));
return EChange.CHANGED;
}
/**
* Change the modifiable data of a user
*
* @param sUserID
* The ID of the user to be modified. May be null
.
* @param sNewPlainTextPassword
* The new password in plain text. May not be null
.
* @return {@link EChange}
*/
@Nonnull
public EChange setUserPassword (@Nullable final String sUserID, @Nonnull final String sNewPlainTextPassword)
{
// Resolve user
final User aUser = getOfID (sUserID);
if (aUser == null)
{
AuditHelper.onAuditModifyFailure (User.OT, sUserID, "no-such-user-id", "password");
return EChange.UNCHANGED;
}
// Create a new password salt upon password change
final PasswordHash aPasswordHash = GlobalPasswordSettings.createUserDefaultPasswordHash (new PasswordSalt (),
sNewPlainTextPassword);
m_aRWLock.writeLock ().lock ();
try
{
final EChange eChange = aUser.setPasswordHash (aPasswordHash);
if (eChange.isUnchanged ())
return EChange.UNCHANGED;
ObjectHelper.setLastModificationNow (aUser);
internalUpdateItem (aUser);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
AuditHelper.onAuditModifySuccess (User.OT, "password", sUserID);
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserPasswordChanged (aUser));
return EChange.CHANGED;
}
@Nonnull
public EChange updateUserLastLogin (@Nullable final String sUserID)
{
// Resolve user
final User aUser = getOfID (sUserID);
if (aUser == null)
{
AuditHelper.onAuditModifyFailure (User.OT, sUserID, "no-such-user-id", "update-last-login");
return EChange.UNCHANGED;
}
m_aRWLock.writeLock ().lock ();
try
{
aUser.onSuccessfulLogin ();
internalUpdateItem (aUser);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
AuditHelper.onAuditModifySuccess (User.OT, "update-last-login", sUserID);
return EChange.CHANGED;
}
@Nonnull
public EChange updateUserLastFailedLogin (@Nullable final String sUserID)
{
// Resolve user
final User aUser = getOfID (sUserID);
if (aUser == null)
{
AuditHelper.onAuditModifyFailure (User.OT, sUserID, "no-such-user-id", "update-last-failed-login");
return EChange.UNCHANGED;
}
m_aRWLock.writeLock ().lock ();
try
{
aUser.onFailedLogin ();
internalUpdateItem (aUser);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
AuditHelper.onAuditModifySuccess (User.OT, "update-last-failed-login", sUserID);
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserLastFailedLoginUpdated (aUser));
return EChange.CHANGED;
}
/**
* Delete the user with the specified ID.
*
* @param sUserID
* The ID of the user to delete
* @return {@link EChange#CHANGED} if the user was deleted,
* {@link EChange#UNCHANGED} otherwise.
*/
@Nonnull
public EChange deleteUser (@Nullable final String sUserID)
{
final User aUser = getOfID (sUserID);
if (aUser == null)
{
AuditHelper.onAuditDeleteFailure (User.OT, "no-such-user-id", sUserID);
return EChange.UNCHANGED;
}
m_aRWLock.writeLock ().lock ();
try
{
if (ObjectHelper.setDeletionNow (aUser).isUnchanged ())
{
AuditHelper.onAuditDeleteFailure (User.OT, "already-deleted", sUserID);
return EChange.UNCHANGED;
}
internalMarkItemDeleted (aUser);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
AuditHelper.onAuditDeleteSuccess (User.OT, sUserID);
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserDeleted (aUser));
return EChange.CHANGED;
}
/**
* Undelete the user with the specified ID.
*
* @param sUserID
* The ID of the user to undelete
* @return {@link EChange#CHANGED} if the user was undeleted,
* {@link EChange#UNCHANGED} otherwise.
*/
@Nonnull
public EChange undeleteUser (@Nullable final String sUserID)
{
final User aUser = getOfID (sUserID);
if (aUser == null)
{
AuditHelper.onAuditUndeleteFailure (User.OT, sUserID, "no-such-user-id");
return EChange.UNCHANGED;
}
m_aRWLock.writeLock ().lock ();
try
{
if (ObjectHelper.setUndeletionNow (aUser).isUnchanged ())
return EChange.UNCHANGED;
internalMarkItemUndeleted (aUser);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
AuditHelper.onAuditUndeleteSuccess (User.OT, sUserID);
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserUndeleted (aUser));
return EChange.CHANGED;
}
/**
* disable the user with the specified ID.
*
* @param sUserID
* The ID of the user to disable
* @return {@link EChange#CHANGED} if the user was disabled,
* {@link EChange#UNCHANGED} otherwise.
*/
@Nonnull
public EChange disableUser (@Nullable final String sUserID)
{
final User aUser = getOfID (sUserID);
if (aUser == null)
{
AuditHelper.onAuditModifyFailure (User.OT, sUserID, "no-such-user-id", "disable");
return EChange.UNCHANGED;
}
m_aRWLock.writeLock ().lock ();
try
{
if (aUser.setDisabled (true).isUnchanged ())
return EChange.UNCHANGED;
internalUpdateItem (aUser);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
AuditHelper.onAuditModifySuccess (User.OT, "disable", sUserID);
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserEnabled (aUser, false));
return EChange.CHANGED;
}
/**
* Enable the user with the specified ID.
*
* @param sUserID
* The ID of the user to enable
* @return {@link EChange#CHANGED} if the user was enabled,
* {@link EChange#UNCHANGED} otherwise.
*/
@Nonnull
public EChange enableUser (@Nullable final String sUserID)
{
final User aUser = getOfID (sUserID);
if (aUser == null)
{
AuditHelper.onAuditModifyFailure (User.OT, sUserID, "no-such-user-id", "enable");
return EChange.UNCHANGED;
}
m_aRWLock.writeLock ().lock ();
try
{
if (aUser.setDisabled (false).isUnchanged ())
return EChange.UNCHANGED;
internalUpdateItem (aUser);
}
finally
{
m_aRWLock.writeLock ().unlock ();
}
AuditHelper.onAuditModifySuccess (User.OT, "enable", sUserID);
// Execute callback as the very last action
m_aCallbacks.forEach (aCB -> aCB.onUserEnabled (aUser, true));
return EChange.CHANGED;
}
/**
* Check if the passed combination of user ID and password matches.
*
* @param sUserID
* The ID of the user
* @param sPlainTextPassword
* The plan text password to validate.
* @return true
if the password hash matches the stored hash for
* the specified user, false
otherwise.
*/
public boolean areUserIDAndPasswordValid (@Nullable final String sUserID, @Nullable final String sPlainTextPassword)
{
// No password is not allowed
if (sPlainTextPassword == null)
return false;
// Is there such a user?
final IUser aUser = getOfID (sUserID);
if (aUser == null)
return false;
// Now compare the hashes
final String sPasswordHashAlgorithm = aUser.getPasswordHash ().getAlgorithmName ();
final IPasswordSalt aSalt = aUser.getPasswordHash ().getSalt ();
final PasswordHash aPasswordHash = GlobalPasswordSettings.createUserPasswordHash (sPasswordHashAlgorithm,
aSalt,
sPlainTextPassword);
return aUser.getPasswordHash ().equals (aPasswordHash);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy