psecio/gatekeeper

View on GitHub
src/Psecio/Gatekeeper/UserModel.php

Summary

Maintainability
D
2 days
Test Coverage
<?php

namespace Psecio\Gatekeeper;

/**
 * User class
 *
 * @property string $username
 * @property string $password
 * @property string $email
 * @property string $firstName
 * @property string $lastName
 * @property string $created
 * @property string $updated
 * @property string $status
 * @property string $id
 * @property string $resetCode
 * @property string $resetCodeTimeout
 * @property string $lastLogin
 */
class UserModel extends \Psecio\Gatekeeper\Model\Mysql
{
    const STATUS_ACTIVE = 'active';
    const STATUS_INACTIVE = 'inactive';

    /**
     * Database table name
     * @var string
     */
    protected $tableName = 'users';

    /**
     * Model properties
     * @var array
     */
    protected $properties = array(
        'username' => array(
            'description' => 'Username',
            'column' => 'username',
            'type' => 'varchar'
        ),
        'password' => array(
            'description' => 'Password',
            'column' => 'password',
            'type' => 'varchar'
        ),
        'email' => array(
            'description' => 'Email Address',
            'column' => 'email',
            'type' => 'varchar'
        ),
        'firstName' => array(
            'description' => 'First Name',
            'column' => 'first_name',
            'type' => 'varchar'
        ),
        'lastName' => array(
            'description' => 'Last Name',
            'column' => 'last_name',
            'type' => 'varchar'
        ),
        'created' => array(
            'description' => 'Date Created',
            'column' => 'created',
            'type' => 'datetime'
        ),
        'updated' => array(
            'description' => 'Date Updated',
            'column' => 'updated',
            'type' => 'datetime'
        ),
        'status' => array(
            'description' => 'Status',
            'column' => 'status',
            'type' => 'varchar'
        ),
        'id' => array(
            'description' => 'User ID',
            'column' => 'id',
            'type' => 'integer'
        ),
        'resetCode' => array(
            'description' => 'Password Reset Code',
            'column' => 'password_reset_code',
            'type' => 'varchar'
        ),
        'resetCodeTimeout' => array(
            'description' => 'Password Reset Code Timeout',
            'column' => 'password_reset_code_timeout',
            'type' => 'datetime'
        ),
        'lastLogin' => array(
            'description' => 'Date and Time of Last Login',
            'column' => 'last_login',
            'type' => 'datetime'
        ),
        'groups' => array(
            'description' => 'Groups the User Belongs to',
            'type' => 'relation',
            'relation' => array(
                'model' => '\\Psecio\\Gatekeeper\\UserGroupCollection',
                'method' => 'findByUserId',
                'local' => 'id'
            )
        ),
        'permissions' => array(
            'description' => 'Permissions the user has',
            'type' => 'relation',
            'relation' => array(
                'model' => '\\Psecio\\Gatekeeper\\UserPermissionCollection',
                'method' => 'findByUserId',
                'local' => 'id'
            )
        ),
        'loginAttempts' => array(
            'description' => 'Number of login attempts by user',
            'type' => 'relation',
            'relation' => array(
                'model' => '\\Psecio\\Gatekeeper\\UserModel',
                'method' => 'findAttemptsByUser',
                'local' => 'id',
                'return' => 'value'
            )
        ),
        'throttle' => array(
            'description' => 'Full throttle information for a user',
            'type' => 'relation',
            'relation' => array(
                'model' => '\\Psecio\\Gatekeeper\\ThrottleModel',
                'method' => 'findByUserId',
                'local' => 'id'
            )
        ),
        'securityQuestions' => array(
            'description' => 'Security questions for the user',
            'type' => 'relation',
            'relation' => array(
                'model' => '\\Psecio\\Gatekeeper\\SecurityQuestionCollection',
                'method' => 'findByUserId',
                'local' => 'id'
            )
        ),
        'authTokens' => array(
            'description' => 'Current auth (remember me) tokens for the user',
            'type' => 'relation',
            'relation' => array(
                'model' => '\\Psecio\\Gatekeeper\\AuthTokenCollection',
                'method' => 'findTokensByUserId',
                'local' => 'id'
            )
        )
    );

    /**
     * Check to see if the password needs to be rehashed
     *
     * @param string $value Password string
     * @return string Updated string value
     */
    public function prePassword($value)
    {
        if (password_needs_rehash($value, PASSWORD_DEFAULT) === true) {
            $value = password_hash($value, PASSWORD_DEFAULT);
        }
        return $value;
    }

    /**
     * Find the user by username
     *     If found, user is automatically loaded into model instance
     *
     * @param string $username Username
     * @return boolean Success/fail of find operation
     */
    public function findByUsername($username)
    {
        return $this->getDb()->find(
            $this, array('username' => $username)
        );
    }

    /**
     * Find a user by their given ID
     *
     * @param integer $userId User ID
     * @return boolean Success/fail of find operation
     */
    public function findByUserId($userId)
    {
        return $this->getDb()->find(
            $this, array('id' => $userId)
        );
    }

