diff --git a/Authentication/Auth.php b/Authentication/Auth.php new file mode 100644 index 0000000..77ebab2 --- /dev/null +++ b/Authentication/Auth.php @@ -0,0 +1,202 @@ + + * @license GNU General Public License, version 2 (GPL-2.0) + * + * For full copyright and license information, please see + * the docs/CREDITS.txt file. + * + */ + +namespace phpBB\SessionsAuthBundle\Authentication; + +/** + * Permission/Auth class + * + * This is a copy based on the original phpBB Auth class. It has been modified to work with + * Doctrine entities for the ACL_OPTIONS_TABLE, and all unneeded stuff has been removed. + * + * This version does not support recreating the user_permissions field in the user table. + * If there is no user_permissions field, it will result in a exception. + * + */ +class Auth +{ + private $acl = array(); + private $cache = array(); + private $aclOptions = array(); + private $aclForumIds = false; + + /** + * Init permissions + * @param $userPermissions string + * @throws \Exception + */ + public function acl($userPermissions) + { + $this->acl = array(); + $this->cache = array(); + $this->aclOptions = array(); + $this->aclForumIds = false; + + if (($this->aclOptions = $cache->get('_acl_options')) === false) + { + $sql = 'SELECT auth_option_id, auth_option, is_global, is_local + FROM ' . ACL_OPTIONS_TABLE . ' + ORDER BY auth_option_id'; + $result = $db->sql_query($sql); + + $global = 0; + $local = 0; + $this->aclOptions = array(); + while ($row = $db->sql_fetchrow($result)) + { + if ($row['is_global']) + { + $this->aclOptions['global'][$row['auth_option']] = $global++; + } + + if ($row['is_local']) + { + $this->aclOptions['local'][$row['auth_option']] = $local++; + } + + $this->aclOptions['id'][$row['auth_option']] = (int) $row['auth_option_id']; + $this->aclOptions['option'][(int) $row['auth_option_id']] = $row['auth_option']; + } + $db->sql_freeresult($result); + + $cache->put('_acl_options', $this->aclOptions); + } + + if (!trim($userPermissions)) + { + throw new \Exception('We require user_permissions set by phpBB.'); + } + + // Fill ACL array + $this->_fill_acl($userPermissions); + + // Verify bitstring length with options provided... + $renew = false; + $global_length = sizeof($this->aclOptions['global']); + $local_length = sizeof($this->aclOptions['local']); + + // Specify comparing length (bitstring is padded to 31 bits) + $global_length = ($global_length % 31) ? ($global_length - ($global_length % 31) + 31) : $global_length; + $local_length = ($local_length % 31) ? ($local_length - ($local_length % 31) + 31) : $local_length; + + // You thought we are finished now? Noooo... now compare them. + foreach ($this->acl as $forum_id => $bitstring) + { + if (($forum_id && strlen($bitstring) != $local_length) || (!$forum_id && strlen($bitstring) != $global_length)) + { + $renew = true; + break; + } + } + + // If a bitstring within the list does not match the options, we have a user with incorrect permissions set and need to renew them + if ($renew) + { + throw new \Exception('Renewing is not supported.'); + } + + return; + } + + /** + * Fill ACL array with relevant bitstrings from user_permissions column + * @param $userPermissions + */ + private function _fill_acl($userPermissions) + { + $seq_cache = array(); + $this->acl = array(); + $userPermissions = explode("\n", $userPermissions); + + foreach ($userPermissions as $f => $seq) + { + if ($seq) + { + $i = 0; + + if (!isset($this->acl[$f])) + { + $this->acl[$f] = ''; + } + + while ($subseq = substr($seq, $i, 6)) + { + if (isset($seq_cache[$subseq])) + { + $converted = $seq_cache[$subseq]; + } + else + { + $result = str_pad(base_convert($subseq, 36, 2), 31, 0, STR_PAD_LEFT); + $converted = $result; + $seq_cache[$subseq] = $result; + } + + // We put the original bitstring into the acl array + $this->acl[$f] .= $converted; + $i += 6; + } + } + } + } + + /** + * Look up an option + * if the option is prefixed with !, then the result becomes negated + * + * If a forum id is specified the local option will be combined with a global option if one exist. + * If a forum id is not specified, only the global option will be checked. + * @param $opt string option + * @param int $forumId int forum_id + * @return bool + */ + public function aclGet($opt, $forumId = 0) + { + $negate = false; + + if (strpos($opt, '!') === 0) + { + $negate = true; + $opt = substr($opt, 1); + } + + if (!isset($this->cache[$forumId][$opt])) + { + // We combine the global/local option with an OR because some options are global and local. + // If the user has the global permission the local one is true too and vice versa + $this->cache[$forumId][$opt] = false; + + // Is this option a global permission setting? + if (isset($this->aclOptions['global'][$opt])) + { + if (isset($this->acl[0])) + { + $this->cache[$forumId][$opt] = $this->acl[0][$this->aclOptions['global'][$opt]]; + } + } + + // Is this option a local permission setting? + // But if we check for a global option only, we won't combine the options... + if ($forumId != 0 && isset($this->aclOptions['local'][$opt])) + { + if (isset($this->acl[$forumId]) && isset($this->acl[$forumId][$this->aclOptions['local'][$opt]])) + { + $this->cache[$forumId][$opt] |= $this->acl[$forumId][$this->aclOptions['local'][$opt]]; + } + } + } + + // Founder always has all global options set to true... + return ($negate) ? !$this->cache[$forumId][$opt] : $this->cache[$forumId][$opt]; + } +} \ No newline at end of file diff --git a/Authentication/phpBBSessionAuthenticator.php b/Authentication/phpBBSessionAuthenticator.php index 3f315a1..64e618c 100644 --- a/Authentication/phpBBSessionAuthenticator.php +++ b/Authentication/phpBBSessionAuthenticator.php @@ -9,11 +9,14 @@ namespace phpBB\SessionsAuthBundle\Authentication; +use Doctrine\ORM\EntityManager; +use phpBB\SessionsAuthBundle\Authentication\Provider\phpBBUserProvider; +use phpBB\SessionsAuthBundle\Entity\Session; use phpBB\SessionsAuthBundle\Tokens\phpBBToken; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\SimplePreAuthenticatorInterface; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -22,36 +25,119 @@ class phpBBSessionAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface { + const ANONYMOUS = 1; + + /** @var string */ + private $cookieName; + /** @var string */ - private $cookiename; + private $boardUrl; /** @var string */ - private $boardurl; + private $loginPage; + + /** @var RequestStack */ + private $requestStack; + + /** @var ContainerInterface */ + private $container; /** @var string */ - private $loginpage; + private $dbConnection; /** * @param $cookiename string * @param $boardurl string * @param $loginpage string + * @param $requestStack RequestStack + * @param ContainerInterface $container */ - public function __construct($cookiename, $boardurl, $loginpage) + public function __construct($cookiename, $boardurl, $loginpage, $dbconnection, + RequestStack $requestStack, ContainerInterface $container) { - $this->cookiename = $cookiename; - $this->boardurl = $boardurl; - $this->loginpage = $loginpage; - + $this->cookieName = $cookiename; + $this->boardUrl = $boardurl; + $this->loginPage = $loginpage; + $this->dbConnection = $dbconnection; + $this->requestStack = $requestStack; + $this->container = $container; } /** * @param TokenInterface $token * @param UserProviderInterface $userProvider * @param $providerKey + * @return null|phpBBToken */ public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey) { - // TODO: Implement authenticateToken() method. + if (!$userProvider instanceof phpBBUserProvider) + { + throw new \InvalidArgumentException( + sprintf( + 'The user provider must be an instance of phpBBUserProvider (%s was given).', + get_class($userProvider) + ) + ); + } + + $sessionId = $this->requestStack->getCurrentRequest()->cookies->get($this->cookieName . '_sid'); + $userId = $this->requestStack->getCurrentRequest()->cookies->get($this->cookieName . '_u'); + + if (empty($sessionId)) + { + return null; // We can't authenticate if no SID is available. + } + + /** @var EntityManager $em */ + $em = $this->container->get('doctrine')->getManager($this->dbConnection); + + /** @var Session $session */ + $session = $em->getRepository('phpbbSessionsAuthBundle:Session')->findById($sessionId); + + + if (!$session || + $session->getUser() == null || + ($session->getUser() != null && $session->getUser()->getId() == self::ANONYMOUS) || + $session->getUser()->getId() != $userId) + { + return null; + } + + $userIp = $this->requestStack->getCurrentRequest()->getClientIp(); + + if (strpos($userIp, ':') !== false && strpos($session->getIp(), ':') !== false) + { + $s_ip = $this->shortIpv6($session->getIp(), 3); + $u_ip = $this->shortIpv6($userIp, 3); + } + else + { + $s_ip = implode('.', array_slice(explode('.', $session->getIp()), 0, 3)); + $u_ip = implode('.', array_slice(explode('.', $userIp), 0, 3)); + } + + // Assume session length of 3600 + if ($u_ip === $s_ip && $session->getTime() < time() - 3600 + 60) + { + // We have a valid user, which is not the guest user. + + $roles = array(); + + if ($session->getUser()->isBot()) { + $roles[] = 'ROLE_BOT'; + } + else + { + + } + + $token = new phpBBToken($session->getUser(), $providerKey, $roles); + + return $token; + } + return null; + } /** @@ -83,7 +169,46 @@ public function createToken(Request $request, $providerKey) */ public function onAuthenticationFailure(Request $request, AuthenticationException $exception) { - return new RedirectResponse($this->boardurl . $this->loginpage); + return new RedirectResponse($this->boardUrl . $this->loginPage); } + + /** + * Returns the first block of the specified IPv6 address and as many additional + * ones as specified in the length paramater. + * If length is zero, then an empty string is returned. + * If length is greater than 3 the complete IP will be returned + * + * @copyright (c) phpBB Limited + * @license GNU General Public License, version 2 (GPL-2.0) + * + * @param $ip + * @param $length + * @return mixed|string + */ + private function shortIpv6($ip, $length) + { + if ($length < 1) + { + return ''; + } + + // extend IPv6 addresses + $blocks = substr_count($ip, ':') + 1; + if ($blocks < 9) + { + $ip = str_replace('::', ':' . str_repeat('0000:', 9 - $blocks), $ip); + } + if ($ip[0] == ':') + { + $ip = '0000' . $ip; + } + if ($length < 4) + { + $ip = implode(':', array_slice(explode(':', $ip), 0, 1 + $length)); + } + + return $ip; + } + } diff --git a/DependencyInjection/phpbbSessionsAuthExtension.php b/DependencyInjection/phpbbSessionsAuthExtension.php index 5880773..c72e453 100644 --- a/DependencyInjection/phpbbSessionsAuthExtension.php +++ b/DependencyInjection/phpbbSessionsAuthExtension.php @@ -30,11 +30,18 @@ public function load(array $configs, ContainerBuilder $container) $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); + $container->setParameter('phpbb_sessions_auth.database.connection', $config['database']['connection']); $container->setParameter('phpbb_sessions_auth.database.prefix', $config['database']['prefix']); $container->setParameter('phpbb_sessions_auth.database.cookiename', $config['session']['cookiename']); $container->setParameter('phpbb_sessions_auth.database.boardurl', $config['session']['boardurl']); $container->setParameter('phpbb_sessions_auth.database.loginpage', $config['session']['loginpage']); + + // Yes, Yes, These defines are needed for Auth (From phpBB) + define('ACL_NEVER', 0); + define('ACL_YES', 1); + define('ACL_NO', -1); + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $loader->load('services.yml'); } diff --git a/Entity/Session.php b/Entity/Session.php new file mode 100644 index 0000000..17ce645 --- /dev/null +++ b/Entity/Session.php @@ -0,0 +1,114 @@ + + * @license MIT + * + */ +namespace phpBB\SessionsAuthBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Security\Core\User\UserInterface; + +/** + * Class Session + * @package phpbb\SessionsAuthBundle\Entity + * @ORM\Entity(readOnly=true) + */ +class Session +{ + /** + * @var string + * @ORM\Column(type="string", name="session_id") + * @ORM\Id + */ + private $id; + + /** + * @var User + * @ORM\ManyToOne(targetEntity="User", inversedBy="sessions") + * @ORM\JoinColumn(name="user_id", referencedColumnName="user_id") + */ + private $user; + + /** + * @var string + * @ORM\Column(type="string", name="user_ip") + * + */ + private $ip; + + /** + * @var + * @ORM\Column(type="integer", name="session_time") + */ + private $time; + + /** + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * @param string $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * @return User + */ + public function getUser() + { + return $this->user; + } + + /** + * @param User $user + */ + public function setUser($user) + { + $this->user = $user; + } + + /** + * @return string + */ + public function getIp() + { + return $this->ip; + } + + /** + * @param string $ip + */ + public function setIp($ip) + { + $this->ip = $ip; + } + + /** + * @return mixed + */ + public function getTime() + { + return $this->time; + } + + /** + * @param mixed $time + */ + public function setTime($time) + { + $this->time = $time; + } + +} + diff --git a/Entity/User.php b/Entity/User.php index 5a1fd57..a5b973c 100644 --- a/Entity/User.php +++ b/Entity/User.php @@ -8,6 +8,7 @@ */ namespace phpBB\SessionsAuthBundle\Entity; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\User\UserInterface; @@ -44,11 +45,28 @@ class User implements UserInterface */ private $password; + /** + * @var boolean + * @ORM\Column(type="integer", name="is_bot") + */ + private $bot; + /** * @var Role[] */ private $roles; + /** + * @var ArrayCollection + * @ORM\OneToMany(targetEntity="Session", mappedBy="user") + */ + private $sessions; + + public function __construct() + { + $this->sessions = new ArrayCollection(); + } + /** * Returns the roles granted to the user. * @@ -152,6 +170,37 @@ public function setEmail($email) $this->email = $email; } + /** + * @return ArrayCollection + */ + public function getSessions() + { + return $this->sessions; + } + + /** + * @param ArrayCollection $sessions + */ + public function setSessions($sessions) + { + $this->sessions = $sessions; + } + + /** + * @return boolean + */ + public function isBot() + { + return $this->bot; + } + + /** + * @param boolean $bot + */ + public function setBot($bot) + { + $this->bot = $bot; + } } diff --git a/Resources/config/services.yml b/Resources/config/services.yml index fafc566..4037fb9 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -14,6 +14,9 @@ services: - %phpbb_sessions_auth.database.cookiename% - %phpbb_sessions_auth.database.boardurl% - %phpbb_sessions_auth.database.loginpage% + - %phpbb_sessions_auth.database.connection% + - @request_stack + - @service_container phpbb.sessionsauthbundle.phpbb_user_provider: class: phpBB\SessionsAuthBundle\Authentication\Provider\phpBBUserProvider diff --git a/Subscriber/TablePrefixSubscriber.php b/Subscriber/TablePrefixSubscriber.php index 89c3abc..fb3ca94 100644 --- a/Subscriber/TablePrefixSubscriber.php +++ b/Subscriber/TablePrefixSubscriber.php @@ -25,11 +25,11 @@ class TablePrefixSubscriber implements EventSubscriber /** * Namespace the entity is in */ - const ENTITY_NAMESPACE = 'phpbb\\SessionAuthBundle\\Entity'; + private static $ENTITY_NAMESPACE = 'phpBB\\SessionsAuthBundle\\Entity'; /** * Entity that will receive the prefix */ - const ENTITY_NAME = 'User'; + private static $ENTITY_NAME; /** * @var string @@ -44,6 +44,7 @@ class TablePrefixSubscriber implements EventSubscriber public function __construct($prefix) { $this->prefix = (string) $prefix; + self::$ENTITY_NAME = array(self::$ENTITY_NAMESPACE . '\\User', self::$ENTITY_NAMESPACE . '\\Session'); } /** @@ -74,13 +75,13 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $args) return; } - if ($classMetadata->namespace == self::ENTITY_NAMESPACE && $classMetadata->name == self::ENTITY_NAME) + if ($classMetadata->namespace == self::$ENTITY_NAMESPACE && in_array($classMetadata->name, self::$ENTITY_NAME)) { // Do not re-apply the prefix when the table is already prefixed if (false === strpos($classMetadata->getTableName(), $this->prefix)) { $tableName = $this->prefix . $classMetadata->getTableName(); - $classMetadata->setPrimaryTable(['name' => $tableName]); + $classMetadata->setPrimaryTable(array('name' => $tableName)); } foreach ($classMetadata->getAssociationMappings() as $fieldName => $mapping)