partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 7089f6e4f083f4d4e80ec328ad7f184519321f4d
parent 90b316951207ab1e6635f8d790ab41cde0ecf432
Author: Felicitus <felicitus@felicitus.org>
Date:   Tue, 15 Sep 2015 19:44:46 +0200

Refactored auth services. See https://github.com/partkeepr/PartKeepr/issues/428 for the reason and verbose explanation

Diffstat:
Mapp/config/config.yml | 22+++++-----------------
Mapp/config/config_test.yml | 13++++++++++---
Mapp/config/security.yml | 22++++++++++++++--------
Msrc/PartKeepr/AuthBundle/Controller/DefaultController.php | 11++++++-----
Msrc/PartKeepr/AuthBundle/DataFixtures/LoadUserData.php | 6+++---
Asrc/PartKeepr/AuthBundle/Entity/FOSUser.php | 19+++++++++++++++++++
Msrc/PartKeepr/AuthBundle/Entity/User.php | 227+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/PartKeepr/AuthBundle/Resources/config/services.xml | 19+++++++++++--------
Asrc/PartKeepr/AuthBundle/Security/Authentication/AuthenticationProviderManager.php | 115+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/AuthBundle/Services/UserService.php | 63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/AuthBundle/Tests/Controller/DefaultControllerTest.php | 6+++++-
11 files changed, 469 insertions(+), 54 deletions(-)

