first implementation of security

This commit is contained in:
Nicolas Lœuillet 2015-01-31 15:14:10 +01:00
parent 71691fe44a
commit c3235553dd
18 changed files with 469 additions and 69 deletions

View file

@ -10,6 +10,14 @@ doc-api:
resource: "@NelmioApiDocBundle/Resources/config/routing.yml" resource: "@NelmioApiDocBundle/Resources/config/routing.yml"
prefix: /api/doc prefix: /api/doc
login:
pattern: /login
defaults: { _controller: WallabagCoreBundle:Security:login }
login_check:
pattern: /login_check
logout:
path: /logout
#wallabag_api: #wallabag_api:
# resource: "@WallabagApiBundle/Controller/" # resource: "@WallabagApiBundle/Controller/"
# type: annotation # type: annotation

View file

@ -1,52 +1,58 @@
# you can read more about security in the related section of the documentation
# http://symfony.com/doc/current/book/security.html
security: security:
# http://symfony.com/doc/current/book/security.html#encoding-the-user-s-password
encoders: encoders:
Symfony\Component\Security\Core\User\User: plaintext Wallabag\CoreBundle\Entity\Users:
algorithm: sha1
encode_as_base64: false
iterations: 1
# http://symfony.com/doc/current/book/security.html#hierarchical-roles
role_hierarchy: role_hierarchy:
ROLE_ADMIN: ROLE_USER ROLE_ADMIN: ROLE_USER
ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] ROLE_SUPER_ADMIN: [ ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH ]
# http://symfony.com/doc/current/book/security.html#where-do-users-come-from-user-providers
providers: providers:
in_memory: administrators:
memory: entity: { class: WallabagCoreBundle:Users, property: username }
users:
user: { password: userpass, roles: [ 'ROLE_USER' ] }
admin: { password: adminpass, roles: [ 'ROLE_ADMIN' ] }
# 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:
# disables authentication for assets and the profiler, adapt it according to your needs #wsse_secured:
dev: # pattern: /api/.*
pattern: ^/(_(profiler|wdt)|css|images|js)/ # wsse: true
security: false login_firewall:
# the login page has to be accessible for everybody pattern: ^/login$
demo_login: anonymous: ~
pattern: ^/demo/secured/login$
security: false
# secures part of the application secured_area:
demo_secured_area: pattern: ^/
pattern: ^/demo/secured/ anonymous: ~
# it's important to notice that in this case _demo_security_check and _demo_login
# are route names and that they are specified in the AcmeDemoBundle
form_login: form_login:
check_path: _demo_security_check login_path: /login
login_path: _demo_login
logout: use_forward: false
path: _demo_logout
target: _demo check_path: /login_check
#anonymous: ~
#http_basic: post_only: true
# realm: "Secured Demo Area"
always_use_default_target_path: true
default_target_path: /
target_path_parameter: redirect_url
use_referer: true
failure_path: null
failure_forward: false
username_parameter: _username
password_parameter: _password
csrf_parameter: _csrf_token
intention: authenticate
logout:
path: /logout
target: /
# with these settings you can restrict or allow access for different parts
# of your application based on roles, ip, host or methods
# http://symfony.com/doc/current/cookbook/security/access_control.html
access_control: access_control:
#- { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https } - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
- { path: ^/, roles: ROLE_USER }

View file

@ -0,0 +1,27 @@
<?php
namespace Wallabag\CoreBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\SecurityContext;
class SecurityController extends Controller
{
public function loginAction(Request $request)
{
$session = $request->getSession();
// get the login error if there is one
if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
$error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
} else {
$error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
$session->remove(SecurityContext::AUTHENTICATION_ERROR);
}
return $this->render('WallabagCoreBundle:Security:login.html.twig', array(
// last username entered by the user
'last_username' => $session->get(SecurityContext::LAST_USERNAME),
'error' => $error,
));
}
}

View file

