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:
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);
}
}