psecio/gatekeeper

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

Summary

Maintainability
C
1 day
Test Coverage
<?php

namespace Psecio\Gatekeeper;

use \Psecio\Gatekeeper\Model\User;

class Gatekeeper
{
    /**
     * Database (PDO) instance
     * @var \PDO
     */
    private static $pdo;

    /**
     * Allowed actions
     * @var array
     */
    private static $actions = array(
        'find', 'delete', 'create', 'save', 'clone', 'count'
    );

    /**
     * Current data source
     * @var \Psecio\Gatekeeper\DataSource
     */
    private static $datasource;

    /**
     * Throttling enabled or disabled
     * @var boolean
     */
    private static $throttleStatus = true;

    /**
     * Current set of restrictions
     * @var array
     */
    private static $restrictions = array();

    /**
     * Current configuration options
     * @var array
     */
    private static $config = array();

    /**
     * Current set of policies (callback versions)
     * @var array
     */
    private static $policies = array();

    private static $logger = null;

    /**
     * Initialize the Gatekeeper instance, set up environment file and PDO connection
     *
     * @param string $envPath Environment file path (defaults to CWD)
     * @param array $config Configuration settings [optional]
     * @param \Psecio\Gatekeeper\DataSource $datasource Custom datasource provider
     */
    public static function init(
        $envPath = null,
        array $config = array(),
        \Psecio\Gatekeeper\DataSource $datasource = null,
        $logger = null
    )
    {
        $result = self::loadConfig($config, $envPath);
        if ($datasource === null) {
            $datasource = self::buildDataSource($config, $result);
        }
        self::$datasource = $datasource;

        if (isset($config['throttle']) && $config['throttle'] === false) {
            self::disableThrottle();
        }
        self::setLogger($logger);
    }

    /**
     * Get the configuration either from the config given or .env path
     *
     * @param array $config Configuration values
     * @param string $envPath Path to .env file
     * @return array Set of configuration values
     */
    public static function loadConfig(array $config, $envPath = null)
    {
        $envPath = ($envPath !== null) ? $envPath : getcwd();
        $result = self::loadDotEnv($envPath);

        // If the .env load failed, use the config given
        if ($result === false) {
            if (empty($config)) {
                throw new \InvalidArgumentException('Configuration values must be defined!');
            }
            $result = $config;
        }
        self::setConfig($result);
        return $result;
    }

    /**
     * Set the current configuration options
     *
     * @param array $config Set of configuration settings
     */
    public static function setConfig(array $config)
    {
        self::$config = $config;
    }

    /**
     * Get the current configuration information
     *     If an index is given, it tries to find it. If found,
     *     returns just that value. If not, returns null. Otherwise
     *     returns all config values
     *
     * @param string $index Index to locate [optional]
     * @return mixed Single value if index found, otherwise array of all
     */
    public static function getConfig($index = null)
    {
        if ($index !== null) {
            return (isset(self::$config[$index])) ? self::$config[$index] : null;
        } else {
            return self::$config;
        }
    }

    /**
     * Set the current logger interface
     *     If the logger value is null, a Monolog instance will be created
     *
     * @param \Psr\Log\LoggerInterface|null $logger PSR logger or null
     */
    public static function setLogger(\Psr\Log\LoggerInterface $logger = null)
    {
        if ($logger === null) {
            // make a monolog logger that logs to /tmp by default
            if (class_exists('\Monolog\Logger') === true) {
                $logger = new \Monolog\Logger('gatekeeper');
                $logger->pushHandler(
                    new \Monolog\Handler\StreamHandler('/tmp/gatekeeper.log')
                );
            }
        }
        self::$logger = $logger;
    }

    /**
     * Get the current logger instance
     *
     * @return \Psr\Log\LoggerInterface object
     */
    public static function getLogger()
    {
        return self::$logger;
    }

