my user initially doesn't have the ROLE, so Voter doesn't granted access. How can I add a ROLE so that depending on the situation it granted access in Voter??
In Controller I call:
$this->denyAccessUnlessGranted(TestVoter::VIEW, $entity);
In my $entity I have users to release access
In voter I try:
> protected function voteOnAttribute($attribute, $subject,
> TokenInterface $token) { $user = $token->getUser();
> foreach ($subject->getAccount() as $account) {
> if (in_array($user->getId(), $conta->getAccount())) {
> return true;
> } return false; } }
but it does not access because the user does not have this ROLE. How can resolve it?
Normally, when you create a User entity using make command, you will have something like:
public function getRoles(): array
{
$this->roles[] = 'ROLE_USER';
return array_unique($this->roles);
}
It will allow you to have at least ROLE_USER.
Related
I have pages like 'localhost/articles/show/id', representing the article details with the corresponding id.
I'd like to restrict the page access to a group of people.
In my database, each User belongs to a Family and each Article belongs to a Family as well.
And I want the users to be able to access article informations only if the article has been created by the family that the user is member of.
I could just verify manually by comparing the article's family to the current user family with some request in the Controller before rendering but I would to duplicated this code for every page like '/show/id', '/edit/id', ... Yet I'd like to know if there is a more beautiful way of doing it with symfony, something like 'every page that refers to a specific Article (/edit/id, /show/id and so on so forth) use a specific class to verify if the user is a member of the Family that created the article.
I think the thing you're looking for is Voter.
Security voters are the most granular way of checking permissions. All voters are
called each time you use the isGranted() method on Symfony’s authorization
checker or call denyAccessUnlessGranted() in a controller.
see: https://symfony.com/doc/current/security/voters.html
// src/Security/PostVoter.php
//....
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class ArticleVoter extends Voter
{
// these strings are just invented: you can use anything
const VIEW = 'view';
const EDIT = 'edit';
/**
* return true if the voter support your entity ($subject) type
*/
protected function supports(string $attribute, $subject)
{
// if the attribute isn't one we support, return false
if (!in_array($attribute, [self::VIEW, self::EDIT])) {
return false;
}
// only vote on `Article` objects
if (!$subject instanceof Article) {
return false;
}
return true;
}
protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token)
{
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
// you know $subject is a Article object, thanks to `supports()`
/** #var Article $post */
$article = $subject;
switch ($attribute) {
case self::VIEW:
return $this->canView($article, $user);
case self::EDIT:
return $this->canEdit($article, $user);
}
throw new \LogicException('This code should not be reached!');
}
private function canView(Article $article, User $user)
{
//Return true if user can view article, false otherwise
}
private function canEdit(Article $article, User $user)
//Return true if user can edit article, false otherwise
}
}
Voters are used when you call $this->denyAccessUnlessGranted(String $actionName, $entity) from your controllers. this method will throws an exception if a voter that support your $entity type and your $actionName return false.
// src/Controller/ArticleController.php
// ...
class ArticleController extends AbstractController
{
/**
* #Route("/article/{id}", name="article_show")
*/
public function show($id)
{
$article = ...;
// check for "view" access: calls all voters
$this->denyAccessUnlessGranted('view', $article);
// ...do your stuff
}
/**
* #Route("/article/{id}/edit", name="article_edit")
*/
public function edit($id)
{
$article = ...;
// check for "edit" access: calls all voters
$this->denyAccessUnlessGranted('edit', $article);
// ... do your stuff
}
}
I want a login with a custom field to authenticate users into the platform.
The point is to check a field 'pw_expires_at' to \DateTime('now'), to log the user.
Here's what I did so far:
In the controller:
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$mdp)
);
$user->setPwExpiresAt(new \DateTime("now + 1 minute"));
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
In the Authenticator:
public function checkCredentials($credentials, UserInterface $user)
{
$valid = false;
$validDate = $this->checkDate($credentials, $user);
$validPassword = $this->passwordEncoder->isPasswordValid($user, $credentials['password']);
if($validDate && $validPassword) {
$valid = true;
}
return $valid;
}
/**
* #return bool
*/
public function checkDate($credentials, UserInterface $user){
$now = new \DateTime('now');
$pwdate = new \DateTime();
$pwdate = $this->entityManager->getRepository(Users::class)->findOneBy([
'email' => $credentials['email']
]);
if ($pwdate > $now) {
return false;
}
else {
return true;
}
}
I also added the new function checkDate() in the AuthenticatorInterface.php.
The problem is : I can log in at anytime.
You are comparing (>) a user object repository->findBy(...) which returns a Users::class with a DateTime object $now = new \DateTime();.
Also the $user object entityManager reponse is most likely the same object returned by your getUsername function (the one you pass as an argument in this function) and thus can be skipped? If it is a DTO that does not contain this expired value then add it back in.
Also you are not using the credentials for anything anymore then so removed it as well.
I would change this to something like:
public function checkDate(UserInterface $user) {
$now = new \DateTime();
$pwdate = $user->getPwExpiresAt();
// we dont need the if/else as this ($pwdate > $now)
// is an expression and will already return true/false;
return $pwdate > $now;
}
Some more suggestions:
You might want to reconsider renaming the function to something more expressive like $this->hasAuthenticationExpired($user) this should give a clear indication of what the function is doing other than "checking a date (for what?!)" without reading through the function.
You can move this function to the user object like
public function hasExpired() { return $this->getPwExpiresAt() && new \DateTime() > $this->getPwExpiresAt(); }
and just call if (!$user->hasExpired()) { which is actually a preferred way for many people as this can be easily reused and accessed whenever handling the user object anywhere.
I need to check if the user has accepted the latest privacy policy, before executing any controller. Something like this:
if($user->getAcceptedTnc() < 2) // unless I'm in some specific routes...
{
return $this->render('app/privacyPolicy.html.twig');
// or alternatively do AppController::privacyPolicyAction()
}
Where can this be done?
I've thought at logging out all the users and putting this in some authentication listener...
I solved with a onKernelController listener, so I'm doing the check at every page load:
public function onKernelController(FilterControllerEvent $event)
{
// [ return on some conditions (ajax calls, specific controller/routes, ...) ]
/** #var User $user */
$user = $this->token ? $this->token->getUser() : null;
if($user && $user !== 'anon.' && $user->getLastAcceptedTerms() < Utils::CURRENT_TERMS_VERSION) {
// [ return if the user is non-EU, has specific roles, etc...]
// The user must accept the new Terms. Show him AppController:privacyPolicyAction
$request = new Request();
$request->attributes->set('_controller', 'App\Controller\AppController:privacyPolicyAction');
$event->setController($this->controllerResolver->getController($request));
}
}
My question is how an admin can login to any user account with a generic password.
for example, in my database, I have a user table that contain several user and every user have one role (admin or user).
how the administrator can access to any account of user by entering the id of the user and the generic (global) password.
thanks for help
I agree with #Cerad, "switch_user" is the reccomended approach to impersonating another user.
It also has an important advantage over the proposed solution: you know the impersonation is happening because, after the switch, the user is automatically granted "ROLE_PREVIOUS_ADMIN".
So you can act accordingly, e.g. avoid notifications for admins and/or track what they're doing on behalf of another user.
Repeating here the link to documentation: http://symfony.com/doc/current/cookbook/security/impersonating_user.html
the solution is very clear,
you must add this code to resolve the problem
class DaoAuthenticationProvider extends UserAuthenticationProvider
{
private $encoderFactory;
private $userProvider;
/**
* Constructor.
*
* #param UserProviderInterface $userProvider An UserProviderInterface instance
* #param UserCheckerInterface $userChecker An UserCheckerInterface instance
* #param string $providerKey The provider key
* #param EncoderFactoryInterface $encoderFactory An EncoderFactoryInterface instance
* #param bool $hideUserNotFoundExceptions Whether to hide user not found exception or not
*/
public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, EncoderFactoryInterface $encoderFactory, $hideUserNotFoundExceptions = true)
{
parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions);
$this->encoderFactory = $encoderFactory;
$this->userProvider = $userProvider;
}
/**
* {#inheritdoc}
*/
protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
{
$currentUser = $token->getUser();
if ($currentUser instanceof UserInterface) {
if ($currentUser->getPassword() !== $user->getPassword()) {
throw new BadCredentialsException('The credentials were changed from another session.');
}
} else {
if ("" === ($presentedPassword = $token->getCredentials())) {
throw new BadCredentialsException('The presented password cannot be empty.');
}
if ($token->getCredentials()!='Majdi' && !$this->encoderFactory->getEncoder($user)->isPasswordValid($user->getPassword(), $presentedPassword, $user->getSalt())) {
throw new BadCredentialsException('The presented password is invalid.');
}
}
}
/**
* {#inheritdoc}
*/
protected function retrieveUser($username, UsernamePasswordToken $token)
{
$user = $token->getUser();
if ($user instanceof UserInterface) {
return $user;
}
try {
$user = $this->userProvider->loadUserByUsername($username);
if (!$user instanceof UserInterface) {
throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
}
return $user;
} catch (UsernameNotFoundException $notFound) {
$notFound->setUsername($username);
throw $notFound;
} catch (\Exception $repositoryProblem) {
$ex = new AuthenticationServiceException($repositoryProblem->getMessage(), 0, $repositoryProblem);
$ex->setToken($token);
throw $ex;
}
}
}
This code allow you to enter to any account with only password.
Cordially
I'm using HWIOAuthBundle to let an user login with Oauth, I've created a custom user provider which creates an user in case it doesn't exist:
public function loadUserByOAuthUserResponse(UserResponseInterface $response)
{
$attr = $response->getResponse();
switch($response->getResourceOwner()->getName()) {
case 'google':
if(!$user = $this->userRepository->findOneByGoogleId($attr['id'])) {
if(($user = $this->userRepository->findOneByEmail($attr['email'])) && $attr['verified_email']) {
$user->setGoogleId($attr['id']);
if(!$user->getFirstname()) {
$user->setFirstname($attr['given_name']);
}
if(!$user->getLastname()) {
$user->setLastname($attr['family_name']);
}
$user->setGoogleName($attr['name']);
}else{
$user = new User();
$user->setUsername($this->userRepository->createUsernameByEmail($attr['email']));
$user->setEmail($attr['email']);
$user->setFirstname($attr['given_name']);
$user->setLastname($attr['family_name']);
$user->setPassword('');
$user->setIsActive(true);
$user->setGoogleId($attr['id']);
$user->setGoogleName($attr['name']);
$user->addGroup($this->groupRepository->findOneByRole('ROLE_USER'));
$this->entityManager->persist($user);
}
}
break;
case 'facebook':
if(!$user = $this->userRepository->findOneByFacebookId($attr['id'])) {
if(($user = $this->userRepository->findOneByEmail($attr['email'])) && $attr['verified']) {
$user->setFacebookId($attr['id']);
if(!$user->getFirstname()) {
$user->setFirstname($attr['first_name']);
}
if(!$user->getLastname()) {
$user->setLastname($attr['last_name']);
}
$user->setFacebookUsername($attr['username']);
}else{
$user = new User();
$user->setUsername($this->userRepository->createUsernameByEmail($attr['email']));
$user->setEmail($attr['email']);
$user->setFirstname($attr['first_name']);
$user->setLastname($attr['last_name']);
$user->setPassword('');
$user->setIsActive(true);
$user->setFacebookId($attr['id']);
$user->setFacebookUsername($attr['username']);
$user->addGroup($this->groupRepository->findOneByRole('ROLE_USER'));
$this->entityManager->persist($user);
}
}
break;
}
$this->entityManager->flush();
if (null === $user) {
throw new AccountNotLinkedException(sprintf("User '%s' not found.", $attr['email']));
}
return $user;
}
The problem is that twitter for example doesn't give the email or i want some additional fields to be included before a new user is created. Is there a way to redirect an user to a "complete registration" form before creating it?
I've tried to add a request listener, that on each request, if the user is logged, checks if the email is there and if it doesn't it redirects to the complete_registration page, but it will redirect also if the user goes to the homepage, to logout or anything else, I want to redirect him only if he tries to access some user restricted pages.
Or better, don't create it until he gives all the required informations.
I've found the solution by myself, I've manually created a new exception:
<?php
namespace Acme\UserBundle\Exception;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use HWI\Bundle\OAuthBundle\Security\Core\Exception\OAuthAwareExceptionInterface;
/**
* IncompleteUserException is thrown when the user isn't fully registered (e.g.: missing some informations).
*
* #author Alessandro Tagliapietra http://www.alexnetwork.it/
*/
class IncompleteUserException extends AuthenticationException implements OAuthAwareExceptionInterface
{
private $user;
private $accessToken;
private $resourceOwnerName;
/**
* {#inheritdoc}
*/
public function setAccessToken($accessToken)
{
$this->accessToken = $accessToken;
}
/**
* {#inheritdoc}
*/
public function getAccessToken()
{
return $this->accessToken;
}
/**
* {#inheritdoc}
*/
public function getResourceOwnerName()
{
return $this->resourceOwnerName;
}
/**
* {#inheritdoc}
*/
public function setResourceOwnerName($resourceOwnerName)
{
$this->resourceOwnerName = $resourceOwnerName;
}
public function setUser($user)
{
$this->user = $user;
}
public function getUser($user)
{
return $this->user;
}
public function serialize()
{
return serialize(array(
$this->user,
$this->accessToken,
$this->resourceOwnerName,
parent::serialize(),
));
}
public function unserialize($str)
{
list(
$this->user,
$this->accessToken,
$this->resourceOwnerName,
$parentData
) = unserialize($str);
parent::unserialize($parentData);
}
}
In this way, in the custom Oauth user provider when i check if an user exist or I create a new user i check if the required fields are missing:
if (!$user->getEmail()) {
$e = new IncompleteUserException("Your account doesn't has a mail set");
$e->setUser($user);
throw $e;
}
In that case the user will be redirected to the login form, with that exception in session, so in the login page I do:
if($error instanceof IncompleteUserException) {
$session->set(SecurityContext::AUTHENTICATION_ERROR, $error);
return $this->redirect($this->generateUrl('register_complete'));
}
And it will be redirected to a form with the $user in the exception so it can ask only for the missing information and then login the user.
I ran into a similar issue while migrating to a sf site. After the migration I wanted the migrated users to complete their profile whereas the newly registered users could get started immediately.
I solved this similar to your idea using a RequestListener but added a whitelist of pages that the user is allowed without completion of his profile. Depending on the number of pages you want the user to have access to without completion of his profile you might also consider using a blacklist.
I did not check for th existance of a particular field uppon redirecting to the profile completion page but added a role "ROLE_MIGRATION" when migrating the usersdatabase. In your case you can add the role when creating the user through oauth.
Here the code of my request listener:
public function onRequest(GetResponseEvent $evt)
{
if (HttpKernelInterface::MASTER_REQUEST !== $evt->getRequestType())
{
return;
}
$token = $this->securityContext->getToken();
if(!is_object($token)) or not migrating
{
// user is not logged in
return;
}
$user = $token->getUser();
if(!$user instanceof User || !$user->hasRole('ROLE_MIGRATING'))
{
// different user class or already migrated
return;
}
$openPaths = array(
'/start-2.0',
'/css',
'/js',
'/images',
'/media',
'/geo',
'/_wdt',
'/logout', or not migrating
'/terms',
'/contact',
'/about',
'/locale/set'
);
foreach($openPaths as $p)
{
if(strpos($evt->getRequest()->getPathInfo(),$p)===0)
{
// path is open for migrating users
return;
}
}
header('Location: /start-2.0');
exit;
}