is there is some way to redirect to login page with message using symfony 2 access_control?
I need current role but only one exception in sub route.
I want to create some session bag message without entering in controller.
Is that is possible?
access_control:
- { path: /user/submission, role: [ROLE_USER_WITH_MESSAGE]}
- { path: ^/user, role: [ROLE_USER] }
I asking this in reason to show message to user if he try to goes on certain route
Yes, it's possible to se the session (of flashbag message) before login without controller involved. It can be done by an Authentication entry point service which is called right after start of authentication process. This Authentication entry point service is a part of firewall configuration as an entry_point key.
More on this can be found here: http://symfony.com/doc/current/components/security/firewall.html#entry-points
class MyAuthEntryPoint implements AuthenticationEntryPointInterface
{
protected $router;
public function __construct($router) {
$this->router = $router;
}
public function start(Request $request, AuthenticationException $authException = null) {
if ($request->get('_route') == 'submission') {
$session = $request->getSession();
$session->getFlashBag()->add('submissionUserMessage', 'Weclome user from submission!');
//or if you do not want to use flashBag
$session->set('submissionUserMessage', 'Weclome user from submission!');
}
return new RedirectResponse($this->router->generate('login'));
}
}
Service
Related
In my current app, we've chosen not to use Doctrine or an ORM. I'm attempting to use Symfony 4.1's authentication system to log people in. What I'd like to do is use PDO directly to fetch users from the database.
I'm following this guide: http://symfony.com/doc/current/security/custom_provider.html. In security.yaml, I've created a new provider entry and added it to the firewall:
providers:
db_provider:
id: App\Utility\Security\UserAuthenticationProvider
encoders:
App\Utility\Security\UserAuthenticationProvider: bcrypt
firewalls:
main:
pattern: ^/
form_login:
login_path: login
check_path: check_login
default_target_path: user
anonymous: ~
provider: db_provider
And I've created UserAuthenticationProvider:
class UserAuthenticationProvider implements UserProviderInterface {
private $config;
private $userDAO;
public function __construct() {
$this->config = new Config();
$this->userDAO = new MySqlUserDAO($this->config);
}
public function loadUserByUsername($username) {
$user = $this->userDAO->getUserByUsername();
...
return $user;
}
public function refreshUser(UserInterface $user) {
...
}
public function supportsClass($class) {
...
}
My userDAO returns an object that implements UserInterface.
So when I got to my route /login, I get my login form. However, no users are loaded from the database. I can see that my UserProviderInterface gets created (by using dump in the constructor), but loadUserByUsername does not.
Do I need to implement something else that uses my UserAuthenticationProvider and calls loadUserByUsername?
Is there perhaps a better way to do authentication in Symfony without using Doctrine?
UPDATE
I found this guide which is older but has a bit more detail / context.
I've changed my classes / configs like so (edited for brevity):
#security.yaml
security:
providers:
db_provider:
id: database_user_provider
main:
pattern: ^/
form_login:
provider: db_provider
login_path: login
check_path: check_login
default_target_path: do_some_stuff
.
#services.yaml
services:
database_user_provider:
class: App\Utility\Security\DatabaseUserProvider
.
class DatabaseUser
implements
UserInterface,
EquatableInterface
{
protected $user;
public function getUser(): User {
return $this->user;
}
public function setUser(User $user): void {
$this->user = $user;
}
public function getRoles() {
return array("ROLE_USER");
}
public function getPassword() {
return $this->getUser()->getPassword();
}
public function getUsername() {
return $this->getUser()->getUsername();
}
}
.
class DatabaseUserProvider implements UserProviderInterface {
private $config;
private $userDAO;
public function __construct() {
$this->config = new Config();
$this->userDAO = new MySqlUserDAO($this->config);
}
public function loadUserByUsername($username): UserInterface {
$user = $this->userDAO->getUserByUsername($username);
$dbUser = new DatabaseUser();
$dbUser->setUser($user);
return $dbUser;
}
public function refreshUser(UserInterface $user) {
return $this->loadUserByUsername($user->getUsername());
}
public function supportsClass($class) {
return DatabaseUser::class === $class;
}
}
So what each file is doing (in a nutshell is):
services.yaml: naming my class DatabaseUserProvider to the service name database_user_provider for use in security.yaml.
security.yaml: setting database_user_provider to the alias db_provider and adding that as the provider on the main firewall.
DatabaseUser: my 'entity' class to represent my user from the database. I have a dumb User class that just has a few properties (username, password) and getters / setters for those. It is set as a property for the DatabaseUser object.
DatabaseUserProvider: Loads my DatabaseUser objects from the database using the DAO and returns them. (more specifically, the DAO returns a user, which is added to a new DatabaseUser object, which is returned).
The DAO simply runs a sql query to get a single result from the user table. it then takes this result and populates a User Value Object and returns it.
Results
When I was using Doctrine (and following this guide for loading users from the database and this one for a login form), the login route on the SecurityController would handle both rendering of the form and processing of the login request. Somehow, Symfony / Doctrine was smart enough to automatically (using a listener maybe???) load the appropriate user entity from the DB and authenticate them against the password they provided (and then set the user token and redirect them to the page they were trying to access.)
When I bypass doctrine (and use my own DAOs) and the classes above, authentication still does not occur. I can see that an instance of DatabaseUserProvider is being created (by dumping some vars in the constructor), and the Security tab of the profilier shows database_user_provider as the provider. But that seems to be as far as it gets.
Question
It seems to me that DatabaseUserProvider::loadUserByUsername should be the next thing that should happen. From where does this method get called from? Do I need to be passing the username into the constructor and kick it off from there? Should I be using this class as a service in my Controller and call the method manually from the controller (something I did not have to do when using doctrine - none of this logic was in the controller)?
I have 3 services which should override the default services only if the user has a specific role.
Or even better. Inject the current user/security in the new services.
The service then performs the check for the user role and calls the original service.
I tried to inject security.context into it. But then $security->getToken() returns null.
In the controllers it works fine. How can i get the current user in my service? This is what i want to do:
class AlwaysVisibleNavigationQueryBuilder extends NavigationQueryBuilder
{
public function __construct(\Sulu\Component\Content\Compat\StructureManagerInterface $structureManager, $languageNamespace, SecurityContext $security)
{
if (in_array('ROLE_SULU_ADMINISTRATOR', $security->getToken()->getRoles())) {
// Show unpublished content, too
$this->published = false;
}
parent::__construct($structureManager, $languageNamespace);
}
}
At the moment of creation of the service, the securityContext was not aware of the current user. The Security is filles when the application runs and not on dependency-resolution.
The following Code works.
class AlwaysVisibleNavigationQueryBuilder extends NavigationQueryBuilder
{
protected $security;
public function __construct(\Sulu\Component\Content\Compat\StructureManagerInterface $structureManager, $languageNamespace, SecurityContext $security)
{
$this->security = $security;
parent::__construct($structureManager, $languageNamespace);
}
public function build($webspaceKey, $locales)
{
$roles = $this->security->getToken()->getRoles();
if (in_array('ROLE_SULU_ADMINISTRATOR', $roles)) {
// Show unpublished content, too
$this->published = false;
}
return parent::build($webspaceKey, $locales);
}
}
Thanks to Matteo!
supposed having certain route string like "/path/index.html" protected by firewall, how to chek whether current user is able to access it?
Thanks in advance!
I am sorry, I should have been more explicit: I have an array of route names and I construct a menu. A lot of users with different roles can access a page with this menu. The purpose is to show only accessible liks in this menu for a particular user.
Something like:
'security_context'->'user'->isGranted('/path/index.html')
This answer is based on your comments:
You should get the roles needed to access that route.to that you need access to the security.access_map service which is private.so it has to be injected directly.e.g: you can create a path_roles service like such that you can get the roles for a certain path:
namespace Acme\FooBundle;
class PathRoles
{
protected $accessMap;
public function __construct($accessMap)
{
$this->accessMap = $accessMap;
}
public function getRoles($path)
{ //$path is the path you want to check access to
//build a request based on path to check access
$request = Symfony\Component\HttpFoundation\Request::create($path, 'GET');
list($roles, $channel) = $this->accessMap->getPatterns($request);//get access_control for this request
return $roles;
}
}
now declare it as a service:
services:
path_roles:
class: 'Acme\FooBundle\PathRoles'
arguments: ['#security.access_map']
now use that service in your controller to get the roles for the path and construct your menu based on those roles and isGranted.i.e:
//code from controller
public function showAction(){
//do stuff and get the link path for the menu,store it in $paths
$finalPaths=array();
foreach($paths as $path){
$roles = $this->get('path_roles')->getRoles($path);
foreach($roles as $role){
$role = $role->getRole();//not sure if this is needed
if($this->get('security.context')->isGranted($role)){
$finalPaths[] = $path;
break;
}
}
//now construct your menu based on $finalPaths
}
}
You could use security.access_control configuration option:
securty:
access_control:
- { path: "^/path/index.html$", roles: ROLE_SOME_ROLE}
Or simply check that manually from within your controller:
class SomeController extends Controller {
public function indexAction() {
if (!$this->get('security.context')->isGranted(...)) {
throw new AccessDeniedException(...);
}
...
}
}
I want to secure all urls that have the ?preview=true query string applied.
The following unfortunately does not work. I guess the request matcher looks at pathInfo only.
# app/config/security.yml
access_control:
- { path: (?|&)preview=true, role: ROLE_ADMIN }
Any hints are much appreciated.
Im pretty sure that access control only looks at the hierarchical part and not the query string. This is because query parameters are not meant to determine content, only to be filters on content. You will likely need to rework your URL structure to match these.
Solved it myself. As #Chausser said access control via query params is not possible in security.yml, so I've created a voter for that.
// src/Your/Bundle/Security/Voter/PreviewAccessVoter.php
namespace Your\Bundle\Security\Voter;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
/**
* voter that denies access if user has not the required role
* to access urls with preview query param set true
*/
class PreviewAccessVoter implements VoterInterface
{
protected $requestStack;
protected $requiredRole;
public function __construct(RequestStack $requestStack, $requiredRole)
{
$this->requestStack = $requestStack;
$this->requiredRole = $requiredRole;
}
public function supportsAttribute($attribute)
{
return true;
}
public function supportsClass($class)
{
return true;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
if(filter_var($this->requestStack->getCurrentRequest()->query->get('preview'), FILTER_VALIDATE_BOOLEAN))
{
foreach($token->getRoles() as $role) {
if($this->requiredRole === $role->getRole()) {
return VoterInterface::ACCESS_GRANTED;
}
}
return VoterInterface::ACCESS_DENIED;
}
return VoterInterface::ACCESS_ABSTAIN;
}
}
Add voter as (hidden) service
# src/Your/Bundle/Resources/config/services.yml
# preview voter that denies access for users without the required role
security.access.preview.voter:
class: Your\BundleBundle\Security\Voter\PreviewAccessVoter
arguments: [ #request_stack , ROLE_ADMIN ]
tags: [ { name: security.voter } ]
public: false
Changing the Access Decision Strategy
# app/config/security.yml
access_decision_manager:
strategy: unanimous # can be: affirmative, unanimous or consensus
see: http://symfony.com/doc/current/cookbook/security/voters.html as a reference
https://github.com/symfony/Security/blob/master/Core/Authorization/Voter/RoleVoter.php might also be helpful
I'm migrating quite a large community to symfony2. The current user table contains a lot of users with non-alphanumeric chars in the username. In the new version I only allow [a-zA-Z0-9-] for benefits like semantic URLs for each user.
Is it possible to catch users who log in with email/pass and have no username set? I would like them to redirect to a page where they will be able to re-pick a username. The tricky part: they should not be able to touch anything on the site unless they have a correct username.
I thought about a event, from the fosuserbundle but I couldn't find a suitable one.
You could use events. See an example here: http://symfony.com/doc/2.0/cookbook/event_dispatcher/before_after_filters.html
Of course the action changing the username should be ignored by the event listener. Just like login and other anonymous actions.
You can return any response, including a redirect, by setting response on an event.
Just an idea. How about the AOP paradigm (JMSAopBundle)? Define a pointcut for you controllers (except for the login one):
class PrivateEntityInformationPointcut implements PointcutInterface
{
public function matchesClass(\ReflectionClass $class)
{
return $class->isSubclassOf('Your\Controller\Superclass')
&& $class->name !== 'Your\Controller\Access';
}
public function matchesMethod(\ReflectionMethod $method)
{
return true; // Any method
}
}
Then the interceptor should redirect to the page for setting the username:
class DenyEntityAccessInterceptor implements MethodInterceptorInterface
{
private $securityContext;
private $logger;
/**
* #DI\InjectParams({
* "securityContext" = #DI\Inject("security.context"),
* "logger" = #DI\Inject("logger"),
* })
*/
public function __construct(SecurityContext $securityContext,
Logger $logger)
{
$this->securityContext = $securityContext;
$this->logger = $logger;
}
public function intercept(MethodInvocation $invocation)
{
// Check username, redirect using the router, log what's happening
// It's OK
return $invocation->proceed();
}
}