    /**
     * Build a datasource object
     *
     * @param array $config Configuration settings
     * @param array $result Environment data
     * @throws \Exception If data source type is not valid
     * @return \Psecio\Gatekeeper\DataSource instance
     */
    public static function buildDataSource(array $config, $result)
    {
        $dsType = (isset($config['source'])) ? $config['source'] : 'mysql';
        $dsClass = '\\Psecio\\Gatekeeper\\DataSource\\'.ucwords($dsType);
        if (!class_exists($dsClass)) {
            throw new \InvalidArgumentException('Data source type "'.$dsType.'" not valid!');
        }

        try {
            $datasource = new $dsClass($result);
            return $datasource;
        } catch (\Exception $e) {
            throw new \Exception('Error creating data source "'.$dsType.'" ('.$e->getMessage().')');
        }
    }

    /**
     * Get the current datasource
     *
     * @return \Psecio\Gatekeeper\DataSource instance
     */
    public static function getDatasource()
    {
        return self::$datasource;
    }

    /**
     * Set the current data source to the one given
     *
     * @param \Psecio\Gatekeeper\DataSource $ds Data source instance
     */
    public static function setDatasource($ds)
    {
        self::$datasource = $ds;
    }

    /**
     * Get the last error message from the current datasource
     *
     * @return string Error message (or empty string)
     */
    public static function getLastError()
    {
        return self::getDatasource()->lastError;
    }

    /**
     * Get the current list of restrictions
     *
     * @return array Set of restrictions
     */
    public static function getRestrictions()
    {
        return self::$restrictions;
    }

    /**
     * Clear out the current restrictions
     */
    public static function clearRestrictions()
    {
        self::$restrictions = [];
    }

    /**
     * Load the variables using the .env handling
     *
     * @param string $envPath Path to the .env file
     * @return array|boolean Array of data if found, false if load fails
     */
    protected static function loadDotEnv($envPath)
    {
        try {
            $dotenv = new \Dotenv\Dotenv($envPath);
            $dotenv->load();

            $config = array(
                'username' => $_SERVER['DB_USER'],
                'password' => $_SERVER['DB_PASS'],
                'name' => $_SERVER['DB_NAME'],
                'type' => (isset($_SERVER['DB_TYPE'])) ? $_SERVER['DB_TYPE'] : 'mysql',
                'host' => $_SERVER['DB_HOST']
            );
            if (isset($_SERVER['DB_PREFIX'])) {
                $config['prefix'] = $_SERVER['DB_PREFIX'];
            }
            return $config;
        } catch (\Exception $e) {
            return false;
        }
    }

    /**
     * Create and setup a new model instance
     *
     * @param string $type Model class name
     * @throws \InvalidArgumentException If class requested is not valid
     * @return object Model instance
     */
    public static function modelFactory($type, array $data = array())
    {
        $class = '\\Psecio\\Gatekeeper\\'.$type;
        if (!class_exists($class)) {
            throw new \InvalidArgumentException('Model type "'.$class.'" does not exist!');
        }
        $model = new $class(self::$datasource, $data);
        return $model;
    }


    /**
     * Safer way to evaluate if hashes equal
     *
     * @param string $hash1 Hash #1
     * @param string $hash2 Hash #1
     * @return boolean Pass/fail on hash equality
     */
    public static function hash_equals($hash1, $hash2)
    {
        if (\function_exists('hash_equals')) {
            return \hash_equals($hash1, $hash2);
        }
        if (\strlen($hash1) !== \strlen($hash2)) {
            return false;
        }
        $res = 0;
        $len = \strlen($hash1);
        for ($i = 0; $i < $len; ++$i) {
            $res |= \ord($hash1[$i]) ^ \ord($hash2[$i]);
        }
        return $res === 0;
    }