    /**
     * Attach a permission to a user account
     *
     * @param integer|PermissionModel $perm Permission ID or model isntance
     * @param integer $expire Expiration time of the permission relationship
     */
    public function addPermission($perm, $expire = null)
    {
        if ($perm instanceof PermissionModel) {
            $perm = $perm->id;
        }
        $data = [
            'user_id' => $this->id,
            'permission_id' => $perm
        ];
        if ($expire !== null && is_int($expire)) {
            $data['expire'] = $expire;
        }
        $perm = new UserPermissionModel($this->getDb(), $data);
        return $this->getDb()->save($perm);
    }

    /**
     * Revoke a user permission
     *
     * @param integer|PermissionModel $perm Permission ID or model instance
     * @return boolean Success/fail of delete
     */
    public function revokePermission($perm)
    {
        if ($perm instanceof PermissionModel) {
            $perm = $perm->id;
        }
        $perm = new UserPermissionModel($this->getDb(), array(
            'user_id' => $this->id,
            'permission_id' => $perm
        ));
        return $this->getDb()->delete($perm);
    }

    /**
     * Add a group to the user
     *
     * @param integer|GroupModel $group Add the user to a group
     * @return boolean Success/fail of add
     */
    public function addGroup($group, $expire = null)
    {
        if ($group instanceof GroupModel) {
            $group = $group->id;
        }
        $data = [
            'group_id' => $group,
            'user_id' => $this->id
        ];
        if ($expire !== null && is_int($expire)) {
            $data['expire'] = $expire;
        }
        $group = new UserGroupModel($this->getDb(), $data);
        return $this->getDb()->save($group);
    }

    /**
     * Revoke access to a group for a user
     *
     * @param integer|GroupModel $group ID or model of group to remove
     * @return boolean
     */
    public function revokeGroup($group)
    {
        if ($group instanceof GroupModel) {
            $group = $group->id;
        }
        $group = new UserGroupModel($this->getDb(), array(
            'group_id' => $group,
            'user_id' => $this->id
        ));
        return $this->getDb()->delete($group);
    }

    /**
     * Activate the user (status)
     *
     * @return boolean Success/fail of activation
     */
    public function activate()
    {
        // Verify we have a user
        if ($this->id === null) {
            return false;
        }
        $this->status = self::STATUS_ACTIVE;
        return $this->getDb()->save($this);
    }

    /**
     * Deactivate the user
     *
     * @return boolean Success/fail of deactivation
     */
    public function deactivate()
    {
        // Verify we have a user
        if ($this->id === null) {
            return false;
        }
        $this->status = self::STATUS_INACTIVE;
        return $this->getDb()->save($this);
    }

    /**
     * Generate and return the code for a password reset
     *     Also updates the user record
     *
     * @param integer $length Length of returned string
     * @return string Geenrated code
     */
    public function getResetPasswordCode($length = 80)
    {
        // Verify we have a user
        if ($this->id === null) {
            return false;
        }
        // Generate a random-ish code and save it to the user record
        $code = substr(bin2hex(openssl_random_pseudo_bytes($length)), 0, $length);
        $this->resetCode = $code;
        $this->resetCodeTimeout = date('Y-m-d H:i:s', strtotime('+1 hour'));
        $this->getDb()->save($this);

        return $code;
    }

    /**
     * Check the given code against he value in the database
     *
     * @param string $resetCode Reset code to verify
     * @return boolean Pass/fail of verification
     */
    public function checkResetPasswordCode($resetCode)
    {
        // Verify we have a user
        if ($this->id === null) {
            return false;
        }
        if ($this->resetCode === null) {
            throw new Exception\PasswordResetInvalid('No reset code defined for user '.$this->username);
        }

        // Verify the timeout
        $timeout = new \DateTime($this->resetCodeTimeout);
        if ($timeout <= new \DateTime()) {
            $this->clearPasswordResetCode();
            throw new Exception\PasswordResetTimeout('Reset code has timeed out!');
        }

        // We made it this far, compare the hashes
        $result = (Gatekeeper::hash_equals($this->resetCode, $resetCode));
        if ($result === true) {
            $this->clearPasswordResetCode();
        }
        return $result;
    }

    /**
     * Clear all data from the passsword reset code handling
     * @return [type] [description]
     */
    public function clearPasswordResetCode()
    {
        // Verify we have a user
        if ($this->id === null) {
            return false;
        }
        $this->resetCode = null;
        $this->resetCodeTimeout = null;
        return $this->getDb()->save($this);
    }

    /**
     * Check to see if the user is in the group
     *
     * @param integer $groupId Group ID or name
     * @return boolean Found/not found in the group
     */
    public function inGroup($groupId)
    {
        $find = ['user_id' => $this->id];
        if (!is_numeric($groupId)) {
            $g = Gatekeeper::findGroupByName($groupId);
            $groupId = $g->id;
        }
        $find['group_id'] = $groupId;

        $userGroup = new UserGroupModel($this->getDb());
        $userGroup = $this->getDb()->find($userGroup, $find);
        if ($userGroup->id === null) {
            return false;
        }
        return ($userGroup->id !== null && $userGroup->groupId == $groupId) ? true : false;
    }

