Symfony: Provide message for security voter that denies access? - symfony

I'm working on security for an API. Some things that I check on each request are:
Is the user's IP address whitelisted for access
Is the user's account expired
Is the user's rate limit exceeded for the day?
It seems like I should use a security Voter, perhaps one for each of these things, and return VoterInterface::ACCESS_DENIED when an access check fails.
However, I want to provide a message to the user in the API response that provides some indication as to WHY their request was denied. I cannot do this with a security voter.
My current workaround is to listen to the kernel controller event, perform my access checks, and then throw an explicit AccessDeniedException with my specific message if the check fails.
Is that a good way to handle this shortcoming? Maybe there's a way to do this within the security voter that I'm overlooking?

I know this is an old post but I just had the same problem and found a solution that works like a charm.
PS: I'm using symfony 3.4.4
The idea is to pass the RequestStack to the voter (constructor injection) then get the session and add a message in the flashBag to be displayed after that.
The voter constructor:
use Symfony\Component\HttpFoundation\RequestStack;
class ContactVoter extends Voter {
private $requestStack;
function __construct(RequestStack $requestStack) {
$this->requestStack= $requestStack;
}
If you are not using autowire and want to pass it as an argument in the service.yml you can use arguments: [#request_stack]
Inside the voter, the function that decide the permissions:
if ( 'Your_Access_Denied_Condition') {
$this->requestStack->getCurrentRequest()->getSession()->getFlashBag()->add('danger', 'Your message !');
return false ;
}
The template to display the message
{% if app.request.hasPreviousSession %}
{% for type, messages in app.session.flashbag.all() %}
{% for message in messages %}
<div class="alert alert-{{ type }}">
{{ message|trans({}, 'messages') }}
</div>
{% endfor %}
{% endfor %}
{% endif %}
If you dont want to use the flashBag, you can use the same logic to throw a customized exception with a specific message, catch it with a listener and display the message you want.

Ok I've had the same problem as you did. There is no easy way for this one you'd have to overwrite the default SecurityListener of Symfony Sensio\Bundle\FrameworkExtraBundle\EventListener\SecurityListener with something in those lines:
use Sensio\Bundle\FrameworkExtraBundle\EventListener\SecurityListener;
class MySecurityListener extends SecurityListener
{
public function onKernelController(FilterControllerEvent $event)
{
$request = $event->getRequest();
if (!$configuration = $request->attributes->get('_acme_security')) {
return;
}
// trick to simulate one security configuration (all in one class/method).
$request->attributes->set('_security', new SecurityConfiguration($configuration));
if (!$this->language->evaluate($configuration->getExpression(), $this->getVariables($request))) {
throw new AccessDeniedException(sprintf($configuration->getMessage());
}
parent::onKernelController($event);
}
}
On top of that you'll need to extend Sensio\Bundle\FrameworkExtraBundle\Configuration\Security like so:
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security as SensioSecurity;
/**
* #Annotation
*/
class Security extends SensioSecurity
{
protected $message;
public function getAliasName()
{
return 'acme_security';
}
public function getMessage()
{
return $this->message;
}
}
This above will allow you to add a message property to your controller annotation and that will be used if there is AccessDenied exception.
And here how to configure your security listener in Yaml:
acme.security.listener:
class: AppBundle\EventListener\SecurityListener
parent: sensio_framework_extra.security.listener
tags:
- { name: kernel.event_subscriber }

Related

Symfony : how to set init data on login

I'm facing a dilemna as well as an optimization problem :
In my Symfony 2.8 application, I have custom settings and other business logic data to load (from database tables, not from SF parameters) that a logged in user can be needed to use at different pages.
At first those data where scarcely needed, so i loaded them only when the page required it. But now as the application grows, i need them more often.
So i was thinking about loading them when the user logs in, and save them as localStorage on client side because cookies are too small.
But i'm not sure how to best do it.
I have a login success handler, that allows to redirect on the correct page when user is successfully logged.
For the moment i have this one :
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationChecker;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
class LoginSuccessHandler implements AuthenticationSuccessHandlerInterface
{
protected $router;
protected $authorizationChecker;
public function __construct(Router $router, AuthorizationChecker $authorizationChecker)
{
$this->router = $router;
$this->authorizationChecker = $authorizationChecker;
}
/**
* What to do when user logs in.
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$response = null;
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
//an admin is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_admin'));
} else if ($this->authorizationChecker->isGranted('ROLE_USER')) {
//a user is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_user'));
}
//redirect to any last visited page if any
$key = '_security.main.target_path';
if ($request->getSession()->has($key)) {
$url = $request->getSession()->get($key);
$request->getSession()->remove($key);
$response = new RedirectResponse($url);
}
return $response;
}
}
So i was thinking about adding a setInitialData() method in which i would get all the settings i need and modifying onAuthenticationSuccess :
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$response = null;
//retrieve array of data to be set in the init
$toBeSaved = $this->setInitialData();
if ($this->authorizationChecker->isGranted('ROLE_ADMIN')) {
//an admin is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_admin', ['initdata'=>$toBeSaved]));
} else if ($this->authorizationChecker->isGranted('ROLE_USER')) {
//a user is redirected towards this page
$response = new RedirectResponse($this->router->generate('my_back_user', ['initdata'=>$toBeSaved]));
}
//redirect to any last visited page if any
$key = '_security.main.target_path';
if ($request->getSession()->has($key)) {
$url = $request->getSession()->get($key);
$request->getSession()->remove($key);
$response = new RedirectResponse($url, ['initdata'=>$toBeSaved]);
}
return $response;
}
And then on the main template, i would retrieve that data
{% for paramName, paramValue in app.request.query %}
{% if paramName == 'initdata' %}
<div id="initdata" data-init="{{paramValue|json_encode}}"></div>
{% endif %}
{% endfor %}
and add a javascript block with something like :
<script>
if ($('#initdata').length > 0){
localStorage.removeItem('initdata');
localStorage.setItem('initdata', JSON.stringify($('#initdata').data('init')));
}
</script>
But this method doesn't seems right : i'm not sure this is the best way to do it.
And furthermore, since these are sent in a redirect, the data are shown in the query string, which is not ideal :(
This will not fly as by having multiple parameters you create multiple <div> elements with identical ID = initdata. Subsequent jQuery selector will only capture the first one (afaik).
I see that you indeed send params via query string. This takes care of multiple value, but this also exposes your user setting in user URL, doesn't it? If it does, it has security vulnerability all over the wall. Remember, such URLs are persisted in your browser's history.
Instead, I suggest you create a separate controller action /_get_user_settings which you will call via AJAX GET. Server will serve JSON response which you can save to your localStorage with little or no problem at all.
Hope this helps...

Symfony 3.1 login errors are not bubbling up with HWIOAuthBundle + FOSUserBundle

The integration of HWIOAuthBundle + FOSUserBundle works fine along with the Facebook and Google sign-in option (Symfony 3.1). If the user enters valid username and password, then no worries, everything is smooth. However when the credentials are incorrect (for the case of FOSUserBundle with local user data), errors are not bubbling! :(
The error messages from FOSUserBundle should normally be stored inside error variable but when I try dump(error) in twig, there is no value set for this variable. I hope HWIOAuthBundle is not interfering with error variables from FOSUserBundle. Does anyone know how to fix this?
My views are stored as follows:
[app]
[Resources]
[FOSUserBundle]
[view]
[Security]
login.html.twig
login_content.html.twig
[HWIOAuthBundle]
[view]
[Connect]
login.html.twig
Update: Problem solved
After inspecting the code, I found out that the controller override of Hwi that I used had the following block of codes, that was responsible for resetting the $error variable to null.
/**
* #author Alexander <iam.asm89#gmail.com>
*/
class ConnectController extends Controller {
public function connectAction(Request $request) {
....
/* FOLLOWING BLOCK WAS CREATING PROBLEM */
/*
if ($request->attributes->has($authErrorKey)) {
$error = $request->attributes->get($authErrorKey);
} elseif (null !== $session && $session->has($authErrorKey)) {
$error = $session->get($authErrorKey);
$session->remove($authErrorKey);
} else {
$error = null;
}
*/
....
}
...
}
After removal of this block, the error bubbling was restored.

Symfony2 service method used in Twigs layout to soon

I've a service registered for Twig and i use its method in my main layout.twig.html to list some things.
Next, in some actions i use the same service to change its state (change some private fields there) and i would like to see those changes in my rendered page. But it looks like Twig invokes the "getter" method to soon, when my data is not yet managed by controller's action.
What is the best practice for such case? Should i somehow use some Events and make my Service kind of event listener?
Example layout code:
<div>{{ myservice.mymethod() }}</div>
Service:
class MyService {
private $myfield = null;
....
public function setMyField($value) {
$this->myfield = $value;
}
public function myMethod() {
if($this->myfield === null) {
return 'not initialized';
} else {
$this->myfield;
}
}
....
Some controller action:
$myservice = $this->container->get('myservice');
$myservice->setMyField('setted in action');
And i always get not initialized on rendered page
I think you have to register this service as a twig extension.
check out this manual: http://symfony.com/doc/current/cookbook/templating/twig_extension.html.

Symfony2 - Is there anyway to implement something like jsf fragments?

I mean, some code that has his own logic related to a specific twig template and a related logic in a controller INSIDE another page.
Something like a bar with specific data for a user. Name, State, Phone number and some services and
this logic included I want to include it into pages where I decide to. Just reusing it.
You can just render a controller that returns that data from your views or make a service which fetches the data and expose it to twig.
1. Controller Example
Controller
class UserDataController extends Controller
{
public function userDataAction()
{
$userData = // fetch user data....
return $this->render('user_data_fragment_template.html.twig', ['user_data' => $userData]);
}
}
Some template where you want to show that fragment
<div>{{ render(controller('YourBundle:UserDataController:userData')) }}</div>
2. Service Example
Data Provider Service
class UserDataProvider
{
public function __construct(...)
{
....
}
public function getUserData()
{
$userData = // fetch user data...
return $userData;
}
}
config.yml
// ...
twig:
globals:
user_data_provider: #your_user_data_provider_service_name
Some template where you want to show that fragment
<div>{% include 'user_data_fragment_template.html.twig' with { userData: user_data_provider.userData } only %}</div>

Symfony 2.1 set locale

Does anybody know how to set the locale in Symfony2.1?
I am trying with:
$this->get('session')->set('_locale', 'en_US');
and
$this->get('request')->setLocale('en_US');
but none of those has any effect, the devbar tells me:
Session Attributes: No session attributes
Anyway, it is always the fallback locale that is used, as defined in config.yml
(PS: I am trying to set up the translation system as described here
Even though the Symfony 2.1 states that you can simply set the locale via the Request or Session objects, I never managed to have it working, setting the locale simply has no effect.
So I ended up using a listener coupled with twig routing to handle the locale/language:
The listener:
namespace FK\MyWebsiteBundle\Listener;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class LocaleListener implements EventSubscriberInterface
{
private $defaultLocale;
public function __construct($defaultLocale = 'en')
{
$this->defaultLocale = $defaultLocale;
}
public function onKernelRequest(GetResponseEvent $event)
{
$request = $event->getRequest();
if (!$request->hasPreviousSession()) {
return;
}
if ($locale = $request->attributes->get('_locale')) {
$request->getSession()->set('_locale', $locale);
} else {
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
}
static public function getSubscribedEvents()
{
return array(
// must be registered before the default Locale listener
KernelEvents::REQUEST => array(array('onKernelRequest', 17)),
);
}
}
Register the listener in service.xml:
<service id="fk.my.listener" class="FK\MyWebsiteBundle\Listener\LocaleListener">
<argument>%locale%</argument>
<tag name="kernel.event_subscriber"/>
</service>
The routing must look like:
homepage:
pattern: /{_locale}
defaults: { _controller: FKMyWebsiteBundle:Default:index, _locale: en }
requirements:
_locale: en|fr|zh
And handle the routing with:
{% for locale in ['en', 'fr', 'zh'] %}
<a href="{{ path(app.request.get('_route'), app.request.get('_route_params')|merge({'_locale' : locale})) }}">
{% endfor %}
This way, the locale will automatically be set when you click on a link to change the language.
You set the locale in your parameters.yml.
[parameters]
...
locale = en
The fallback from your config.yml references %locale% which is the setting from the above parameters.yml file.
If you are trying to set it on-the-fly then this should work:
$this->get('session')->setLocale('en_US');
Test it by printing it out straight after:
print_r($this->get('session')->getLocale());
Edit
In 2.1 the locale is now stored in the request but can still be set in the session. http://symfony.com/doc/2.1/book/translation.html#handling-the-user-s-locale
$this->get('session')->set('_locale', 'en_US');
// setting via request with get and setLocale
$request = $this->getRequest();
$locale = $request->getLocale();
$request->setLocale('en_US');
It's not:
$this->get('request')->setLocale('en_US');
But:
$this->get('request')->getSession()->set('_locale', 'en_US');
From the symfony cookbook:
"Locale is stored in the Request, which means that it's not "sticky" during a user's request. In this article, you'll learn how to make the locale of a user "sticky" so that once it's set, that same locale will be used for every subsequent request."
http://symfony.com/doc/current/cookbook/session/locale_sticky_session.html
You can notice this when you set the locale and use the symfony profiler (in dev mode) to view the sub requests.

Resources