@ -82,17 +82,18 @@ class WallabagRestController extends Controller
*/ */
public function postEntriesAction(Request $request) public function postEntriesAction(Request $request)
{ {
//TODO la récup ne marche //TODO la récup ne marche pas
//TODO gérer si on passe le titre //TODO gérer si on passe le titre
//TODO gérer si on passe les tags //TODO gérer si on passe les tags
//TODO ne pas avoir du code comme ça qui doit se trouver dans le Repository //TODO ne pas avoir du code comme ça qui doit se trouver dans le Repository
$url = $request->request->get('url');
$content = Extractor::extract($url);
$entry = new Entries(); $entry = new Entries();
$entry->setUserId(1); $entry->setUserId(1);
$content = Extractor::extract($request->request->get('url')); $entry->setUrl($url);
$entry->setTitle($content->getTitle()); $entry->setTitle($content->getTitle());
$entry->setContent($content->getBody()); $entry->setContent($content->getBody());
$em = $this->getDoctrine()->getManager(); $em = $this->getDoctrine()->getManager();
$em->persist($entry); $em->persist($entry);
$em->flush(); $em->flush();

View file

@ -0,0 +1,40 @@
<?php
namespace Wallabag\CoreBundle\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

@ -10,6 +10,7 @@ use Symfony\Component\Validator\Constraints as Assert;
* *
* @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\EntriesRepository") * @ORM\Entity(repositoryClass="Wallabag\CoreBundle\Repository\EntriesRepository")
* @ORM\Table(name="entries") * @ORM\Table(name="entries")
*
*/ */
class Entries class Entries
{ {

View file

@ -3,6 +3,9 @@
namespace Wallabag\CoreBundle\Entity; namespace Wallabag\CoreBundle\Entity;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
/** /**
* Users * Users
@ -10,7 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Table(name="users") * @ORM\Table(name="users")
* @ORM\Entity * @ORM\Entity
*/ */
class Users class Users implements AdvancedUserInterface, \Serializable
{ {
/** /**
* @var integer * @var integer
@ -28,6 +31,11 @@ class Users
*/ */
private $username; private $username;
/**
* @ORM\Column(type="string", length=32)
*/
private $salt;
/** /**
* @var string * @var string
* *
@ -49,7 +57,16 @@ class Users
*/ */
private $email; private $email;
/**
* @ORM\Column(name="is_active", type="boolean")
*/
private $isActive;
public function __construct()
{
$this->isActive = true;
$this->salt = md5(uniqid(null, true));
}
/** /**
* Get id * Get id
@ -84,6 +101,22 @@ class Users
return $this->username; return $this->username;
} }
/**
* @inheritDoc
*/
public function getSalt()
{
return $this->salt;
}
/**
* @inheritDoc
*/
public function getRoles()
{
return array('ROLE_USER');
}
/** /**
* Set password * Set password
* *
@ -152,4 +185,56 @@ class Users
{ {
return $this->email; return $this->email;
} }
/**
* @inheritDoc
*/
public function eraseCredentials()
{
}
/**
* @see \Serializable::serialize()
*/
public function serialize()
{
return serialize(array(
$this->id,
));
}
/**
* @see \Serializable::unserialize()
*/
public function unserialize($serialized)
{
list (
$this->id,
) = unserialize($serialized);
}
public function isEqualTo(UserInterface $user)
{
return $this->username === $user->getUsername();
}
public function isAccountNonExpired()
{
return true;
}
public function isAccountNonLocked()
{
return true;
}
public function isCredentialsNonExpired()
{
return true;
}
public function isEnabled()
{
return $this->isActive;
}
} }

View file

@ -0,0 +1,10 @@
<?php
namespace Wallabag\CoreBundle\Helper;
class Entries {
}

View file

@ -5,6 +5,8 @@ namespace Wallabag\CoreBundle\Repository;
use Doctrine\ORM\Query; use Doctrine\ORM\Query;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Tools\Pagination\Paginator; use Doctrine\ORM\Tools\Pagination\Paginator;
use Wallabag\CoreBundle\Entity\Entries;
use Wallabag\CoreBundle\Service\Extractor;
class EntriesRepository extends EntityRepository class EntriesRepository extends EntityRepository
{ {
@ -79,6 +81,7 @@ class EntriesRepository extends EntityRepository
public function findEntries($userId, $isArchived, $isStarred, $isDeleted, $sort, $order) public function findEntries($userId, $isArchived, $isStarred, $isDeleted, $sort, $order)
{ {
//TODO tous les paramètres ne sont pas utilisés, à corriger
$qb = $this->createQueryBuilder('e') $qb = $this->createQueryBuilder('e')
->select('e') ->select('e')
->where('e.isFav =:isStarred')->setParameter('isStarred', $isStarred) ->where('e.isFav =:isStarred')->setParameter('isStarred', $isStarred)

View file

@ -5,13 +5,25 @@
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services> <services>
<!-- Twig -->
<service id="wallabag_core.twig.wallabag" class="Wallabag\CoreBundle\Twig\Extension\WallabagExtension"> <service id="wallabag_core.twig.wallabag" class="Wallabag\CoreBundle\Twig\Extension\WallabagExtension">
<tag name="twig.extension" /> <tag name="twig.extension" />
</service> </service>
<!-- Security -->
<service id="wsse.security.authentication.provider"
class="Wallabag\CoreBundle\Security\Authentication\Provider\WsseProvider" public="false">
<argument /> <!-- User Provider -->
<argument>%kernel.cache_dir%/security/nonces</argument>
</service>
<service id="wsse.security.authentication.listener"
class="Wallabag\CoreBundle\Security\Firewall\WsseListener" public="false">
<argument type="service" id="security.context"/>
<argument type="service" id="security.authentication.manager" />
</service>
</services> </services>
</container> </container>

View file

@ -0,0 +1,32 @@
{% extends "WallabagCoreBundle::layout-login.html.twig" %}
{% block title %}{% trans %}login to your wallabag{% endtrans %}{% endblock %}
{% block content %}
{% if error %}
<div>{{ error.message }}</div>
{% endif %}
<form action="{{ path('login_check') }}" method="post" name="loginform">
<fieldset class="w500p center">
<h2 class="mbs txtcenter">{% trans %}Login to wallabag{% endtrans %}</h2>
<div class="row">
<label class="col w150p" for="username">{% trans %}Username{% endtrans %}</label>
<input type="text" id="username" name="_username" value="{{ last_username }}" />
</div>
<div class="row">
<label class="col w150p" for="password">{% trans %}Password{% endtrans %}</label>
<input type="password" id="password" name="_password" />
</div>
{#
Si vous voulez contrôler l'URL vers laquelle l'utilisateur est redirigé en cas de succès
(plus de détails ci-dessous)
<input type="hidden" name="_target_path" value="/account" />
#}
<div class="row mts txtcenter">
<button type="submit">login</button>
</div>
</fieldset>
</form>
{% endblock %}

View file

@ -10,6 +10,6 @@
</li> </li>
<li><a href="?view=config">{% trans %}config{% endtrans %}</a></li> <li><a href="?view=config">{% trans %}config{% endtrans %}</a></li>
<li><a href={{ path('about') }}>{% trans %}about{% endtrans %}</a></li> <li><a href={{ path('about') }}>{% trans %}about{% endtrans %}</a></li>
<li><a class="icon icon-power" href="?logout" title="{% trans %}logout{% endtrans %}">{% trans %}logout{% endtrans %}</a></li> <li><a class="icon icon-power" href="{{ path('logout') }}" title="{% trans %}logout{% endtrans %}">{% trans %}logout{% endtrans %}</a></li>
</ul> </ul>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<!--[if lte IE 6]><html class="no-js ie6 ie67 ie678" lang="en"><![endif]-->
<!--[if lte IE 7]><html class="no-js ie7 ie67 ie678" lang="en"><![endif]-->
<!--[if IE 8]><html class="no-js ie8 ie678" lang="en"><![endif]-->
<!--[if gt IE 8]><html class="no-js" lang="en"><![endif]-->
<html lang="en">
<head>
<meta name="viewport" content="initial-scale=1.0">
<meta charset="utf-8">
<!--[if IE]>
<meta http-equiv="X-UA-Compatible" content="IE=10">
<![endif]-->
<title>{% block title %}{% endblock %} - wallabag</title>
{% include "WallabagCoreBundle::_head.html.twig" %}
</head>
<body class="login">
{% include "WallabagCoreBundle::_top.html.twig" %}
<div id="main">
{% block menu %}{% endblock %}
<div id="content" class="w600p center">
{% block content %}{% endblock %}
</div>
</div>
{% include "WallabagCoreBundle::_footer.html.twig" %}
</body>
</html>

View file

@ -4,30 +4,30 @@
<!--[if IE 8]><html class="no-js ie8 ie678" lang="en"><![endif]--> <!--[if IE 8]><html class="no-js ie8 ie678" lang="en"><![endif]-->
<!--[if gt IE 8]><html class="no-js" lang="en"><![endif]--> <!--[if gt IE 8]><html class="no-js" lang="en"><![endif]-->
<html lang="en"> <html lang="en">
<head> <head>
<meta name="viewport" content="initial-scale=1.0"> <meta name="viewport" content="initial-scale=1.0">
<meta charset="utf-8"> <meta charset="utf-8">
<!--[if IE]> <!--[if IE]>
<meta http-equiv="X-UA-Compatible" content="IE=10"> <meta http-equiv="X-UA-Compatible" content="IE=10">
<![endif]--> <![endif]-->
<title>{% block title %}{% endblock %} - wallabag</title> <title>{% block title %}{% endblock %} - wallabag</title>
{% include "WallabagCoreBundle::_head.html.twig" %} {% include "WallabagCoreBundle::_head.html.twig" %}
{% include "WallabagCoreBundle::_bookmarklet.html.twig" %} {% include "WallabagCoreBundle::_bookmarklet.html.twig" %}
</head> </head>
<body> <body>
{% include "WallabagCoreBundle::_top.html.twig" %} {% include "WallabagCoreBundle::_top.html.twig" %}
<div id="main"> <div id="main">
{% block menu %}{% endblock %} {% block menu %}{% endblock %}
{% block precontent %}{% endblock %} {% block precontent %}{% endblock %}
{% for flashMessage in app.session.flashbag.get('notice') %} {% for flashMessage in app.session.flashbag.get('notice') %}
<div class="flash-notice"> <div class="flash-notice">
{{ flashMessage }} {{ flashMessage }}
</div>
{% endfor %}
<div id="content" class="w600p center">
{% block content %}{% endblock %}
</div> </div>
{% endfor %}
<div id="content" class="w600p center">
{% block content %}{% endblock %}
</div> </div>
</div> {% include "WallabagCoreBundle::_footer.html.twig" %}
{% include "WallabagCoreBundle::_footer.html.twig" %} </body>
</body>
</html> </html>

View file

@ -0,0 +1,59 @@
<?php
namespace Wallabag\CoreBundle\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\CoreBundle\Security\Authentication\Token\WsseUserToken;
class WsseProvider implements AuthenticationProviderInterface
{
private $userProvider;
private $cacheDir;
public function __construct(UserProviderInterface $userProvider, $cacheDir)
{
$this->userProvider = $userProvider;
$this->cacheDir = $cacheDir;
}
public function authenticate(TokenInterface $token)
{
$user = $this->userProvider->loadUserByUsername($token->getUsername());
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)
{
// Expire le timestamp après 5 minutes
if (time() - strtotime($created) > 300) {
return false;
}
// Valide que le nonce est unique dans les 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());
// Valide le Secret
$expected = base64_encode(sha1(base64_decode($nonce).$created.$secret, true));
return $digest === $expected;
}
public function supports(TokenInterface $token)
{
return $token instanceof WsseUserToken;
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Wallabag\CoreBundle\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

@ -0,0 +1,58 @@
<?php
namespace Wallabag\CoreBundle\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\CoreBundle\Security\Authentication\Token\WsseUserToken;
class WsseListener implements ListenerInterface
{
protected $securityContext;
protected $authenticationManager;
public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager)
{
$this->securityContext = $securityContext;
$this->authenticationManager = $authenticationManager;
}
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);
} catch (AuthenticationException $failed) {
// ... you might log something here
// To deny the authentication clear the token. This will redirect to the login page.
// $this->securityContext->setToken(null);
// return;
// Deny authentication with a '403 Forbidden' HTTP response
$response = new Response();
$response->setStatusCode(403);
$event->setResponse($response);
}
}
}

View file

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