    /**
     * Check to see if a user has a permission
     *
     * @param integer $permId Permission ID or name
     * @return boolean Found/not found in user permission set
     */
    public function hasPermission($permId)
    {
        $find = ['user_id' => $this->id];
        if (!is_numeric($permId)) {
            $p = Gatekeeper::findPermissionByName($permId);
            $permId = $p->id;
        }
        $find['permission_id'] = $permId;

        $perm = new UserPermissionModel($this->getDb());
        $perm = $this->getDb()->find($perm, $find);
        return ($perm->id !== null && $perm->id === $permId) ? true : false;
    }

    /**
     * Check to see if a user is banned
     *
     * @return boolean User is/is not banned
     */
    public function isBanned()
    {
        $throttle = new ThrottleModel($this->getDb());
        $throttle = $this->getDb()->find($throttle, array('user_id' => $this->id));

        return ($throttle->status === ThrottleModel::STATUS_BLOCKED) ? true : false;
    }

    /**
     * Find the number of login attempts for a user
     *
     * @param integer $userId User ID [optional]
     * @return integer Number of login attempts
     */
    public function findAttemptsByUser($userId = null)
    {
        $userId = ($userId === null) ? $this->id : $userId;

        $throttle = new ThrottleModel($this->getDb());
        $throttle = $this->getDb()->find($throttle, array('user_id' => $userId));

        return ($throttle->attempts === null) ? 0 : $throttle->attempts;
    }

    /**
     * Grant permissions and groups (multiple) at the same time
     *
     * @param array $config Configuration settings (permissions & groups)
     * @param integer $expire Expiration time for the settings
     */
    public function grant(array $config, $expire = null)
    {
        $return = true;
        if (isset($config['permissions'])) {
            $result = $this->grantPermissions($config['permissions'], $expire);
            if ($result === false && $return === true) {
                $return = false;
            }
        }
        if (isset($config['groups'])) {
            $result = $this->grantGroups($config['groups'], $expire);
            if ($result === false && $return === true) {
                $return = false;
            }
        }
        return $return;
    }

    /**
     * Handle granting of multiple permissions
     *
     * @param array $permissions Set of permissions (either IDs or objects)
     * @param integer $expire EXpiration (unix timestamp) for the permissions
     * @return boolean Success/fail of all saves
     */
    public function grantPermissions(array $permissions, $expire = null)
    {
        $return = true;
        foreach ($permissions as $permission) {
            $permission = ($permission instanceof PermissionModel) ? $permission->id : $permission;
            $data = [
                'userId' => $this->id,
                'permissionId' => $permission
            ];
            if ($expire !== null && is_int($expire)) {
                $data['expire'] = $expire;
            }
            $userPerm = new UserPermissionModel($this->getDb(), $data);
            $result = $this->getDb()->save($userPerm);
            if ($result === false && $return === true) {
                $return = false;
            }
        }
        return $return;
    }

    /**
     * Handle granting of multiple groups
     *
     * @param array $groups Set of groups (either IDs or objects)
     * @param integer $expire EXpiration (unix timestamp) for the permissions
     * @return boolean Success/fail of all saves
     */
    public function grantGroups(array $groups, $expire = null)
    {
        $return = true;
        foreach ($groups as $group) {
            $group = ($group instanceof GroupModel) ? $group->id : $group;
            $data = [
                'userId' => $this->id,
                'groupId' => $group
            ];
            if ($expire !== null && is_int($expire)) {
                $data['expire'] = $expire;
            }
            $userGroup = new UserGroupModel($this->getDb(), $data);
            $result = $this->getDb()->save($userGroup);
            if ($result === false && $return === true) {
                $return = false;
            }
        }
        return $return;
    }

    /**
     * Add a new security question to the current user
     *
     * @param array $data Security question data
     * @return boolean Result of save operation
     */
    public function addSecurityQuestion(array $data)
    {
        if (!isset($data['question']) || !isset($data['answer'])) {
            throw new \InvalidArgumentException('Invalid question/answer data provided.');
        }

        // Ensure that the answer isn't the same as the user's password
        if (password_verify($data['answer'], $this->password) === true) {
            throw new \InvalidArgumentException('Security question answer cannot be the same as password.');
        }

        $question = new SecurityQuestionModel($this->getDb(), array(
            'question' => $data['question'],
            'answer' => $data['answer'],
            'userId' => $this->id
        ));
        return $this->getDb()->save($question);
    }

    /**
     * Update the last login time for the current user
     *
     * @param integer $time Unix timestamp [optional]
     * @return boolean Success/fail of update
     */
    public function updateLastLogin($time = null)
    {
        $time = ($time !== null) ? $time : time();
        $this->lastLogin = date('Y-m-d H:i:s', $time);
        return $this->getDb()->save($this);
    }
}