    /**
     * Authenticate a user given the username/password credentials
     *
     * @param array $credentials Credential information (must include "username" and "password")
     * @param boolean $remember Flag to activate the "remember me" functionality
     * @return boolean Pass/fail of authentication
     */
    public static function authenticate(array $credentials, $remember = false)
    {
        $username = $credentials['username'];
        $user = new UserModel(self::$datasource);
        $user->findByUsername($username);

        self::getLogger()->info('Authenticating user.', array('username' => $username));

        // If they're inactive, they can't log in
        if ($user->status === UserModel::STATUS_INACTIVE) {
            self::getLogger()->error('User is inactive and cannot login.', array('username' => $username));
            throw new Exception\UserInactiveException('User "'.$username.'" is inactive and cannot log in.');
        }

        // Handle some throttle logic, if it's turned on
        if (self::$throttleStatus === true) {
            // Set up our default throttle restriction
            $instance = new \Psecio\Gatekeeper\Restrict\Throttle(array('userId' => $user->id));
            self::$restrictions[] = $instance;
        }

        // Check any restrictions
        if (!empty(self::$restrictions)) {
            foreach (self::$restrictions as $restriction) {
                if ($restriction->evaluate() === false) {
                    self::getLogger()->error('Restriction failed.', array('restriction' => get_class($restriction)));
                    throw new Exception\RestrictionFailedException('Restriction '.get_class($restriction).' failed.');
                }
            }
        }

        // Verify the password!
        $result = password_verify($credentials['password'], $user->password);

        if (self::$throttleStatus === true && $result === true) {
            self::getLogger()->info('User login verified.', array('username' => $username));

            // If throttling is enabled, set the user back to allow
            if (isset($instance)) {
                $instance->model->allow();
            }

            $user->updateLastLogin();

            if ($remember === true) {
                self::getLogger()->info('Activating remember me.', array('username' => $username));
                self::rememberMe($user);
            }
        }

        return $result;
    }

    /**
     * Disable the throttling
     */
    public static function disableThrottle()
    {
        self::$throttleStatus = false;
    }

    /**
     * Enable the throttling feature
     */
    public static function enableThrottle()
    {
        self::$throttleStatus = true;
    }

    /**
     * Return the enabled/disabled status of the throttling
     *
     * @return boolean Throttle status
     */
    public static function throttleStatus()
    {
        return self::$throttleStatus;
    }

    /**
     * Get the user throttle information
     *     If not found, makes a new one
     *
     * @param integer $userId User ID
     * @return ThrottleModel instance
     */
    public static function getUserThrottle($userId)
    {
        $throttle = Gatekeeper::findThrottleByUserId($userId);

        if ($throttle === false) {
            $data = array(
                'user_id' => $userId,
                'attempts' => 1,
                'status' => ThrottleModel::STATUS_ALLOWED,
                'last_attempt' => date('Y-m-d H:i:s'),
                'status_change' => date('Y-m-d H:i:s')
            );
            $throttle = Gatekeeper::modelFactory('ThrottleModel', $data);
        }
        return $throttle;
    }

    /**
     * Register a new user
     *
     * @param array $userData User data
     * @return boolean Success/fail of user create
     */
    public static function register(array $userData)
    {
        $user = new UserModel(self::$datasource, $userData);
        if (self::$datasource->save($user)  === false) {
            return false;
        }
        // Add groups if they're given
        if (isset($userData['groups'])) {
            foreach ($userData['groups'] as $group) {
                $group = (is_int($group)) ? self::findGroupById($group) : self::findGroupByName($group);
                $user->addGroup($group);
            }
        }
        // Add permissions if they're given
        if (isset($userData['permissions'])) {
            foreach ($userData['permissions'] as $perm) {
                $perm = (is_int($perm)) ? self::findPermissionById($perm) : self::findPermissionByName($perm);
                $user->addPermission($perm);
            }
        }
        return $user;
    }

    /**
     * Handle undefined static function calls
     *
     * @param string $name Function name
     * @param arrya $args Arguments set
     * @return mixed Boolean false if function not matched, otherwise Model instance
     */
    public static function __callStatic($name, $args)
    {
        // find the action first
        $action = 'find';
        foreach (self::$actions as $a) {
            if (strstr($name, $a) !== false) {
                $action = $a;
            }
        }

        if ($action == 'find') {
            $action = new \Psecio\Gatekeeper\Handler\FindBy($name, $args, self::$datasource);
        } elseif ($action == 'create') {
            $action = new \Psecio\Gatekeeper\Handler\Create($name, $args, self::$datasource);
        } elseif ($action == 'delete') {
            $action = new \Psecio\Gatekeeper\Handler\Delete($name, $args, self::$datasource);
        } elseif ($action == 'save') {
            $action = new \Psecio\Gatekeeper\Handler\Save($name, $args, self::$datasource);
        } elseif ($action == 'clone') {
            $action = new \Psecio\Gatekeeper\Handler\CloneInstance($name, $args, self::$datasource);
        } elseif ($action == 'count') {
            $action = new \Psecio\Gatekeeper\Handler\Count($name, $args, self::$datasource);
        }
        return $action->execute();
    }

