* public registration

* remove WSSE implementation
* add oAuth2 implementation
This commit is contained in:
Nicolas Lœuillet 2015-09-29 14:31:52 +02:00 committed by Jeremy Benoist
parent 8a60bc4cc2
commit fcb1fba5c2
33 changed files with 551 additions and 528 deletions

View file

@ -26,6 +26,7 @@ class AppKernel extends Kernel
new Wallabag\ApiBundle\WallabagApiBundle(), new Wallabag\ApiBundle\WallabagApiBundle(),
new Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle(), new Bazinga\Bundle\HateoasBundle\BazingaHateoasBundle(),
new Lexik\Bundle\FormFilterBundle\LexikFormFilterBundle(), new Lexik\Bundle\FormFilterBundle\LexikFormFilterBundle(),
new FOS\OAuthServerBundle\FOSOAuthServerBundle(),
); );
if (in_array($this->getEnvironment(), array('dev', 'test'))) { if (in_array($this->getEnvironment(), array('dev', 'test'))) {

View file

@ -157,3 +157,17 @@ fos_user:
db_driver: orm db_driver: orm
firewall_name: main firewall_name: main
user_class: Wallabag\CoreBundle\Entity\User user_class: Wallabag\CoreBundle\Entity\User
registration:
form:
type: wallabag_user_registration
confirmation:
enabled: true
fos_oauth_server:
db_driver: orm
client_class: Wallabag\ApiBundle\Entity\Client
access_token_class: Wallabag\ApiBundle\Entity\AccessToken
refresh_token_class: Wallabag\ApiBundle\Entity\RefreshToken
auth_code_class: Wallabag\ApiBundle\Entity\AuthCode
service:
user_provider: fos_user.user_manager

View file

@ -17,11 +17,6 @@ monolog:
type: fingers_crossed type: fingers_crossed
action_level: error action_level: error
handler: nested handler: nested
wsse:
type: stream
path: %kernel.logs_dir%/%kernel.environment%.wsse.log
level: error
channels: [wsse]
nested: nested:
type: stream type: stream
path: "%kernel.logs_dir%/%kernel.environment%.log" path: "%kernel.logs_dir%/%kernel.environment%.log"

View file

@ -30,3 +30,9 @@ homepage:
defaults: { _controller: WallabagCoreBundle:Entry:showUnread, page : 1 } defaults: { _controller: WallabagCoreBundle:Entry:showUnread, page : 1 }
requirements: requirements:
page: \d+ page: \d+
fos_user:
resource: "@FOSUserBundle/Resources/config/routing/all.xml"
fos_oauth_server_token:
resource: "@FOSOAuthServerBundle/Resources/config/routing/token.xml"

View file

@ -1,9 +1,6 @@
security: security:
encoders: encoders:
Wallabag\CoreBundle\Entity\User: FOS\UserBundle\Model\UserInterface: sha512
algorithm: sha1
encode_as_base64: false
iterations: 1
role_hierarchy: role_hierarchy:
ROLE_ADMIN: ROLE_USER ROLE_ADMIN: ROLE_USER
@ -18,11 +15,15 @@ security:
# the main part of the security, where you can set up firewalls # the main part of the security, where you can set up firewalls
# for specific sections of your app # for specific sections of your app
firewalls: firewalls:
wsse_secured: oauth_token:
pattern: /api/.* pattern: ^/oauth/v2/token
wsse: true security: false
stateless: true api:
anonymous: true pattern: /api/.*
fos_oauth: true
stateless: true
anonymous: false
login_firewall: login_firewall:
pattern: ^/login$ pattern: ^/login$
anonymous: ~ anonymous: ~
@ -45,9 +46,9 @@ security:
target: / target: /
access_control: access_control:
- { path: ^/api/salt, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/api/doc, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/register, role: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/forgot-password, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/forgot-password, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: /(unread|starred|archive).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: /(unread|starred|archive).xml$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER } - { path: ^/, roles: ROLE_USER }

View file

@ -1,9 +1,4 @@
# Learn more about services, parameters and containers at
# http://symfony.com/doc/current/book/service_container.html
parameters: parameters:
security.authentication.provider.dao.class: Wallabag\CoreBundle\Security\Authentication\Provider\WallabagAuthenticationProvider
security.encoder.digest.class: Wallabag\CoreBundle\Security\Authentication\Encoder\WallabagPasswordEncoder
security.validator.user_password.class: Wallabag\CoreBundle\Security\Validator\WallabagUserPasswordValidator
lexik_form_filter.get_filter.doctrine_orm.class: Wallabag\CoreBundle\Event\Subscriber\CustomDoctrineORMSubscriber lexik_form_filter.get_filter.doctrine_orm.class: Wallabag\CoreBundle\Event\Subscriber\CustomDoctrineORMSubscriber
services: services:

View file

@ -53,7 +53,8 @@
"pagerfanta/pagerfanta": "~1.0.3", "pagerfanta/pagerfanta": "~1.0.3",
"lexik/form-filter-bundle": "~4.0", "lexik/form-filter-bundle": "~4.0",
"j0k3r/graby": "~1.0", "j0k3r/graby": "~1.0",
"friendsofsymfony/user-bundle": "dev-master" "friendsofsymfony/user-bundle": "dev-master",
"friendsofsymfony/oauth-server-bundle": "^1.4@dev"
}, },
"require-dev": { "require-dev": {
"doctrine/doctrine-fixtures-bundle": "~2.2.0", "doctrine/doctrine-fixtures-bundle": "~2.2.0",

158
composer.lock generated
View file

@ -1,10 +1,10 @@
{ {
"_readme": [ "_readme": [
"This file locks the dependencies of your project to a known state", "This file locks the dependencies of your project to a known state",
"Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"hash": "350d05d95be50b6d93e8a046f784e00c", "hash": "7c1f2c88df608eb6e1b4bc7c5ed24acc",
"packages": [ "packages": [
{ {
"name": "doctrine/annotations", "name": "doctrine/annotations",
@ -858,6 +858,129 @@
], ],
"time": "2014-05-20 12:10:12" "time": "2014-05-20 12:10:12"
}, },
{
"name": "friendsofsymfony/oauth-server-bundle",
"version": "1.4.2",
"target-dir": "FOS/OAuthServerBundle",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfSymfony/FOSOAuthServerBundle.git",
"reference": "9e15c229eff547443d686445d629e9356ab0672e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfSymfony/FOSOAuthServerBundle/zipball/9e15c229eff547443d686445d629e9356ab0672e",
"reference": "9e15c229eff547443d686445d629e9356ab0672e",
"shasum": ""
},
"require": {
"friendsofsymfony/oauth2-php": "~1.1.0",
"php": ">=5.3.3",
"symfony/framework-bundle": "~2.1",
"symfony/security-bundle": "~2.1"
},
"require-dev": {
"doctrine/doctrine-bundle": "~1.0",
"doctrine/mongodb-odm": "1.0.*@dev",
"doctrine/orm": ">=2.2,<2.5-dev",
"symfony/class-loader": "~2.1",
"symfony/yaml": "~2.1",
"willdurand/propel-typehintable-behavior": "1.0.*"
},
"suggest": {
"doctrine/doctrine-bundle": "*",
"doctrine/mongodb-odm-bundle": "*",
"propel/propel-bundle": "If you want to use Propel with Symfony2, then you will have to install the PropelBundle",
"willdurand/propel-typehintable-behavior": "The Typehintable behavior is useful to add type hints on generated methods, to be compliant with interfaces"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-0": {
"FOS\\OAuthServerBundle": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Arnaud Le Blanc",
"email": "arnaud.lb@gmail.com"
},
{
"name": "FriendsOfSymfony Community",
"homepage": "https://github.com/FriendsOfSymfony/FOSOAuthServerBundle/contributors"
}
],
"description": "Symfony2 OAuth Server Bundle",
"homepage": "http://friendsofsymfony.github.com",
"keywords": [
"oauth",
"oauth2",
"server"
],
"time": "2014-10-31 13:44:14"
},
{
"name": "friendsofsymfony/oauth2-php",
"version": "1.1.1",
"source": {
"type": "git",
"url": "https://github.com/FriendsOfSymfony/oauth2-php.git",
"reference": "23e76537c4a02e666ab4ba5abe67a69a886a0310"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FriendsOfSymfony/oauth2-php/zipball/23e76537c4a02e666ab4ba5abe67a69a886a0310",
"reference": "23e76537c4a02e666ab4ba5abe67a69a886a0310",
"shasum": ""
},
"require": {
"php": ">=5.3.2",
"symfony/http-foundation": "~2.0"
},
"require-dev": {
"phpunit/phpunit": "~4.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"OAuth2\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Arnaud Le Blanc",
"email": "arnaud.lb@gmail.com"
},
{
"name": "FriendsOfSymfony Community",
"homepage": "https://github.com/FriendsOfSymfony/oauth2-php/contributors"
}
],
"description": "OAuth2 library",
"homepage": "https://github.com/FriendsOfSymfony/oauth2-php",
"keywords": [
"oauth",
"oauth2"
],
"time": "2014-11-03 10:21:20"
},
{ {
"name": "friendsofsymfony/rest-bundle", "name": "friendsofsymfony/rest-bundle",
"version": "1.7.1", "version": "1.7.1",
@ -2787,12 +2910,12 @@
"version": "v2.7.0", "version": "v2.7.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/AsseticBundle.git", "url": "https://github.com/symfony/assetic-bundle.git",
"reference": "3ae5c8ca3079b6e0033cc9fbfb6500e2bc964da5" "reference": "3ae5c8ca3079b6e0033cc9fbfb6500e2bc964da5"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/AsseticBundle/zipball/3ae5c8ca3079b6e0033cc9fbfb6500e2bc964da5", "url": "https://api.github.com/repos/symfony/assetic-bundle/zipball/3ae5c8ca3079b6e0033cc9fbfb6500e2bc964da5",
"reference": "3ae5c8ca3079b6e0033cc9fbfb6500e2bc964da5", "reference": "3ae5c8ca3079b6e0033cc9fbfb6500e2bc964da5",
"shasum": "" "shasum": ""
}, },
@ -2857,12 +2980,12 @@
"version": "v2.7.1", "version": "v2.7.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/MonologBundle.git", "url": "https://github.com/symfony/monolog-bundle.git",
"reference": "9320b6863404c70ebe111e9040dab96f251de7ac" "reference": "9320b6863404c70ebe111e9040dab96f251de7ac"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/MonologBundle/zipball/9320b6863404c70ebe111e9040dab96f251de7ac", "url": "https://api.github.com/repos/symfony/monolog-bundle/zipball/9320b6863404c70ebe111e9040dab96f251de7ac",
"reference": "9320b6863404c70ebe111e9040dab96f251de7ac", "reference": "9320b6863404c70ebe111e9040dab96f251de7ac",
"shasum": "" "shasum": ""
}, },
@ -2916,12 +3039,12 @@
"version": "v2.3.8", "version": "v2.3.8",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/SwiftmailerBundle.git", "url": "https://github.com/symfony/swiftmailer-bundle.git",
"reference": "970b13d01871207e81d17b17ddda025e7e21e797" "reference": "970b13d01871207e81d17b17ddda025e7e21e797"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/SwiftmailerBundle/zipball/970b13d01871207e81d17b17ddda025e7e21e797", "url": "https://api.github.com/repos/symfony/swiftmailer-bundle/zipball/970b13d01871207e81d17b17ddda025e7e21e797",
"reference": "970b13d01871207e81d17b17ddda025e7e21e797", "reference": "970b13d01871207e81d17b17ddda025e7e21e797",
"shasum": "" "shasum": ""
}, },
@ -2970,20 +3093,20 @@
}, },
{ {
"name": "symfony/symfony", "name": "symfony/symfony",
"version": "v2.7.5", "version": "v2.7.4",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/symfony.git", "url": "https://github.com/symfony/symfony.git",
"reference": "619528a274647cffc1792063c3ea04c4fa8266a0" "reference": "1fdf23fe28876844b887b0e1935c9adda43ee645"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/symfony/zipball/619528a274647cffc1792063c3ea04c4fa8266a0", "url": "https://api.github.com/repos/symfony/symfony/zipball/1fdf23fe28876844b887b0e1935c9adda43ee645",
"reference": "619528a274647cffc1792063c3ea04c4fa8266a0", "reference": "1fdf23fe28876844b887b0e1935c9adda43ee645",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"doctrine/common": "~2.4", "doctrine/common": "~2.3",
"php": ">=5.3.9", "php": ">=5.3.9",
"psr/log": "~1.0", "psr/log": "~1.0",
"twig/twig": "~1.20|~2.0" "twig/twig": "~1.20|~2.0"
@ -3036,9 +3159,9 @@
}, },
"require-dev": { "require-dev": {
"doctrine/data-fixtures": "1.0.*", "doctrine/data-fixtures": "1.0.*",
"doctrine/dbal": "~2.4", "doctrine/dbal": "~2.2",
"doctrine/doctrine-bundle": "~1.2", "doctrine/doctrine-bundle": "~1.2",
"doctrine/orm": "~2.4,>=2.4.5", "doctrine/orm": "~2.2,>=2.2.3",
"egulias/email-validator": "~1.2", "egulias/email-validator": "~1.2",
"ircmaxell/password-compat": "~1.0", "ircmaxell/password-compat": "~1.0",
"monolog/monolog": "~1.11", "monolog/monolog": "~1.11",
@ -3088,7 +3211,7 @@
"keywords": [ "keywords": [
"framework" "framework"
], ],
"time": "2015-09-25 11:16:52" "time": "2015-09-08 14:26:39"
}, },
{ {
"name": "tecnickcom/tcpdf", "name": "tecnickcom/tcpdf",
@ -4488,7 +4611,8 @@
"aliases": [], "aliases": [],
"minimum-stability": "dev", "minimum-stability": "dev",
"stability-flags": { "stability-flags": {
"friendsofsymfony/user-bundle": 20 "friendsofsymfony/user-bundle": 20,
"friendsofsymfony/oauth-server-bundle": 20
}, },
"prefer-stable": true, "prefer-stable": true,
"prefer-lowest": false, "prefer-lowest": false,

View file

@ -2,8 +2,8 @@
namespace Wallabag\ApiBundle\Controller; namespace Wallabag\ApiBundle\Controller;
use FOS\RestBundle\Controller\FOSRestController;
use Nelmio\ApiDocBundle\Annotation\ApiDoc; use Nelmio\ApiDocBundle\Annotation\ApiDoc;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Wallabag\CoreBundle\Entity\Entry; use Wallabag\CoreBundle\Entity\Entry;
@ -11,7 +11,7 @@ use Wallabag\CoreBundle\Entity\Tag;
use Hateoas\Configuration\Route; use Hateoas\Configuration\Route;
use Hateoas\Representation\Factory\PagerfantaFactory; use Hateoas\Representation\Factory\PagerfantaFactory;
class WallabagRestController extends Controller class WallabagRestController extends FOSRestController
{ {
/** /**
* @param Entry $entry * @param Entry $entry
@ -38,31 +38,6 @@ class WallabagRestController extends Controller
} }
} }
/**
* Retrieve salt for a giver user.
*
* @ApiDoc(
* parameters={
* {"name"="username", "dataType"="string", "required"=true, "description"="username"}
* }
* )
*
* @return array
*/
public function getSaltAction($username)
{
$user = $this
->getDoctrine()
->getRepository('WallabagCoreBundle:User')
->findOneByUsername($username);
if (is_null($user)) {
throw $this->createNotFoundException();
}
return array($user->getSalt() ?: null);
}
/** /**
* Retrieve all entries. It could be filtered by many options. * Retrieve all entries. It could be filtered by many options.
* *
@ -122,7 +97,7 @@ class WallabagRestController extends Controller
*/ */
public function getEntryAction(Entry $entry) public function getEntryAction(Entry $entry)
{ {
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId()); $this->validateUserAccess($entry->getUser()->getId());
$json = $this->get('serializer')->serialize($entry, 'json'); $json = $this->get('serializer')->serialize($entry, 'json');
@ -184,7 +159,7 @@ class WallabagRestController extends Controller
*/ */
public function patchEntriesAction(Entry $entry, Request $request) public function patchEntriesAction(Entry $entry, Request $request)
{ {
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId()); $this->validateUserAccess($entry->getUser()->getId());
$title = $request->request->get('title'); $title = $request->request->get('title');
$isArchived = $request->request->get('is_archived'); $isArchived = $request->request->get('is_archived');
@ -228,7 +203,7 @@ class WallabagRestController extends Controller
*/ */
public function deleteEntriesAction(Entry $entry) public function deleteEntriesAction(Entry $entry)
{ {
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId()); $this->validateUserAccess($entry->getUser()->getId());
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$em->remove($entry); $em->remove($entry);
@ -250,7 +225,7 @@ class WallabagRestController extends Controller
*/ */
public function getEntriesTagsAction(Entry $entry) public function getEntriesTagsAction(Entry $entry)
{ {
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId()); $this->validateUserAccess($entry->getUser()->getId());
$json = $this->get('serializer')->serialize($entry->getTags(), 'json'); $json = $this->get('serializer')->serialize($entry->getTags(), 'json');
@ -271,7 +246,7 @@ class WallabagRestController extends Controller
*/ */
public function postEntriesTagsAction(Request $request, Entry $entry) public function postEntriesTagsAction(Request $request, Entry $entry)
{ {
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId()); $this->validateUserAccess($entry->getUser()->getId());
$tags = $request->request->get('tags', ''); $tags = $request->request->get('tags', '');
if (!empty($tags)) { if (!empty($tags)) {
@ -299,7 +274,7 @@ class WallabagRestController extends Controller
*/ */
public function deleteEntriesTagsAction(Entry $entry, Tag $tag) public function deleteEntriesTagsAction(Entry $entry, Tag $tag)
{ {
$this->validateUserAccess($entry->getUser()->getId(), $this->getUser()->getId()); $this->validateUserAccess($entry->getUser()->getId());
$entry->removeTag($tag); $entry->removeTag($tag);
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
@ -334,7 +309,7 @@ class WallabagRestController extends Controller
*/ */
public function deleteTagAction(Tag $tag) public function deleteTagAction(Tag $tag)
{ {
$this->validateUserAccess($tag->getUser()->getId(), $this->getUser()->getId()); $this->validateUserAccess($tag->getUser()->getId());
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$em->remove($tag); $em->remove($tag);
@ -350,12 +325,12 @@ class WallabagRestController extends Controller
* If not, throw exception. It means a user try to access information from an other user. * If not, throw exception. It means a user try to access information from an other user.
* *
* @param int $requestUserId User id from the requested source * @param int $requestUserId User id from the requested source
* @param int $currentUserId User id from the retrieved source
*/ */
private function validateUserAccess($requestUserId, $currentUserId) private function validateUserAccess($requestUserId)
{ {
if ($requestUserId != $currentUserId) { $user = $this->get('security.context')->getToken()->getUser();
throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$requestUserId.', logged user id: '.$currentUserId); if ($requestUserId != $user->getId()) {
throw $this->createAccessDeniedException('Access forbidden. Entry user id: '.$requestUserId.', logged user id: '.$user->getId());
} }
} }

View file

@ -1,40 +0,0 @@
<?php
namespace Wallabag\ApiBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
class WsseFactory implements SecurityFactoryInterface
{
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
$providerId = 'security.authentication.provider.wsse.'.$id;
$container
->setDefinition($providerId, new DefinitionDecorator('wsse.security.authentication.provider'))
->replaceArgument(0, new Reference($userProvider))
;
$listenerId = 'security.authentication.listener.wsse.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('wsse.security.authentication.listener'));
return array($providerId, $listenerId, $defaultEntryPoint);
}
public function getPosition()
{
return 'pre_auth';
}
public function getKey()
{
return 'wsse';
}
public function addConfiguration(NodeDefinition $node)
{
}
}

View file

@ -13,9 +13,6 @@ class WallabagApiExtension extends Extension
{ {
$configuration = new Configuration(); $configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs); $config = $this->processConfiguration($configuration, $configs);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
} }
public function getAlias() public function getAlias()

View file

@ -0,0 +1,31 @@
<?php
namespace Wallabag\ApiBundle\Entity;
use FOS\OAuthServerBundle\Entity\AccessToken as BaseAccessToken;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table("oauth2_access_tokens")
* @ORM\Entity
*/
class AccessToken extends BaseAccessToken
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\ManyToOne(targetEntity="Client")
* @ORM\JoinColumn(nullable=false)
*/
protected $client;
/**
* @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\User")
*/
protected $user;
}

View file

@ -0,0 +1,31 @@
<?php
namespace Wallabag\ApiBundle\Entity;
use FOS\OAuthServerBundle\Entity\AuthCode as BaseAuthCode;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table("oauth2_auth_codes")
* @ORM\Entity
*/
class AuthCode extends BaseAuthCode
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\ManyToOne(targetEntity="Client")
* @ORM\JoinColumn(nullable=false)
*/
protected $client;
/**
* @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\User")
*/
protected $user;
}

View file

@ -0,0 +1,25 @@
<?php
namespace Wallabag\ApiBundle\Entity;
use FOS\OAuthServerBundle\Entity\Client as BaseClient;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table("oauth2_clients")
* @ORM\Entity
*/
class Client extends BaseClient
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
public function __construct()
{
parent::__construct();
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Wallabag\ApiBundle\Entity;
use FOS\OAuthServerBundle\Entity\RefreshToken as BaseRefreshToken;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Table("oauth2_refresh_tokens")
* @ORM\Entity
*/
class RefreshToken extends BaseRefreshToken
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\ManyToOne(targetEntity="Client")
* @ORM\JoinColumn(nullable=false)
*/
protected $client;
/**
* @ORM\ManyToOne(targetEntity="Wallabag\CoreBundle\Entity\User")
*/
protected $user;
}

View file

@ -1,12 +0,0 @@
services:
wsse.security.authentication.provider:
class: Wallabag\ApiBundle\Security\Authentication\Provider\WsseProvider
public: false
arguments: ['', '%kernel.cache_dir%/security/nonces']
wsse.security.authentication.listener:
class: Wallabag\ApiBundle\Security\Firewall\WsseListener
public: false
tags:
- { name: monolog.logger, channel: wsse }
arguments: ['@security.context', '@security.authentication.manager', '@logger']

View file

@ -1,79 +0,0 @@
<?php
namespace Wallabag\ApiBundle\Security\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\Exception\NonceExpiredException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Wallabag\ApiBundle\Security\Authentication\Token\WsseUserToken;
class WsseProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
public function __construct(UserProviderInterface $userProvider, $cacheDir)
{
$this->userProvider = $userProvider;
$this->cacheDir = $cacheDir;
// If cache directory does not exist we create it
if (!is_dir($this->cacheDir)) {
mkdir($this->cacheDir, 0777, true);
}
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
if (!$user) {
throw new AuthenticationException('Bad credentials. Did you forgot your username?');
}
if ($user && $this->validateDigest($token->digest, $token->nonce, $token->created, $user->getPassword())) {
$authenticatedToken = new WsseUserToken($user->getRoles());
$authenticatedToken->setUser($user);
return $authenticatedToken;
}
throw new AuthenticationException('The WSSE authentication failed.');
}
protected function validateDigest($digest, $nonce, $created, $secret)
{
// Check created time is not in the future
if (strtotime($created) > time()) {
throw new AuthenticationException('Back to the future...');
}
// Expire timestamp after 5 minutes
if (time() - strtotime($created) > 300) {
throw new AuthenticationException('Too late for this timestamp... Watch your watch.');
}
// Validate nonce is unique within 5 minutes
if (file_exists($this->cacheDir.'/'.$nonce) && file_get_contents($this->cacheDir.'/'.$nonce) + 300 > time()) {
throw new NonceExpiredException('Previously used nonce detected');
}
file_put_contents($this->cacheDir.'/'.$nonce, time());
// Validate Secret
$expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));
if ($digest !== $expected) {
throw new AuthenticationException('Bad credentials ! Digest is not as expected.');
}
return $digest === $expected;
}
public function supports(TokenInterface $token)
{
return $token instanceof WsseUserToken;
}
}

View file

@ -1,24 +0,0 @@
<?php
namespace Wallabag\ApiBundle\Security\Authentication\Token;
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
class WsseUserToken extends AbstractToken
{
public $created;
public $digest;
public $nonce;
public function __construct(array $roles = array())
{
parent::__construct($roles);
$this->setAuthenticated(count($roles) > 0);
}
public function getCredentials()
{
return '';
}
}

View file

@ -1,62 +0,0 @@
<?php
namespace Wallabag\ApiBundle\Security\Firewall;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Wallabag\ApiBundle\Security\Authentication\Token\WsseUserToken;
use Psr\Log\LoggerInterface;
class WsseListener implements ListenerInterface
{
protected $securityContext;
protected $authenticationManager;
protected $logger;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager, LoggerInterface $logger)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
$this->logger = $logger;
}
public function handle(GetResponseEvent $event)
{
$request = $event->getRequest();
$wsseRegex = '/UsernameToken Username="([^"]+)", PasswordDigest="([^"]+)", Nonce="([^"]+)", Created="([^"]+)"/';
if (!$request->headers->has('x-wsse') || 1 !== preg_match($wsseRegex, $request->headers->get('x-wsse'), $matches)) {
return;
}
$token = new WsseUserToken();
$token->setUser($matches[1]);
$token->digest = $matches[2];
$token->nonce = $matches[3];
$token->created = $matches[4];
try {
$authToken = $this->authenticationManager->authenticate($token);
$this->securityContext->setToken($authToken);
return;
} catch (AuthenticationException $failed) {
$failedMessage = 'WSSE Login failed for '.$token->getUsername().'. Why ? '.$failed->getMessage();
$this->logger->err($failedMessage);
// Deny authentication with a '403 Forbidden' HTTP response
$response = new Response();
$response->setStatusCode(403);
$response->setContent($failedMessage);
$event->setResponse($response);
return;
}
}
}

View file

@ -0,0 +1,46 @@
<?php
namespace Wallabag\ApiBundle\Tests;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\BrowserKit\Cookie;
abstract class AbstractControllerTest extends WebTestCase
{
/**
* @var Client
*/
protected $client = null;
public function setUp()
{
$this->client = $this->createAuthorizedClient();
}
/**
* @return Client
*/
protected function createAuthorizedClient()
{
$client = static::createClient();
$container = $client->getContainer();
$session = $container->get('session');
/** @var $userManager \FOS\UserBundle\Doctrine\UserManager */
$userManager = $container->get('fos_user.user_manager');
/** @var $loginManager \FOS\UserBundle\Security\LoginManager */
$loginManager = $container->get('fos_user.security.login_manager');
$firewallName = $container->getParameter('fos_user.firewall_name');
$user = $userManager->findUserBy(array('username' => 'admin'));
$loginManager->loginUser($firewallName, $user);
// save the login token into the session and put it in a cookie
$container->get('session')->set('_security_'.$firewallName,
serialize($container->get('security.context')->getToken()));
$container->get('session')->save();
$client->getCookieJar()->set(new Cookie($session->getName(), $session->getId()));
return $client;
}
}

View file

@ -2,99 +2,15 @@
namespace Wallabag\ApiBundle\Tests\Controller; namespace Wallabag\ApiBundle\Tests\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Wallabag\ApiBundle\Tests\AbstractControllerTest;
class WallabagRestControllerTest extends WebTestCase class WallabagRestControllerTest extends AbstractControllerTest
{ {
protected static $salt; protected static $salt;
/**
* Grab the salt once and store it to be available for all tests.
*/
public static function setUpBeforeClass()
{
$client = self::createClient();
$user = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:User')
->findOneByUsername('admin');
self::$salt = $user->getSalt();
}
/**
* Generate HTTP headers for authenticate user on API.
*
* @param string $username
* @param string $password
*
* @return array
*/
private function generateHeaders($username, $password)
{
$encryptedPassword = sha1($password.$username.self::$salt);
$nonce = substr(md5(uniqid('nonce_', true)), 0, 16);
$now = new \DateTime('now', new \DateTimeZone('UTC'));
$created = (string) $now->format('Y-m-d\TH:i:s\Z');
$digest = base64_encode(sha1(base64_decode($nonce).$created.$encryptedPassword, true));
return array(
'HTTP_AUTHORIZATION' => 'Authorization profile="UsernameToken"',
'HTTP_x-wsse' => 'X-WSSE: UsernameToken Username="'.$username.'", PasswordDigest="'.$digest.'", Nonce="'.$nonce.'", Created="'.$created.'"',
);
}
public function testGetSalt()
{
$client = $this->createClient();
$client->request('GET', '/api/salts/admin.json');
$user = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:User')
->findOneByUsername('admin');
$this->assertEquals(200, $client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey(0, $content);
$this->assertEquals($user->getSalt(), $content[0]);
$client->request('GET', '/api/salts/notfound.json');
$this->assertEquals(404, $client->getResponse()->getStatusCode());
}
public function testWithBadHeaders()
{
$client = $this->createClient();
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry')
->findOneByIsArchived(false);
if (!$entry) {
$this->markTestSkipped('No content found in db.');
}
$badHeaders = array(
'HTTP_AUTHORIZATION' => 'Authorization profile="UsernameToken"',
'HTTP_x-wsse' => 'X-WSSE: UsernameToken Username="admin", PasswordDigest="Wr0ngDig3st", Nonce="n0Nc3", Created="2015-01-01T13:37:00Z"',
);
$client->request('GET', '/api/entries/'.$entry->getId().'.json', array(), array(), $badHeaders);
$this->assertEquals(403, $client->getResponse()->getStatusCode());
}
public function testGetOneEntry() public function testGetOneEntry()
{ {
$client = $this->createClient(); $entry = $this->client->getContainer()
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry') ->getRepository('WallabagCoreBundle:Entry')
->findOneBy(array('user' => 1, 'isArchived' => false)); ->findOneBy(array('user' => 1, 'isArchived' => false));
@ -103,18 +19,17 @@ class WallabagRestControllerTest extends WebTestCase
$this->markTestSkipped('No content found in db.'); $this->markTestSkipped('No content found in db.');
} }
$client->request('GET', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers); $this->client->request('GET', '/api/entries/'.$entry->getId().'.json');
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true);
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertEquals($entry->getTitle(), $content['title']); $this->assertEquals($entry->getTitle(), $content['title']);
$this->assertEquals($entry->getUrl(), $content['url']); $this->assertEquals($entry->getUrl(), $content['url']);
$this->assertCount(count($entry->getTags()), $content['tags']); $this->assertCount(count($entry->getTags()), $content['tags']);
$this->assertTrue( $this->assertTrue(
$client->getResponse()->headers->contains( $this->client->getResponse()->headers->contains(
'Content-Type', 'Content-Type',
'application/json' 'application/json'
) )
@ -123,10 +38,7 @@ class WallabagRestControllerTest extends WebTestCase
public function testGetOneEntryWrongUser() public function testGetOneEntryWrongUser()
{ {
$client = $this->createClient(); $entry = $this->client->getContainer()
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry') ->getRepository('WallabagCoreBundle:Entry')
->findOneBy(array('user' => 2, 'isArchived' => false)); ->findOneBy(array('user' => 2, 'isArchived' => false));
@ -135,21 +47,18 @@ class WallabagRestControllerTest extends WebTestCase
$this->markTestSkipped('No content found in db.'); $this->markTestSkipped('No content found in db.');
} }
$client->request('GET', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers); $this->client->request('GET', '/api/entries/'.$entry->getId().'.json');
$this->assertEquals(403, $client->getResponse()->getStatusCode()); $this->assertEquals(403, $this->client->getResponse()->getStatusCode());
} }
public function testGetEntries() public function testGetEntries()
{ {
$client = $this->createClient(); $this->client->request('GET', '/api/entries');
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('GET', '/api/entries', array(), array(), $headers); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true);
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThanOrEqual(1, count($content)); $this->assertGreaterThanOrEqual(1, count($content));
$this->assertNotEmpty($content['_embedded']['items']); $this->assertNotEmpty($content['_embedded']['items']);
@ -158,7 +67,7 @@ class WallabagRestControllerTest extends WebTestCase
$this->assertGreaterThanOrEqual(1, $content['pages']); $this->assertGreaterThanOrEqual(1, $content['pages']);
$this->assertTrue( $this->assertTrue(
$client->getResponse()->headers->contains( $this->client->getResponse()->headers->contains(
'Content-Type', 'Content-Type',
'application/json' 'application/json'
) )
@ -167,14 +76,11 @@ class WallabagRestControllerTest extends WebTestCase
public function testGetStarredEntries() public function testGetStarredEntries()
{ {
$client = $this->createClient(); $this->client->request('GET', '/api/entries', array('star' => 1, 'sort' => 'updated'));
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('GET', '/api/entries', array('star' => 1, 'sort' => 'updated'), array(), $headers); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true);
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThanOrEqual(1, count($content)); $this->assertGreaterThanOrEqual(1, count($content));
$this->assertNotEmpty($content['_embedded']['items']); $this->assertNotEmpty($content['_embedded']['items']);
@ -183,7 +89,7 @@ class WallabagRestControllerTest extends WebTestCase
$this->assertGreaterThanOrEqual(1, $content['pages']); $this->assertGreaterThanOrEqual(1, $content['pages']);
$this->assertTrue( $this->assertTrue(
$client->getResponse()->headers->contains( $this->client->getResponse()->headers->contains(
'Content-Type', 'Content-Type',
'application/json' 'application/json'
) )
@ -192,14 +98,11 @@ class WallabagRestControllerTest extends WebTestCase
public function testGetArchiveEntries() public function testGetArchiveEntries()
{ {
$client = $this->createClient(); $this->client->request('GET', '/api/entries', array('archive' => 1));
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('GET', '/api/entries', array('archive' => 1), array(), $headers); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true);
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThanOrEqual(1, count($content)); $this->assertGreaterThanOrEqual(1, count($content));
$this->assertNotEmpty($content['_embedded']['items']); $this->assertNotEmpty($content['_embedded']['items']);
@ -208,7 +111,7 @@ class WallabagRestControllerTest extends WebTestCase
$this->assertGreaterThanOrEqual(1, $content['pages']); $this->assertGreaterThanOrEqual(1, $content['pages']);
$this->assertTrue( $this->assertTrue(
$client->getResponse()->headers->contains( $this->client->getResponse()->headers->contains(
'Content-Type', 'Content-Type',
'application/json' 'application/json'
) )
@ -217,10 +120,7 @@ class WallabagRestControllerTest extends WebTestCase
public function testDeleteEntry() public function testDeleteEntry()
{ {
$client = $this->createClient(); $entry = $this->client->getContainer()
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry') ->getRepository('WallabagCoreBundle:Entry')
->findOneByUser(1); ->findOneByUser(1);
@ -229,36 +129,31 @@ class WallabagRestControllerTest extends WebTestCase
$this->markTestSkipped('No content found in db.'); $this->markTestSkipped('No content found in db.');
} }
$client->request('DELETE', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers); $this->client->request('DELETE', '/api/entries/'.$entry->getId().'.json');
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true); $content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertEquals($entry->getTitle(), $content['title']); $this->assertEquals($entry->getTitle(), $content['title']);
$this->assertEquals($entry->getUrl(), $content['url']); $this->assertEquals($entry->getUrl(), $content['url']);
// We'll try to delete this entry again // We'll try to delete this entry again
$headers = $this->generateHeaders('admin', 'mypassword'); $this->client->request('DELETE', '/api/entries/'.$entry->getId().'.json');
$client->request('DELETE', '/api/entries/'.$entry->getId().'.json', array(), array(), $headers); $this->assertEquals(404, $this->client->getResponse()->getStatusCode());
$this->assertEquals(404, $client->getResponse()->getStatusCode());
} }
public function testPostEntry() public function testPostEntry()
{ {
$client = $this->createClient(); $this->client->request('POST', '/api/entries.json', array(
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('POST', '/api/entries.json', array(
'url' => 'http://www.lemonde.fr/pixels/article/2015/03/28/plongee-dans-l-univers-d-ingress-le-jeu-de-google-aux-frontieres-du-reel_4601155_4408996.html', 'url' => 'http://www.lemonde.fr/pixels/article/2015/03/28/plongee-dans-l-univers-d-ingress-le-jeu-de-google-aux-frontieres-du-reel_4601155_4408996.html',
'tags' => 'google', 'tags' => 'google',
), array(), $headers); ));
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true); $content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertGreaterThan(0, $content['id']); $this->assertGreaterThan(0, $content['id']);
$this->assertEquals('http://www.lemonde.fr/pixels/article/2015/03/28/plongee-dans-l-univers-d-ingress-le-jeu-de-google-aux-frontieres-du-reel_4601155_4408996.html', $content['url']); $this->assertEquals('http://www.lemonde.fr/pixels/article/2015/03/28/plongee-dans-l-univers-d-ingress-le-jeu-de-google-aux-frontieres-du-reel_4601155_4408996.html', $content['url']);
@ -269,10 +164,7 @@ class WallabagRestControllerTest extends WebTestCase
public function testPatchEntry() public function testPatchEntry()
{ {
$client = $this->createClient(); $entry = $this->client->getContainer()
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry') ->getRepository('WallabagCoreBundle:Entry')
->findOneByUser(1); ->findOneByUser(1);
@ -284,16 +176,16 @@ class WallabagRestControllerTest extends WebTestCase
// hydrate the tags relations // hydrate the tags relations
$nbTags = count($entry->getTags()); $nbTags = count($entry->getTags());
$client->request('PATCH', '/api/entries/'.$entry->getId().'.json', array( $this->client->request('PATCH', '/api/entries/'.$entry->getId().'.json', array(
'title' => 'New awesome title', 'title' => 'New awesome title',
'tags' => 'new tag '.uniqid(), 'tags' => 'new tag '.uniqid(),
'star' => true, 'star' => true,
'archive' => false, 'archive' => false,
), array(), $headers); ));
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true); $content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertEquals($entry->getId(), $content['id']); $this->assertEquals($entry->getId(), $content['id']);
$this->assertEquals($entry->getUrl(), $content['url']); $this->assertEquals($entry->getUrl(), $content['url']);
@ -303,10 +195,7 @@ class WallabagRestControllerTest extends WebTestCase
public function testGetTagsEntry() public function testGetTagsEntry()
{ {
$client = $this->createClient(); $entry = $this->client->getContainer()
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry') ->getRepository('WallabagCoreBundle:Entry')
->findOneWithTags(1); ->findOneWithTags(1);
@ -322,17 +211,14 @@ class WallabagRestControllerTest extends WebTestCase
$tags[] = array('id' => $tag->getId(), 'label' => $tag->getLabel()); $tags[] = array('id' => $tag->getId(), 'label' => $tag->getLabel());
} }
$client->request('GET', '/api/entries/'.$entry->getId().'/tags', array(), array(), $headers); $this->client->request('GET', '/api/entries/'.$entry->getId().'/tags');
$this->assertEquals(json_encode($tags, JSON_HEX_QUOT), $client->getResponse()->getContent()); $this->assertEquals(json_encode($tags, JSON_HEX_QUOT), $this->client->getResponse()->getContent());
} }
public function testPostTagsOnEntry() public function testPostTagsOnEntry()
{ {
$client = $this->createClient(); $entry = $this->client->getContainer()
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry') ->getRepository('WallabagCoreBundle:Entry')
->findOneByUser(1); ->findOneByUser(1);
@ -345,16 +231,16 @@ class WallabagRestControllerTest extends WebTestCase
$newTags = 'tag1,tag2,tag3'; $newTags = 'tag1,tag2,tag3';
$client->request('POST', '/api/entries/'.$entry->getId().'/tags', array('tags' => $newTags), array(), $headers); $this->client->request('POST', '/api/entries/'.$entry->getId().'/tags', array('tags' => $newTags));
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true); $content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertArrayHasKey('tags', $content); $this->assertArrayHasKey('tags', $content);
$this->assertEquals($nbTags + 3, count($content['tags'])); $this->assertEquals($nbTags + 3, count($content['tags']));
$entryDB = $client->getContainer() $entryDB = $this->client->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry') ->getRepository('WallabagCoreBundle:Entry')
->find($entry->getId()); ->find($entry->getId());
@ -369,15 +255,13 @@ class WallabagRestControllerTest extends WebTestCase
} }
} }
public function testDeleteOneTagEntrie() public function testDeleteOneTagEntry()
{ {
$client = $this->createClient(); $entry = $this->client->getContainer()
$headers = $this->generateHeaders('admin', 'mypassword');
$entry = $client->getContainer()
->get('doctrine.orm.entity_manager') ->get('doctrine.orm.entity_manager')
->getRepository('WallabagCoreBundle:Entry') ->getRepository('WallabagCoreBundle:Entry')
->findOneByUser(1); ->findOneWithTags(1);
$entry = $entry[0];
if (!$entry) { if (!$entry) {
$this->markTestSkipped('No content found in db.'); $this->markTestSkipped('No content found in db.');
@ -387,11 +271,11 @@ class WallabagRestControllerTest extends WebTestCase
$nbTags = count($entry->getTags()); $nbTags = count($entry->getTags());
$tag = $entry->getTags()[0]; $tag = $entry->getTags()[0];
$client->request('DELETE', '/api/entries/'.$entry->getId().'/tags/'.$tag->getId().'.json', array(), array(), $headers); $this->client->request('DELETE', '/api/entries/'.$entry->getId().'/tags/'.$tag->getId().'.json');
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$content = json_decode($client->getResponse()->getContent(), true); $content = json_decode($this->client->getResponse()->getContent(), true);
$this->assertArrayHasKey('tags', $content); $this->assertArrayHasKey('tags', $content);
$this->assertEquals($nbTags - 1, count($content['tags'])); $this->assertEquals($nbTags - 1, count($content['tags']));
@ -399,14 +283,11 @@ class WallabagRestControllerTest extends WebTestCase
public function testGetUserTags() public function testGetUserTags()
{ {
$client = $this->createClient(); $this->client->request('GET', '/api/tags.json');
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('GET', '/api/tags.json', array(), array(), $headers); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true);
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertGreaterThan(0, $content); $this->assertGreaterThan(0, $content);
$this->assertArrayHasKey('id', $content[0]); $this->assertArrayHasKey('id', $content[0]);
@ -420,14 +301,11 @@ class WallabagRestControllerTest extends WebTestCase
*/ */
public function testDeleteUserTag($tag) public function testDeleteUserTag($tag)
{ {
$client = $this->createClient(); $this->client->request('DELETE', '/api/tags/'.$tag['id'].'.json');
$headers = $this->generateHeaders('admin', 'mypassword');
$client->request('DELETE', '/api/tags/'.$tag['id'].'.json', array(), array(), $headers); $this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$this->assertEquals(200, $client->getResponse()->getStatusCode()); $content = json_decode($this->client->getResponse()->getContent(), true);
$content = json_decode($client->getResponse()->getContent(), true);
$this->assertArrayHasKey('label', $content); $this->assertArrayHasKey('label', $content);
$this->assertEquals($tag['label'], $content['label']); $this->assertEquals($tag['label'], $content['label']);

View file

@ -3,16 +3,7 @@
namespace Wallabag\ApiBundle; namespace Wallabag\ApiBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Bundle\Bundle;
use Wallabag\ApiBundle\DependencyInjection\Security\Factory\WsseFactory;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class WallabagApiBundle extends Bundle class WallabagApiBundle extends Bundle
{ {
public function build(ContainerBuilder $container)
{
parent::build($container);
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new WsseFactory());
}
} }

View file

@ -25,6 +25,7 @@ class ConfigController extends Controller
{ {
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$config = $this->getConfig(); $config = $this->getConfig();
$userManager = $this->container->get('fos_user.user_manager');
$user = $this->getUser(); $user = $this->getUser();
// handle basic config detail (this form is defined as a service) // handle basic config detail (this form is defined as a service)
@ -52,9 +53,8 @@ class ConfigController extends Controller
$pwdForm->handleRequest($request); $pwdForm->handleRequest($request);
if ($pwdForm->isValid()) { if ($pwdForm->isValid()) {
$user->setPassword($pwdForm->get('new_password')->getData()); $user->setPlainPassword($pwdForm->get('new_password')->getData());
$em->persist($user); $userManager->updateUser($user, true);
$em->flush();
$this->get('session')->getFlashBag()->add( $this->get('session')->getFlashBag()->add(
'notice', 'notice',
@ -69,8 +69,7 @@ class ConfigController extends Controller
$userForm->handleRequest($request); $userForm->handleRequest($request);
if ($userForm->isValid()) { if ($userForm->isValid()) {
$em->persist($user); $userManager->updateUser($user, true);
$em->flush();
$this->get('session')->getFlashBag()->add( $this->get('session')->getFlashBag()->add(
'notice', 'notice',
@ -97,14 +96,14 @@ class ConfigController extends Controller
} }
// handle adding new user // handle adding new user
$newUser = new User(); $newUser = $userManager->createUser();
// enable created user by default // enable created user by default
$newUser->setEnabled(true); $newUser->setEnabled(true);
$newUserForm = $this->createForm(new NewUserType(), $newUser, array('validation_groups' => array('Profile'))); $newUserForm = $this->createForm(new NewUserType(), $newUser, array('validation_groups' => array('Profile')));
$newUserForm->handleRequest($request); $newUserForm->handleRequest($request);
if ($newUserForm->isValid()) { if ($newUserForm->isValid() && $this->get('security.authorization_checker')->isGranted('ROLE_SUPER_ADMIN')) {
$em->persist($newUser); $userManager->updateUser($newUser, true);
$config = new Config($newUser); $config = new Config($newUser);
$config->setTheme($this->container->getParameter('theme')); $config->setTheme($this->container->getParameter('theme'));

View file

@ -18,8 +18,9 @@ class LoadUserData extends AbstractFixture implements OrderedFixtureInterface
$userAdmin->setName('Big boss'); $userAdmin->setName('Big boss');
$userAdmin->setEmail('bigboss@wallabag.org'); $userAdmin->setEmail('bigboss@wallabag.org');
$userAdmin->setUsername('admin'); $userAdmin->setUsername('admin');
$userAdmin->setPassword('mypassword'); $userAdmin->setPlainPassword('mypassword');
$userAdmin->setEnabled(true); $userAdmin->setEnabled(true);
$userAdmin->addRole('ROLE_SUPER_ADMIN');
$manager->persist($userAdmin); $manager->persist($userAdmin);
@ -29,7 +30,7 @@ class LoadUserData extends AbstractFixture implements OrderedFixtureInterface
$bobUser->setName('Bobby'); $bobUser->setName('Bobby');
$bobUser->setEmail('bobby@wallabag.org'); $bobUser->setEmail('bobby@wallabag.org');
$bobUser->setUsername('bob'); $bobUser->setUsername('bob');
$bobUser->setPassword('mypassword'); $bobUser->setPlainPassword('mypassword');
$bobUser->setEnabled(true); $bobUser->setEnabled(true);
$manager->persist($bobUser); $manager->persist($bobUser);

View file

@ -6,7 +6,6 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
use JMS\Serializer\Annotation\ExclusionPolicy; use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose; use JMS\Serializer\Annotation\Expose;
use FOS\UserBundle\Model\User as BaseUser; use FOS\UserBundle\Model\User as BaseUser;
@ -22,7 +21,7 @@ use FOS\UserBundle\Model\User as BaseUser;
* @UniqueEntity("email") * @UniqueEntity("email")
* @UniqueEntity("username") * @UniqueEntity("username")
*/ */
class User extends BaseUser implements AdvancedUserInterface, \Serializable class User extends BaseUser
{ {
/** /**
* @var int * @var int
@ -75,6 +74,7 @@ class User extends BaseUser implements AdvancedUserInterface, \Serializable
parent::__construct(); parent::__construct();
$this->entries = new ArrayCollection(); $this->entries = new ArrayCollection();
$this->tags = new ArrayCollection(); $this->tags = new ArrayCollection();
$this->roles = array('ROLE_USER');
} }
/** /**
@ -90,24 +90,6 @@ class User extends BaseUser implements AdvancedUserInterface, \Serializable
$this->updatedAt = new \DateTime(); $this->updatedAt = new \DateTime();
} }
/**
* Set password.
*
* @param string $password
*
* @return User
*/
public function setPassword($password)
{
if (!$password && 0 === strlen($password)) {
return;
}
$this->password = sha1($password.$this->getUsername().$this->getSalt());
return $this;
}
/** /**
* Set name. * Set name.
* *

View file

@ -0,0 +1,44 @@
<?php
namespace Wallabag\CoreBundle\EventListener;
use FOS\UserBundle\FOSUserEvents;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use FOS\UserBundle\Event\FilterUserResponseEvent;
use Wallabag\CoreBundle\Entity\Config;
class AuthenticationListener implements EventSubscriberInterface
{
private $em;
private $container;
public function __construct(Container $container, $em)
{
$this->container = $container;
$this->em = $em;
}
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_CONFIRMED => 'authenticate',
);
}
public function authenticate(FilterUserResponseEvent $event, $eventName = null, EventDispatcherInterface $eventDispatcher = null)
{
if (!$event->getUser()->isEnabled()) {
return;
}
$config = new Config($event->getUser());
$config->setTheme($this->container->getParameter('theme'));
$config->setItemsPerPage($this->container->getParameter('items_on_page'));
$config->setRssLimit($this->container->getParameter('rss_limit'));
$config->setLanguage($this->container->getParameter('language'));
$this->em->persist($config);
$this->em->flush();
}
}

View file

@ -13,7 +13,8 @@ class NewUserType extends AbstractType
{ {
$builder $builder
->add('username', 'text', array('required' => true)) ->add('username', 'text', array('required' => true))
->add('password', 'password', array( ->add('plainPassword', 'repeated', array(
'type' => 'password',
'constraints' => array( 'constraints' => array(
new Constraints\Length(array( new Constraints\Length(array(
'min' => 8, 'min' => 8,

View file

@ -0,0 +1,24 @@
<?php
namespace Wallabag\CoreBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class RegistrationType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name');
}
public function getParent()
{
return 'fos_user_registration';
}
public function getName()
{
return 'wallabag_user_registration';
}
}

View file

@ -13,6 +13,11 @@ services:
tags: tags:
- { name: form.type, alias: config } - { name: form.type, alias: config }
wallabag_core.form.registration:
class: Wallabag\CoreBundle\Form\Type\RegistrationType
tags:
- { name: form.type, alias: wallabag_user_registration }
wallabag_core.form.type.forgot_password: wallabag_core.form.type.forgot_password:
class: Wallabag\CoreBundle\Form\Type\ForgotPasswordType class: Wallabag\CoreBundle\Form\Type\ForgotPasswordType
arguments: arguments:
@ -40,3 +45,9 @@ services:
class: Wallabag\CoreBundle\Helper\ContentProxy class: Wallabag\CoreBundle\Helper\ContentProxy
arguments: arguments:
- @wallabag_core.graby - @wallabag_core.graby
wallabag_core.registration_confirmed:
class: Wallabag\CoreBundle\EventListener\AuthenticationListener
arguments: [@service_container, @doctrine.orm.entity_manager]
tags:
- { name: kernel.event_subscriber }

View file

@ -135,6 +135,7 @@
{{ form_rest(form.pwd) }} {{ form_rest(form.pwd) }}
</form> </form>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<h2>{% trans %}Add a user{% endtrans %}</h2> <h2>{% trans %}Add a user{% endtrans %}</h2>
<form action="{{ path('config') }}" method="post" {{ form_enctype(form.new_user) }}> <form action="{{ path('config') }}" method="post" {{ form_enctype(form.new_user) }}>
@ -150,9 +151,17 @@
<fieldset class="w500p inline"> <fieldset class="w500p inline">
<div class="row"> <div class="row">
{{ form_label(form.new_user.password) }} {{ form_label(form.new_user.plainPassword.first) }}
{{ form_errors(form.new_user.password) }} {{ form_errors(form.new_user.plainPassword.first) }}
{{ form_widget(form.new_user.password) }} {{ form_widget(form.new_user.plainPassword.first) }}
</div>
</fieldset>
<fieldset class="w500p inline">
<div class="row">
{{ form_label(form.new_user.plainPassword.second) }}
{{ form_errors(form.new_user.plainPassword.second) }}
{{ form_widget(form.new_user.plainPassword.second) }}
</div> </div>
</fieldset> </fieldset>
@ -165,5 +174,6 @@
</fieldset> </fieldset>
{{ form_rest(form.new_user) }} {{ form_rest(form.new_user) }}
{% endif %}
</form> </form>
{% endblock %} {% endblock %}

View file

@ -15,7 +15,9 @@
<li class="tab col s3"><a href="#set2">{% trans %}RSS{% endtrans %}</a></li> <li class="tab col s3"><a href="#set2">{% trans %}RSS{% endtrans %}</a></li>
<li class="tab col s3"><a href="#set3">{% trans %}User information{% endtrans %}</a></li> <li class="tab col s3"><a href="#set3">{% trans %}User information{% endtrans %}</a></li>
<li class="tab col s3"><a href="#set4">{% trans %}Password{% endtrans %}</a></li> <li class="tab col s3"><a href="#set4">{% trans %}Password{% endtrans %}</a></li>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<li class="tab col s3"><a href="#set5">{% trans %}Add a user{% endtrans %}</a></li> <li class="tab col s3"><a href="#set5">{% trans %}Add a user{% endtrans %}</a></li>
{% endif %}
</ul> </ul>
</div> </div>
@ -175,7 +177,7 @@
</form> </form>
</div> </div>
{% if is_granted('ROLE_SUPER_ADMIN') %}
<div id="set5" class="col s12"> <div id="set5" class="col s12">
<form action="{{ path('config') }}#set5" method="post" {{ form_enctype(form.new_user) }}> <form action="{{ path('config') }}#set5" method="post" {{ form_enctype(form.new_user) }}>
{{ form_errors(form.new_user) }} {{ form_errors(form.new_user) }}
@ -190,9 +192,17 @@
<div class="row"> <div class="row">
<div class="input-field col s12"> <div class="input-field col s12">
{{ form_label(form.new_user.password) }} {{ form_label(form.new_user.plainPassword.first) }}
{{ form_errors(form.new_user.password) }} {{ form_errors(form.new_user.plainPassword.first) }}
{{ form_widget(form.new_user.password) }} {{ form_widget(form.new_user.plainPassword.first) }}
</div>
</div>
<div class="row">
<div class="input-field col s12">
{{ form_label(form.new_user.plainPassword.second) }}
{{ form_errors(form.new_user.plainPassword.second) }}
{{ form_widget(form.new_user.plainPassword.second) }}
</div> </div>
</div> </div>
@ -211,6 +221,7 @@
</form> </form>
</div> </div>
{% endif %}
</div> </div>
</div> </div>

View file

@ -49,6 +49,7 @@
{% trans %}Login{% endtrans %} {% trans %}Login{% endtrans %}
<i class="mdi-content-send right"></i> <i class="mdi-content-send right"></i>
</button> </button>
<a href="{{ path('fos_user_registration_register') }}">{% trans %}Register{% endtrans %}</a>
</div> </div>
</form> </form>
</div> </div>

View file

@ -258,7 +258,8 @@ class ConfigControllerTest extends WallabagCoreTestCase
array( array(
array( array(
'new_user[username]' => '', 'new_user[username]' => '',
'new_user[password]' => '', 'new_user[plainPassword][first]' => '',
'new_user[plainPassword][second]' => '',
'new_user[email]' => '', 'new_user[email]' => '',
), ),
'Please enter a username', 'Please enter a username',
@ -266,7 +267,8 @@ class ConfigControllerTest extends WallabagCoreTestCase
array( array(
array( array(
'new_user[username]' => 'a', 'new_user[username]' => 'a',
'new_user[password]' => 'mypassword', 'new_user[plainPassword][first]' => 'mypassword',
'new_user[plainPassword][second]' => 'mypassword',
'new_user[email]' => '', 'new_user[email]' => '',
), ),
'The username is too short', 'The username is too short',
@ -274,7 +276,8 @@ class ConfigControllerTest extends WallabagCoreTestCase
array( array(
array( array(
'new_user[username]' => 'wallace', 'new_user[username]' => 'wallace',
'new_user[password]' => 'mypassword', 'new_user[plainPassword][first]' => 'mypassword',
'new_user[plainPassword][second]' => 'mypassword',
'new_user[email]' => 'test', 'new_user[email]' => 'test',
), ),
'The email is not valid', 'The email is not valid',
@ -282,11 +285,21 @@ class ConfigControllerTest extends WallabagCoreTestCase
array( array(
array( array(
'new_user[username]' => 'admin', 'new_user[username]' => 'admin',
'new_user[password]' => 'wallacewallace', 'new_user[plainPassword][first]' => 'wallacewallace',
'new_user[plainPassword][second]' => 'wallacewallace',
'new_user[email]' => 'wallace@wallace.me', 'new_user[email]' => 'wallace@wallace.me',
), ),
'The username is already used', 'The username is already used',
), ),
array(
array(
'new_user[username]' => 'wallace',
'new_user[plainPassword][first]' => 'mypassword1',
'new_user[plainPassword][second]' => 'mypassword2',
'new_user[email]' => 'wallace@wallace.me',
),
'This value is not valid',
),
); );
} }
@ -325,7 +338,8 @@ class ConfigControllerTest extends WallabagCoreTestCase
$data = array( $data = array(
'new_user[username]' => 'wallace', 'new_user[username]' => 'wallace',
'new_user[password]' => 'wallace1', 'new_user[plainPassword][first]' => 'wallace1',
'new_user[plainPassword][second]' => 'wallace1',
'new_user[email]' => 'wallace@wallace.me', 'new_user[email]' => 'wallace@wallace.me',
); );