All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.jgcomptech.tools.authc.UserManager Maven / Gradle / Ivy

There is a newer version: 1.5.1
Show newest version
package com.jgcomptech.tools.authc;

import com.jgcomptech.tools.SecurityTools;
import com.jgcomptech.tools.databasetools.jdbc.DataTypes;
import com.jgcomptech.tools.databasetools.jdbc.Database;
import com.jgcomptech.tools.databasetools.jdbc.TableNotFoundException;
import com.jgcomptech.tools.databasetools.jdbc.TypedStatement;
import com.jgcomptech.tools.databasetools.jdbc.builders.ColumnBuilder;
import com.jgcomptech.tools.databasetools.jdbc.builders.QueryBuilder;
import org.jetbrains.annotations.Contract;

import java.sql.SQLException;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Locale;

/**
 * Manages all user accounts in the database.
 * @since 1.4.0
 */
public final class UserManager {
    private static final String ID_FIELD = "Id";
    private static final String USERNAME_FIELD = "Username";
    private static final String PASSWORD_FIELD = "Password";
    private static final String SALT_FIELD = "Salt";
    private static final String TYPE_FIELD = "Type";
    private static final String ACCOUNT_CREATION_DATE_FIELD = "Account_Creation_Date";
    private static final String PASSWORD_EXPIRATION_DATE_FIELD = "Password_Expiration_Date";
    private static final String PASSWORD_SET_TO_EXPIRE_FIELD = "Password_Set_To_Expire";
    private static final String ACCOUNT_LOCKED_FIELD = "Account_Locked";
    private static final String TABLE_NAME = "Users";
    private static final String INDEX_NAME = "login_index";
    private final Database db;
    private SessionManager sessionManager;
    private final UserRoleManager userRoleManager = UserRoleManager.getInstance();
    private String appIconPath;
    private String programName;

    /**
     * Creates a new instance of the User Manager.
     * @param db the database to use
     * @throws IllegalArgumentException if db is null
     * @throws UserManagerException if an error occurs while creating the users table
     */
    public UserManager(final Database db) { this(db, null); }

    /**
     * Creates a new instance of the User Manager.
     * @param db the database to use
     * @param appIconPath the path to the icon to use for all dialogs
     * @throws IllegalArgumentException if db is null
     * @throws UserManagerException if an error occurs while creating the users table
     */
    public UserManager(final Database db, final String appIconPath) { this(db, appIconPath, null); }

    /**
     * Creates a new instance of the User Manager.
     * @param db the database to use
     * @param appIconPath the path to the icon to use for all dialogs
     * @param programName the program name to use for all dialogs
     * @throws IllegalArgumentException if db is null
     * @throws UserManagerException if an error occurs while creating the users table
     */
    public UserManager(final Database db, final String appIconPath, final String programName) {
        this.db = db;
        this.appIconPath = appIconPath;
        this.programName = programName;

        if(db == null) throw new IllegalArgumentException("Database Cannot Be Null!");

        try {
            if(!db.getInfo().tableExists(TABLE_NAME)) {
                //The password field is 150 chars in length due to the length of a SHA-512 hash
                TypedStatement.newTable()
                        .CREATE(TABLE_NAME, db)
                        .addColumn(new ColumnBuilder(ID_FIELD, DataTypes.INTEGER).notNull().primaryKey().autoIncrement())
                        .addColumn(new ColumnBuilder(USERNAME_FIELD, DataTypes.NVARCHAR, 100).notNull())
                        .addColumn(new ColumnBuilder(PASSWORD_FIELD, DataTypes.NVARCHAR, 150).notNull())
                        .addColumn(new ColumnBuilder(SALT_FIELD, DataTypes.NVARCHAR, 100).notNull())
                        .addColumn(new ColumnBuilder(TYPE_FIELD, DataTypes.NVARCHAR, 100).notNull())
                        .addColumn(new ColumnBuilder(ACCOUNT_CREATION_DATE_FIELD, DataTypes.DATETIME).notNull())
                        .addColumn(new ColumnBuilder(ACCOUNT_LOCKED_FIELD, DataTypes.BOOLEAN).notNull())
                        .addColumn(new ColumnBuilder(PASSWORD_SET_TO_EXPIRE_FIELD, DataTypes.BOOLEAN).notNull())
                        .addColumn(new ColumnBuilder(PASSWORD_EXPIRATION_DATE_FIELD, DataTypes.DATETIME))
                        .buildAndCreate();
            }

            TypedStatement.newIndex()
                    .CREATE_UNIQUE(INDEX_NAME, TABLE_NAME, db)
                    .addColumn(USERNAME_FIELD, PASSWORD_FIELD, SALT_FIELD)
                    .buildAndCreate();
        } catch (final SQLException e) { throw new UserManagerException(e); }
    }