    /**
     * Build the model instance with data given
     *
     * @param string $action Action called (ex: "delete" or "create")
     * @param string $name Function nname
     * @param array $args Arguments set
     * @throws \Exception\ModelNotFoundException If model type is not found
     * @return object Model instance
     */
    public static function buildModel($action = 'find', $name, array $args)
    {
        $name = str_replace($action, '', $name);
        preg_match('/By(.+)/', $name, $matches);

        if (empty($matches) && $args[0] instanceof \Modler\Model) {
            $model = $name;
            $data = $args[0]->toArray();
        } else {
            $property = lcfirst($matches[1]);
            $model = str_replace($matches[0], '', $name);
            $data = array($property => $args[0]);
        }

        $modelNs = '\\Psecio\\Gatekeeper\\'.$model.'Model';
        if (!class_exists($modelNs)) {
            throw new Exception\ModelNotFoundException('Model type '.$model.' could not be found');
        }

        $instance = new $modelNs(self::$datasource);
        $instance = self::$datasource->find($instance, $data);
        return $instance;
    }

    /**
     * Create a restriction and add it to be evaluated
     *
     * @param string $type Restriction type
     * @param array $config Restriction configuration
     */
    public static function restrict($type, array $config)
    {
        $classNs = '\\Psecio\\Gatekeeper\\Restrict\\'.ucwords(strtolower($type));
        if (!class_exists($classNs)) {
            throw new \InvalidArgumentException('Restriction type "'.$type.'" is invalid');
        }
        $instance = new $classNs($config);
        self::$restrictions[] = $instance;
    }

    /**
     * Enable and set up the "Remember Me" cookie token handling for the given user
     *
     * @param \Psecio\Gatekeeper\UserModel|string $user User model instance
     * @param array $config Set of configuration settings
     * @return boolean Success/fail of sesssion setup
     */
    public static function rememberMe($user, array $config = array())
    {
        if (is_string($user)) {
            $user = Gatekeeper::findUserByUsername($user);
        }

        $data = array_merge($_COOKIE, $config);
        $remember = new Session\RememberMe(self::$datasource, $data, $user);
        return $remember->setup();
    }

    /**
     * Check the "Remember Me" token information (if it exists)
     *
     * @return boolean|\Psecio\Gatekeeper\UserModel Success/fail of token validation or User model instance
     */
    public static function checkRememberMe()
    {
        $remember = new Session\RememberMe(self::$datasource, $_COOKIE);
        return $remember->verify();
    }

    /**
     * Evaluate the policy (found by name) against the data provided
     *
     * @param string $name Name of the policy
     * @param mixed $data Data to use in evaluation (single object or array)
     * @return boolean Pass/fail status of evaluation
     */
    public static function evaluatePolicy($name, $data)
    {
        // See if it's a closure policy first
        if (array_key_exists($name, self::$policies)) {
            $policy = self::$policies[$name];
            $result = $policy($data);
            return (!is_bool($result)) ? false : $result;
        } else {
            $policy = Gatekeeper::findPolicyByName($name);
            return $policy->evaluate($data);
        }
    }

    /**
     * Allow for the creation of a policy as a callback too
     *
     * @param array $policy Policy settings
     * @return boolean Success/fail of policy creation
     */
    public static function createPolicy(array $policy)
    {
        if (is_callable($policy['expression'])) {
            $name = $policy['name'];
            self::$policies[$name] = $policy['expression'];
            return true;
        } else {
            return self::handleCreate('createPolicy', array($policy));
        }
    }
}