diff --git a/app/config/config.yml b/app/config/config.yml @@ -105,7 +105,7 @@ twig: fos_user: db_driver: orm firewall_name: main - user_class: PartKeepr\AuthBundle\Entity\User + user_class: PartKeepr\AuthBundle\Entity\FOSUser doctrine_migrations: dir_name: %kernel.root_dir%/../src/PartKeepr/CoreBundle/DoctrineMigrations @@ -151,7 +151,7 @@ services: part.category_path_listener: class: PartKeepr\PartBundle\Listeners\CategoryPathListener arguments: - - "@service_container" + - "service_container" tags: - { name: doctrine.event_listener, event: onFlush } @@ -367,12 +367,8 @@ services: - "@resource.part" # Resource - [ "PUT" ] # Methods - "/parts/{id}/addStock" # Path - - "PartKeeprPartBundle:Part:addStock" # Controller + - "partkeepr.part.add_stock" # Controller - "PartAddStock" # Route name - - # Context (will be present in Hydra documentation) - "@type": "hydra:Operation" - "hydra:title": "A custom operation" - "returns": "xmls:string" resource.part.item_operation.remove_stock: class: "Dunglas\ApiBundle\Api\Operation\Operation" @@ -382,12 +378,8 @@ services: - "@resource.part" # Resource - [ "PUT" ] # Methods - "/parts/{id}/removeStock" # Path - - "PartKeeprPartBundle:Part:removeStock" # Controller + - "partkeepr.part.remove_stock" # Controller - "PartRemoveStock" # Route name - - # Context (will be present in Hydra documentation) - "@type": "hydra:Operation" - "hydra:title": "A custom operation" - "returns": "xmls:string" resource.part.item_operation.set_stock: class: "Dunglas\ApiBundle\Api\Operation\Operation" @@ -397,12 +389,8 @@ services: - "@resource.part" # Resource - [ "PUT" ] # Methods - "/parts/{id}/setStock" # Path - - "PartKeeprPartBundle:Part:setStock" # Controller + - "partkeepr.part.set_stock" # Controller - "PartSetStock" # Route name - - # Context (will be present in Hydra documentation) - "@type": "hydra:Operation" - "hydra:title": "A custom operation" - "returns": "xmls:string" resource.part: parent: "api.resource" diff --git a/app/config/config_test.yml b/app/config/config_test.yml @@ -46,9 +46,16 @@ liip_functional_test: username: "admin" password: "admin" - security: + providers: + in_memory: + memory: + users: + admin: + password: admin + roles: 'ROLE_ADMIN' firewalls: main: - anonymous: ~ - http_basic: ~ + http_basic: + realm: "Secured Demo Area" + provider: in_memory diff --git a/app/config/security.yml b/app/config/security.yml @@ -1,5 +1,6 @@ security: encoders: + Symfony\Component\Security\Core\User\User: plaintext FOS\UserBundle\Model\UserInterface: algorithm: sha512 iterations: 1 @@ -9,6 +10,11 @@ security: ROLE_SUPER_ADMIN: ROLE_ADMIN providers: + chain_provider: + chain: + providers: [in_memory, fos_userbundle] + in_memory: + memory: ~ fos_userbundle: id: fos_user.user_provider.username @@ -18,16 +24,17 @@ security: security: false main: pattern: ^/api/.* + provider: fos_userbundle wsse: - realm: "Secured with WSSE" #identifies the set of resources to which the authentication information will apply (WWW-Authenticate) - profile: "UsernameToken" #WSSE profile (WWW-Authenticate) - encoder: #digest algorithm - algorithm: sha512 - encodeHashAsBase64: true - iterations: 1 + realm: "Secured with WSSE" #identifies the set of resources to which the authentication information will apply (WWW-Authenticate) + profile: "UsernameToken" #WSSE profile (WWW-Authenticate) + encoder: #digest algorithm + algorithm: sha512 + encodeHashAsBase64: true + iterations: 1 access_control: - { path: ^/login$, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/resetting, role: IS_AUTHENTICATED_ANONYMOUSLY } - - { path: ^/admin/, role: ROLE_ADMIN }- \ No newline at end of file + - { path: ^/admin/, role: ROLE_ADMIN } diff --git a/src/PartKeepr/AuthBundle/Controller/DefaultController.php b/src/PartKeepr/AuthBundle/Controller/DefaultController.php @@ -6,6 +6,7 @@ use FOS\RestBundle\Controller\Annotations\RequestParam; use FOS\RestBundle\Controller\Annotations\View; use FOS\RestBundle\Controller\FOSRestController; use FOS\RestBundle\Request\ParamFetcher; +use PartKeepr\AuthBundle\Entity\FOSUser; use PartKeepr\AuthBundle\Entity\User; use PartKeepr\AuthBundle\Entity\User\Exceptions\InvalidLoginDataException; use PartKeepr\AuthBundle\Response\LoginResponse; @@ -27,18 +28,19 @@ class DefaultController extends FOSRestController * @return LoginResponse * @throws InvalidLoginDataException */ - public function getSaltAction (ParamFetcher $paramFetcher) { + public function getSaltAction(ParamFetcher $paramFetcher) + { $entityManager = $this->getDoctrine()->getManager(); $repository = $entityManager->getRepository( - 'PartKeepr\AuthBundle\Entity\User' + 'PartKeepr\AuthBundle\Entity\FOSUser' ); /** - * @var $user User + * @var $user FOSUser */ $user = $repository->findOneBy(array("username" => $paramFetcher->get("username"))); return $user->getSalt(); } -}- \ No newline at end of file +} diff --git a/src/PartKeepr/AuthBundle/DataFixtures/LoadUserData.php b/src/PartKeepr/AuthBundle/DataFixtures/LoadUserData.php @@ -3,11 +3,12 @@ namespace PartKeepr\AuthBundle\DataFixtures; use Doctrine\Common\DataFixtures\AbstractFixture; use Doctrine\Common\Persistence\ObjectManager; +use PartKeepr\AuthBundle\Entity\FOSUser; use PartKeepr\AuthBundle\Entity\User; class LoadUserData extends AbstractFixture { public function load (ObjectManager $manager) { - $admin = new User(); + $admin = new FOSUser(); $admin->setUsername("admin"); $admin->setPassword("admin"); $admin->setEmail("foo@bar.com"); @@ -17,4 +18,4 @@ class LoadUserData extends AbstractFixture { $this->addReference("user.admin", $admin); } -}- \ No newline at end of file +} diff --git a/src/PartKeepr/AuthBundle/Entity/FOSUser.php b/src/PartKeepr/AuthBundle/Entity/FOSUser.php @@ -0,0 +1,19 @@ +<?php +namespace PartKeepr\AuthBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; +use FOS\UserBundle\Model\User as BaseUser; + +/** + * @ORM\Entity + * @ORM\Table(name="FOSUser") + */ +class FOSUser extends BaseUser +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue(strategy="AUTO") + */ + protected $id; +} diff --git a/src/PartKeepr/AuthBundle/Entity/User.php b/src/PartKeepr/AuthBundle/Entity/User.php @@ -2,20 +2,230 @@ namespace PartKeepr\AuthBundle\Entity; use Doctrine\ORM\Mapping as ORM; -use FOS\UserBundle\Model\User as BaseUser; use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; +use PartKeepr\Util\BaseEntity; /** * @ORM\Entity - * @ORM\Table(name="PartKeeprUser") + * @ORM\Table( + * name="PartKeeprUser", + * uniqueConstraints={@ORM\UniqueConstraint(name="username_provider", columns={"username", "provider"})}) * @TargetService(uri="/api/users") */ -class User extends BaseUser +class User extends BaseEntity { /** - * @ORM\Id - * @ORM\Column(type="integer") - * @ORM\GeneratedValue(strategy="AUTO") + * @ORM\Column(length=50) */ - protected $id; -}- \ No newline at end of file + private $username; + + /** + * @ORM\Column(length=32,nullable=true) + */ + private $password; + + /** + * @ORM\Column(type="boolean") + */ + private $admin; + + /** + * @ORM\Column(type="datetime",nullable=true) + */ + private $lastSeen; + + /** + * @ORM\Column(type="string", length=255) + * @var string + */ + private $provider; + + /** + * Creates a new user object. + * + * @param string $username The username to set (optional) + * @param string $provider The authentification provider + * + * @throws \Exception + */ + public function __construct($username = null, $provider = null) + { + if ($username !== null) { + $this->setUsername($username); + } + + if ($provider === null) { + throw new \Exception("The authentification provider must be specified"); + } + + $this->setProvider($provider); + + $this->setAdmin(false); + } + + /** + * Sets the authentification provider + * + * @param $provider + */ + public function setProvider($provider) + { + $this->provider = $provider; + } + + /** + * Returns the authentification provider + * + * @return string + */ + public function getProvider() + { + return $this->provider; + } + + /** + * Sets the username. + * + * @param string $username The username to set. + */ + public function setUsername($username) + { + $this->username = $username; + } + + /** + * Sets the raw username, without replacing any special chars. + * + * This method should only be used for building a temporary user + * for login checks. + * + * @param string $username The raw username + */ + public function setRawUsername($username) + { + $this->username = $username; + } + + /** + * Returns the username. + * + * @return string The username + */ + public function getUsername() + { + return $this->username; + } + + /** + * Sets the admin flag + * + * @param boolean $bAdmin True if the user is an admin, false otherwise + */ + public function setAdmin($bAdmin) + { + $this->admin = (boolean)$bAdmin; + } + + /** + * Returns the admin flag + * + * @return boolean True if the user is an admin + */ + public function isAdmin() + { + return $this->admin; + } + + /** + * Sets the user's password. Automatically + * applies md5 hashing. + * + * @param string $password + */ + public function setPassword($password) + { + $this->setHashedPassword(md5($password)); + } + + /** + * Returns the user's md5-hashed password. + * + * @param none + * + * @return string The md5-hashed password + */ + public function getHashedPassword() + { + return $this->password; + } + + /** + * Sets the user's password. Expects a hash + * and does not apply md5 hasing. + * + * @param string $hashedPassword + */ + public function setHashedPassword($hashedPassword) + { + $this->password = $hashedPassword; + } + + /** + * Compares the given un-hashed password with the + * object's hashed password. + * + * + * @param string $password The unhashed password + * + * @return boolean true if the passwords match, false otherwise + */ + public function comparePassword($password) + { + return $this->compareHashedPassword(md5($password)); + } + + /** + * Compares the given hashed password with the object's + * hashed password. + * + * @param string $hashedPassword The md5-hashed password + * + * @return boolean true if the passwords match, false otherwise + */ + public function compareHashedPassword($hashedPassword) + { + if ($hashedPassword == $this->password) { + return true; + } else { + return false; + } + } + + /** + * Updates the last seen field to the current time. + */ + public function updateSeen() + { + $this->lastSeen = new \DateTime("now"); + } + + /** + * Retrieve the last seen flag for a user. + * + * @return \DateTime + */ + public function getLastSeen() + { + return $this->lastSeen; + } + + /** + * Unserializes the user. + * + * @param string $serialized + */ + public function unserialize($serialized) + { + + } +} diff --git a/src/PartKeepr/AuthBundle/Resources/config/services.xml b/src/PartKeepr/AuthBundle/Resources/config/services.xml @@ -1,16 +1,19 @@ <?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> + + <parameters> + <parameter key="security.authentication.manager.class"> + PartKeepr\AuthBundle\Security\Authentication\AuthenticationProviderManager + </parameter> + </parameters> - <!-- <services> - <service id="part_keepr_auth.example" class="PartKeepr\AuthBundle\Example"> - <argument type="service" id="service_id" /> - <argument>plain_value</argument> - <argument>%parameter_name%</argument> + <service id="partkeepr.userservice" class="PartKeepr\AuthBundle\Services\UserService"> + <argument type="service" id="security.token_storage"/> + <argument type="service" id="doctrine.orm.entity_manager"/> </service> </services> - --> </container> diff --git a/src/PartKeepr/AuthBundle/Security/Authentication/AuthenticationProviderManager.php b/src/PartKeepr/AuthBundle/Security/Authentication/AuthenticationProviderManager.php @@ -0,0 +1,115 @@ +<?php +namespace PartKeepr\AuthBundle\Security\Authentication; + +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface; +use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Security\Core\Event\AuthenticationEvent; +use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; +use Symfony\Component\Security\Core\Exception\AccountStatusException; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\Exception\ProviderNotFoundException; + +/** + * Class AuthenticationProviderManager + * + * Re-implementation of the Symfony AuthenticationProviderManager to store the authentication provider in + * a token property. + */ +class AuthenticationProviderManager implements AuthenticationManagerInterface +{ + private $providers; + + private $eraseCredentials; + + private $eventDispatcher; + + /** + * Constructor. + * + * @param AuthenticationProviderInterface[] $providers An array of AuthenticationProviderInterface instances + * @param bool $eraseCredentials Whether to erase credentials after authentication or not + * + * @throws \InvalidArgumentException + */ + public function __construct(array $providers, $eraseCredentials = true) + { + if (!$providers) { + throw new \InvalidArgumentException('You must at least add one authentication provider.'); + } + + foreach ($providers as $provider) { + if (!$provider instanceof AuthenticationProviderInterface) { + throw new \InvalidArgumentException(sprintf('Provider "%s" must implement the AuthenticationProviderInterface.', + get_class($provider))); + } + } + + $this->providers = $providers; + $this->eraseCredentials = (bool)$eraseCredentials; + } + + public function setEventDispatcher(EventDispatcherInterface $dispatcher) + { + $this->eventDispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public function authenticate(TokenInterface $token) + { + $lastException = null; + $result = null; + + foreach ($this->providers as $provider) { + if (!$provider->supports($token)) { + continue; + } + + try { + $result = $provider->authenticate($token); + + if (null !== $result) { + $result->setAttribute("provider", get_class($provider)); + break; + } + } catch (AccountStatusException $e) { + $e->setToken($token); + + throw $e; + } catch (AuthenticationException $e) { + $lastException = $e; + } + } + + if (null !== $result) { + if (true === $this->eraseCredentials) { + $result->eraseCredentials(); + } + + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(AuthenticationEvents::AUTHENTICATION_SUCCESS, + new AuthenticationEvent($result)); + } + + return $result; + } + + if (null === $lastException) { + $lastException = new ProviderNotFoundException(sprintf('No Authentication Provider found for token of class "%s".', + get_class($token))); + } + + if (null !== $this->eventDispatcher) { + $this->eventDispatcher->dispatch(AuthenticationEvents::AUTHENTICATION_FAILURE, + new AuthenticationFailureEvent($token, $lastException)); + } + + $lastException->setToken($token); + + throw $lastException; + } +} diff --git a/src/PartKeepr/AuthBundle/Services/UserService.php b/src/PartKeepr/AuthBundle/Services/UserService.php @@ -0,0 +1,63 @@ +<?php +namespace PartKeepr\AuthBundle\Services; + + +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\NoResultException; +use Doctrine\ORM\QueryBuilder; +use PartKeepr\AuthBundle\Entity\User; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; + +class UserService +{ + private $tokenStorage; + + /* + * @var EntityManager + */ + private $entityManager; + + public function __construct(TokenStorage $tokenStorage, EntityManager $entityManager) + { + $this->tokenStorage = $tokenStorage; + $this->entityManager = $entityManager; + } + + public function getUser() + { + $provider = $this->tokenStorage->getToken()->getAttribute("provider"); + $username = $this->tokenStorage->getToken()->getUsername(); + + /** + * @var QueryBuilder $queryBuilder + */ + $queryBuilder = $this->entityManager->createQueryBuilder(); + + $queryBuilder->select("u") + ->from("PartKeeprAuthBundle:User", "u") + ->where("u.provider = :provider") + ->andWhere("u.username = :username") + ->setParameter("provider", $provider) + ->setParameter("username", $username); + + $query = $queryBuilder->getQuery(); + + try { + $user = $query->getSingleResult(); + + return $user; + } catch (NoResultException $e) { + return $this->createProxyUser($username, $provider); + } + + } + + private function createProxyUser($username, $provider) + { + $user = new User($username, $provider); + $this->entityManager->persist($user); + $this->entityManager->flush(); + + return $user; + } +} diff --git a/src/PartKeepr/AuthBundle/Tests/Controller/DefaultControllerTest.php b/src/PartKeepr/AuthBundle/Tests/Controller/DefaultControllerTest.php @@ -4,6 +4,7 @@ namespace PartKeepr\AuthBundle\Tests\Controller; use Doctrine\Common\DataFixtures\ProxyReferenceRepository; use Liip\FunctionalTestBundle\Test\WebTestCase; +use PartKeepr\AuthBundle\Entity\FOSUser; use PartKeepr\AuthBundle\Entity\User; class DefaultControllerTest extends WebTestCase @@ -28,6 +29,7 @@ class DefaultControllerTest extends WebTestCase $request = array("username" => "admin"); + $client->request( 'POST', '/auth/getSalt', @@ -37,13 +39,15 @@ class DefaultControllerTest extends WebTestCase json_encode($request) ); + $response = json_decode($client->getResponse()->getContent()); $admin = $this->fixtures->getReference("user.admin"); /** - * @var User $admin + * @var FOSUser $admin */ + $this->assertEquals($admin->getSalt(), $response); } }