    /**
     * Creates a new user in the database using SHA-512 password hashing.
     * @param username the username to add
     * @param password the password for the new user
     * @param userRole the system user role for the new user
     * @return true if user creation is successful
     * @throws IllegalArgumentException if values are null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while creating the user
     * @throws PasswordHashingFailedException if an error occurs while hashing the password
     */
    public boolean createUser(final String username,
                              final String password,
                              final UserRoleManager.SystemUserRoles userRole) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        if(password == null) {
            throw new IllegalArgumentException("Password Cannot Be Null!");
        }
        if(userRole == null) {
            throw new IllegalArgumentException("User Role Cannot Be Null!");
        }
        return createUser(username, password, userRole.getName());
    }

    /**
     * Creates a new user in the database using SHA-512 password hashing.
     * @param username the username to add
     * @param password the password for the new user
     * @param userRole the name of the user role for the new user
     * @return true if user creation is successful
     * @throws IllegalArgumentException if values are null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while creating the user
     * @throws PasswordHashingFailedException if an error occurs while hashing the password
     */
    public boolean createUser(final String username, final String password, final String userRole) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        if(password == null) {
            throw new IllegalArgumentException("Password Cannot Be Null!");
        }
        if(userRole == null || userRole.trim().isEmpty()) {
            throw new IllegalArgumentException("User Role Cannot Be Empty!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                if(!userExists(username.toLowerCase(Locale.ENGLISH))) {
                    final var salt = SecurityTools.PasswordHashes.createBCryptSaltString(12);
                    final var hashedPassword = SecurityTools.PasswordHashes.createBCryptHash(password, salt);
                    return TypedStatement.newInsert()
                            .INSERT_INTO(TABLE_NAME, USERNAME_FIELD, PASSWORD_FIELD, SALT_FIELD, TYPE_FIELD,
                                    ACCOUNT_CREATION_DATE_FIELD, PASSWORD_SET_TO_EXPIRE_FIELD,
                                    ACCOUNT_LOCKED_FIELD, PASSWORD_EXPIRATION_DATE_FIELD)
                            .VALUES(username.toLowerCase(Locale.ENGLISH), hashedPassword, salt, userRole,
                                    LocalDateTime.now().toString(), "false", "false",
                                    LocalDateTime.now().plusYears(1000).toString())
                            .buildAndInsert(db) != 0;
                }
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
        return false;
    }

    /**
     * Deletes the specified user.
     * @param username the username of the user to delete
     * @return true if no errors occur
     * @throws IllegalArgumentException if username is null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while deleting user
     */
    public boolean deleteUser(final String username) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                return TypedStatement.newDelete()
                        .DELETE_FROM(TABLE_NAME)
                        .WHERE(USERNAME_FIELD, username)
                        .buildAndDelete(db) == 1;
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
    }

    /**
     * Returns a UserAccount object representing the specified username.
     * @param username the username to lookup
     * @return a UserAccount object representing the specified username
     * @throws IllegalArgumentException if username is null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs during lookup
     */
    public UserAccount getUser(final String username) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        final UserAccount account;
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                if (userExists(username)) {
                    final var statement = new QueryBuilder()
                            .SELECT(USERNAME_FIELD, PASSWORD_FIELD, TYPE_FIELD, SALT_FIELD,
                                    ACCOUNT_CREATION_DATE_FIELD, ACCOUNT_LOCKED_FIELD,
                                    PASSWORD_SET_TO_EXPIRE_FIELD, PASSWORD_EXPIRATION_DATE_FIELD)
                            .FROM(TABLE_NAME)
                            .WHERE(USERNAME_FIELD, username)
                            .build(db);

                    try (final var rs = statement.executeQuery()) {
                        rs.next();
                        account = new UserAccount(rs.getString(USERNAME_FIELD),
                                rs.getTimestamp(ACCOUNT_CREATION_DATE_FIELD).toLocalDateTime(),
                                rs.getBoolean(ACCOUNT_LOCKED_FIELD), rs.getBoolean(PASSWORD_SET_TO_EXPIRE_FIELD),
                                rs.getTimestamp(PASSWORD_EXPIRATION_DATE_FIELD).toLocalDateTime());
                    }
                } else return null;
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
        return account;
    }

    /**
     * Checks if the specified username exists in the database.
     * @param username the username to check
     * @return true if the user exists
     * @throws IllegalArgumentException if username is null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs during lookup
     */
    public boolean userExists(final String username) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                try (final var rs = TypedStatement.newQuery().SELECT_ALL().FROM(TABLE_NAME).buildAndExecute(db)) {
                    while(rs.next()) {
                        if (rs.getString(USERNAME_FIELD).toLowerCase().equals(username)) return true;
                    }
                }
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) { throw new UserManagerException(e); }
        return false;
    }

    /**
     * Returns userType for the specified username.
     * @param username the username to lookup
     * @return the userType
     * @throws IllegalArgumentException if username is null or empty or if username does not exist
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if error occurs during lookup
     */
    public UserRole getUserRole(final String username) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                if(userExists(username)) {
                    final var statement = new QueryBuilder()
                            .SELECT(TYPE_FIELD)
                            .FROM(TABLE_NAME)
                            .WHERE(USERNAME_FIELD, username)
                            .build(db);

                    try(final var rs = statement.executeQuery()) {
                        rs.next();
                        return userRoleManager.getUserRole(rs.getString(TYPE_FIELD).toLowerCase(Locale.ENGLISH));
                    }
                } else throw new IllegalArgumentException('"' + username + "\" Not Found!");
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) { throw new UserManagerException(e); }
    }

    /**
     * Sets the user type of the specified user.
     * @param username the username of the user to update
     * @param userRole the system user role to change to
     * @return true if no errors occurred
     * @throws IllegalArgumentException if values are null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while changing user type
     */
    public boolean setUserRole(final String username,
                               final UserRoleManager.SystemUserRoles userRole) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        if(userRole == null) {
            throw new IllegalArgumentException("User Role Cannot Be Null!");
        }
        return setUserRole(username, userRole.getName());
    }

    /**
     * Sets the user type of the specified user.
     * @param username the username of the user to update
     * @param userRole the name of the user role to change to
     * @return true if no errors occurred
     * @throws IllegalArgumentException if values are null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while changing user type
     */
    public boolean setUserRole(final String username, final String userRole) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        if(userRole == null || userRole.trim().isEmpty()) {
            throw new IllegalArgumentException("User Role Cannot Be Empty!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                return TypedStatement.newUpdate()
                        .UPDATE(TABLE_NAME)
                        .SET(TYPE_FIELD, userRole)
                        .WHERE(USERNAME_FIELD, username)
                        .buildAndUpdate(db) != 0;
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
    }

    /**
     * Sets a new password for an existing user using SHA-512 password hashing.
     * @param username the username to change
     * @param password the new password
     * @return true if password is changed successfully
     * @throws IllegalArgumentException if values are null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while changing password
     * @throws PasswordHashingFailedException if an error occurs while hashing the password
     */
    public boolean setPassword(final String username, final String password) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        if(password == null) {
            throw new IllegalArgumentException("Password Cannot Be Null!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                final var salt = SecurityTools.PasswordHashes.createBCryptSaltString(12);
                final var hashedPassword = SecurityTools.PasswordHashes.createBCryptHash(password, salt);
                return TypedStatement.newUpdate()
                        .UPDATE(TABLE_NAME)
                        .SET(PASSWORD_FIELD, hashedPassword)
                        .SET(SALT_FIELD, salt)
                        .WHERE(USERNAME_FIELD, username)
                        .buildAndUpdate(db) != 0;
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
    }

    /**
     * Sets the lock status for the specified user.
     * @param username the username to change
     * @param status the status to set
     * @return true if the status is changed successfully
     * @throws IllegalArgumentException if values are null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while changing password
     * @throws PasswordHashingFailedException if an error occurs while hashing the password
     * @since 1.5.0
     */
    public boolean setLockStatus(final String username, final Boolean status) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                return TypedStatement.newUpdate()
                        .UPDATE(TABLE_NAME)
                        .SET(ACCOUNT_LOCKED_FIELD, status.toString())
                        .WHERE(USERNAME_FIELD, username)
                        .buildAndUpdate(db) != 0;
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
    }

    /**
     * Disables the specified user's password expiration.
     * @param username the username to change
     * @return true if password expiration disabled successfully
     * @throws IllegalArgumentException if the username is null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while changing password
     * @throws PasswordHashingFailedException if an error occurs while hashing the password
     * @since 1.5.0
     */
    public boolean disablePasswordExpiration(final String username) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                return TypedStatement.newUpdate()
                        .UPDATE(TABLE_NAME)
                        .SET(PASSWORD_SET_TO_EXPIRE_FIELD, "false")
                        .SET(PASSWORD_EXPIRATION_DATE_FIELD, LocalDateTime.now().plusYears(1000).toString())
                        .WHERE(USERNAME_FIELD, username)
                        .buildAndUpdate(db) != 0;
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
    }

    /**
     * Sets the specified user's password expiration date.
     * @param username the username to change
     * @param date the date to set
     * @return true if the date is changed successfully
     * @throws IllegalArgumentException if values are null or empty
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs while changing password
     * @throws PasswordHashingFailedException if an error occurs while hashing the password
     * @since 1.5.0
     */
    public boolean setPasswordExpirationDate(final String username, final LocalDateTime date) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                return TypedStatement.newUpdate()
                        .UPDATE(TABLE_NAME)
                        .SET(PASSWORD_SET_TO_EXPIRE_FIELD, "true")
                        .SET(PASSWORD_EXPIRATION_DATE_FIELD, date.toString())
                        .WHERE(USERNAME_FIELD, username)
                        .buildAndUpdate(db) != 0;
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
    }

    /**
     * Checks to see if the specified password matches the stored password in the database.
     * @param username the username to check against
     * @param password the password to check against
     * @return true if the passwords match
     * @throws IllegalArgumentException if values are null or empty or if username does not exist
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs during lookup
     * @throws PasswordHashingFailedException if an error occurs while hashing the password
     */
    public boolean checkPasswordMatches(final String username, final String password) {
        if(username == null || username.trim().isEmpty()) {
            throw new IllegalArgumentException("Username Cannot Be Empty!");
        }
        if(password == null) {
            throw new IllegalArgumentException("Password Cannot Be Null!");
        }
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                if(userExists(username)) {
                    final var statement = new QueryBuilder()
                            .SELECT(SALT_FIELD, PASSWORD_FIELD)
                            .FROM(TABLE_NAME)
                            .WHERE(USERNAME_FIELD, username)
                            .build(db);

                    final String salt;
                    final String databasePass;
                    try(final var rs = statement.executeQuery()) {
                        rs.next();
                        salt = rs.getString(SALT_FIELD);
                        databasePass = rs.getString(PASSWORD_FIELD);
                    }
                    return SecurityTools.PasswordHashes.checkBCryptHashesMatch(password, databasePass, salt);
                } else throw new IllegalArgumentException('"' + username + "\" Not Found!");
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
    }

    /**
     * Returns a list of user accounts.
     * @return a HashSet of UserAccount objects representing the users in the users table
     * @throws TableNotFoundException if users table is missing
     * @throws UserManagerException if an error occurs during lookup
     */
    public HashSet getUsersList() {
        final HashSet accounts = new HashSet<>();
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                final var statement = new QueryBuilder()
                        .SELECT(USERNAME_FIELD, PASSWORD_FIELD, TYPE_FIELD, SALT_FIELD,
                                ACCOUNT_CREATION_DATE_FIELD, ACCOUNT_LOCKED_FIELD,
                                PASSWORD_SET_TO_EXPIRE_FIELD, PASSWORD_EXPIRATION_DATE_FIELD)
                        .FROM(TABLE_NAME)
                        .build(db);

                try (final var rs = statement.executeQuery()) {
                    while(rs.next()) {
                        final var account = new UserAccount(rs.getString(USERNAME_FIELD),
                                rs.getTimestamp(ACCOUNT_CREATION_DATE_FIELD).toLocalDateTime(),
                                rs.getBoolean(ACCOUNT_LOCKED_FIELD), rs.getBoolean(PASSWORD_SET_TO_EXPIRE_FIELD),
                                rs.getTimestamp(PASSWORD_EXPIRATION_DATE_FIELD).toLocalDateTime());
                        accounts.add(account);
                    }
                }
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) { throw new UserManagerException(e); }
        return accounts;
    }

    /**
     * Returns a list of the user names in the database.
     * @return a list of the user names in the database
     * @throws TableNotFoundException if users table is missing
     */
    public HashSet getUsernameList() {
        final HashSet accounts = new HashSet<>();
        try {
            if(db.getInfo().tableExists(TABLE_NAME)) {
                final var statement = new QueryBuilder()
                        .SELECT(USERNAME_FIELD, PASSWORD_FIELD, TYPE_FIELD)
                        .FROM(TABLE_NAME)
                        .build(db);

                try (final var rs = statement.executeQuery()) {
                    while(rs.next()) {
                        accounts.add(rs.getString(USERNAME_FIELD));
                    }
                }
            } else throw new TableNotFoundException(TABLE_NAME);
        } catch (final SQLException e) {
            throw new UserManagerException(e);
        }
        return accounts;
    }

    /**
     * Returns an instance of the SessionManager for login use.
     * @return an instance of the SessionManager
     */
    public SessionManager getSessionManager() {
        if(sessionManager == null) sessionManager = new SessionManager(this, appIconPath, programName);
        return sessionManager;
    }

    /**
     * Returns the path to the icon to use for all dialogs.
     * @return the path to the icon to use for all dialogs
     */
    public String getAppIconPath() { return appIconPath; }

    /**
     * Sets the path to the icon to use for all dialogs.
     * @param appIconPath the path to set
     */
    public void setAppIconPath(final String appIconPath) { this.appIconPath = appIconPath; }

    /**
     * Returns the program name to use for all dialogs.
     * @return the program name to use for all dialogs
     */
    public String getProgramName() { return programName; }

    /**
     * Sets the program name to use for all dialogs.
     * @param programName the program name to set
     */
    public void setProgramName(final String programName) { this.programName = programName; }

    @Contract(" -> fail")
    @Override public Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy