check with voters single field in symfony - php

I would like to know how to implement a check for a field inside voters of an entity.
I have for example my entity Post where I want that a user not admin can't edit title field. Only admin can edit this field.
So I have created my voters but I don't know how to create this check because inside $post there is the old post entity and I don't know how to implement the check for title field
This is my easy voters file
class PostVoter extends Voter
{
const VIEW = 'view';
const EDIT = 'edit';
private $decisionManager;
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
protected function supports($attribute, $subject)
{
if (!in_array($attribute, array(self::VIEW, self::EDIT))) {
return false;
}
if (!$subject instanceof Post) {
return false;
}
return true;
}
protected function voteOnAttribute(
$attribute,
$subject,
TokenInterface $token
) {
$user = $token->getUser();
if (!$user instanceof User) {
return false;
}
if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) {
return true;
}
/** #var Post $post */
$post = $subject;
switch ($attribute) {
case self::VIEW:
return $this->canView($post, $user);
case self::EDIT:
return $this->canEdit($post, $user);
}
throw new \LogicException('This code should not be reached!');
}
private function canView(Post $post, User $user)
{
if ($this->canEdit($post, $user)) {
return true;
}
return true;
}
private function canEdit(Post $post, User $user)
{
return $user === $post->getUser();
}
}
I would like to implement inside canEdit a check for the title field.
I have tried to print $post but there is only old value not some information for new value.

Couple of possible approaches.
The one I would use is to add a 'edit_title' permission to the voter then adjust my form to make the title read only if the edit_title permission was denied. This not only eliminates the need to check for a changed title but also makes things a bit friendlier for the users. One might imagine them being a bit frustrated with a form that allows them to change the title but then the app rejects the change.
If you really wanted to detect a title change then you could adjust the setTitle method in your post entity. Something like:
class Post {
private $titleWasChanged = false;
public function setTitle($title) {
if ($title !== $this->title) $this->titleWasChanged = true;
$this->title = $title;
And then of course check $titleWasChanged from the voter.
If you really wanted to go all out, the Doctrine entity manager actually has some change checking capability. You could probably access it via the voter but that would probably be overkill. http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/change-tracking-policies.html

Related

Codeigniter - Error - There is no data to update

I was to update my database but I get the error, "no data to update". Here is my script;
I have created a simple toggle to up update the database. The toggle makes the user active (is_active=1) or inactive (is_active=0). The problem I am encountering is that although the object is change from 1 to 0 or 0 to 1, when I pass it to the model, it comes back with the error, "There is no data to update". The method is as follows;
namespace App\Controllers\Admin;
use App\Entities\User;
class Users extends \App\Controllers\BaseController
{
private $model;
public function __construct()
{
$this->model = new \App\Models\UserModel;
}
public function toggle_user_is_active($id)
{
$users = $this->model->where('id', $id)->first(); // get the record
// if the current user is ACTIVE, then set it to DEACTIVE
if ($users->is_active == 1) {
$users->is_active = 0;
$this->model->save($users)); // gives an error, nothing to update
return redirect()->to('/Admin/Users/index')
->with('info', 'Success - User deactivated');
} else {
// if the current used is ACTIVE(1), change to INACTIVE(0)
$users->is_active = 1;
$this->model->save($users); // same error as above
return redirect()->to('/Admin/Users/index')
->with('info', 'Success - User Activated');
}
} // end method
}
The really strange thing is this is a copy of another method that works perfectly as follows;
namespace App\Controllers\Admin;
use App\Entities\CategoryEntity;
use App\Entities\PostEntity;
class Post extends \App\Controllers\BaseController
{
private $model;
public function __construct()
{
$this->model = new \App\Models\PostModel;
$this->CategoryModel = new \App\Models\CategoryModel;
$auth = new \App\Libraries\Authentication;
$this->current_user = $auth->getCurrentUser();
}
public function toggle_post_is_published($post_id)
{
$post = $this->model->where('post_id', $post_id)->first();
// if the current post is PUBLISHED, then set it to UNPUBLISHED
if ($post->post_is_published == 1) {
echo
$post->post_is_published = 0;
$this->model->save($post);
return redirect()->to('/admin/post/post_index')
->with('info', 'Success - Post unpublished');
} else {
// if the current post is UNPUBLISHED, then set it to PUBLISHED
$post->post_is_published = 1;
$this->model->save($post);
return redirect()->to('/admin/post/post_index')
->with('info', 'Success - Post published');
}
}
} // end class
I finally figured it out. In my UserModel I did not add 'is_active' to protected $allowedFields . I have not added 'is_active' to the allowedFields and it works.
Have you declare your model, because look like you use $this- >model but not setup your model in your constructor or other method.

Laravel nova restrict viewAny()

When I want users not to be able to enter an individual resource I can use policies to do the following:
public function view(User $user, Model $object)
{
if($user->groupName != $object->groupName) {
return false;
} else {
return true;
}
}
This has as a result that the Components of your group have the eye icon (see red cirle). Components I do not want the user to see dont have the eye icon.
My desired result is that the should not be seen component is not shown at all. How can I achieve this?
I tried:
public function viewAny(User $user)
{
// $object does not exist here so I cannot use it to filter
if($user->groupName == $object->groupName) {
return true;
} else {
return false;
}
}
You need to update the index query of your resource. see more
public static function indexQuery(NovaRequest $request, $query)
{
return $query->where('groupName', $request->user()->group_name);
}
You should consider updating the relateble query too.

How to forward action processing to a second action in Laravel Controllers?

I have two similar user profiles, and they share pretty much the same view structure and information. However, a "special user profile" would display an additional block, and require a different URL - say, /user/igor and /special-user/igor.
What's the best approach to handle this?
In other frameworks I would have pseudocode similar to this:
class UserController extends Controller {
/** #url /user/{$slug} */
function actionSimpleUser($slug) {
if (!$this->viewHas('special')) {
$this->toView('special', false);
}
return $this->render('user', ['slug' => $slug]);
}
/** #url /special-user/{$slug} */
function actionSpecialUser($slug) {
$this->toView('special', true);
return $this->forwardAction('user/simpleUser', [$slug]);
}
}
There are various ways you could implement this in Laravel.
You could have 2 routes that go in to 2 controller actions:
Route::get('/user/{slug}', 'UserController#userProfile');
Route::get('/special-user/{slug}', 'UserController#specialUserProfile');
And then in these controller actions, you could render the same view and set a flag to mark it as special or not:
public function userProfile($slug)
{
$user = User::where('slug', $slug)->first();
$special = false;
return view('users.show', compact('user', 'special');
}
public function specialUserProfile($slug)
{
$user = User::where('slug', $slug)->first();
$special = true;
return view('users.show', compact('user', 'special');
}
Then in the view file check whether the user is special or not:
#if($special === true)
// special user code block
#endif

Symfony 2 - Loading roles from database

My roles are stored in the database and I am trying to load them dynamically upon login. What I'm doing is querying for the roles and setting them on the user object in my user provider as seen here:
public function loadUserByUsername($username) {
$q = $this
->createQueryBuilder('u')
->where('u.username = :username')
->setParameter('username', $username)
->getQuery()
;
try {
// The Query::getSingleResult() method throws an exception
// if there is no record matching the criteria.
$user = $q->getSingleResult();
// Permissions
$permissions = $this->_em->getRepository('E:ModulePermission')->getPermissionsForUser($user);
if ($permissions !== null) {
foreach ($permissions as $permission) {
$name = strtoupper(str_replace(" ", "_", $permission['name']));
$role = "ROLE_%s_%s";
if ($permission['view']) {
$user->addRole(sprintf($role, $name, 'VIEW'));
}
if ($permission['add']) {
$user->addRole(sprintf($role, $name, 'ADD'));
}
if ($permission['edit']) {
$user->addRole(sprintf($role, $name, 'EDIT'));
}
if ($permission['delete']) {
$user->addRole(sprintf($role, $name, 'DELETE'));
}
}
}
} catch (NoResultException $e) {
throw new UsernameNotFoundException(sprintf('Unable to find an active admin Entity:User object identified by "%s".', $username), null, 0, $e);
}
return $user;
}
And the user entity:
class User implements AdvancedUserInterface, \Serializable {
....
protected $roles;
....
public function __construct() {
$this->salt = base_convert(sha1(uniqid(mt_rand(), true)), 16, 36);
$this->roles = array();
}
....
public function getRoles() {
$roles = $this->roles;
// Ensure we having something
$roles[] = static::ROLE_DEFAULT;
return array_unique($roles);
}
public function addRole($role) {
$role = strtoupper($role);
$roles = $this->getRoles();
if ($role === static::ROLE_DEFAULT) {
return $this;
}
if (!in_array($role, $roles, true)) {
$this->roles[] = $role;
}
return $this;
}
public function hasRole($role) {
$role = strtoupper($role);
$roles = $this->getRoles();
return in_array($role, $roles, true);
}
}
This works fine and dandy and I see the correct roles when I do:
$this->get('security.context')->getUser()->getRoles()
The problem (I think), is that the token does not know about these roles. Because calling getRoles() on the token is showing only ROLE_USER, which is the default role.
It seems to me that the token is being created before the user is loaded by the UserProvider. I've looked through a lot of the security component but I can't for the life of me find the right part of the process to hook into to set these roles correctly so that the token knows about them.
Update Following the Load roles from database doc works fine, but this does not match my use case as shown here. My schema differs as each role has additional permissions (view/add/edit/delete) and this is why I am attempting the approach here. I don't want to have to alter my schema just to work with Symfony's security. I'd rather understand why these roles are not properly bound (not sure the correct doctrine word here) on my user object at this point.
It looks like you may not be aware of the built in role management that Symfony offers. Read the docs - Managing roles in the database It is actually quite simple to do what you want, all you need to do is implement an interface and define your necessary function. The docs I linked to provide great examples. Take a look.
UPDATE
It looks like the docs don't give you the use statement for the AdvancedUserInterface. Here it is:
// in your user entity
use Symfony\Component\Security\Core\User\AdvancedUserInterface;
then in your role entity:
use Symfony\Component\Security\Core\Role\RoleInterface;
The docs show you how to do the rest.
UPDATE
Take a look at this blog post, which shows how to create roles dynamically:
Dynamically create roles
The problem here stemmed from the fact that I thought I was implementing
Symfony\Component\Security\Core\User\EquatableInterface;
but wasn't (as you can see in the original question, I forgot to add it to my class definition). I'm leaving this here for people if they come across it. All you need is to implement this interface, and add the following method to your user entity.
public function isEqualTo(UserInterface $user) {
if ($user instanceof User) {
// Check that the roles are the same, in any order
$isEqual = count($this->getRoles()) == count($user->getRoles());
if ($isEqual) {
foreach($this->getRoles() as $role) {
$isEqual = $isEqual && in_array($role, $user->getRoles());
}
}
return $isEqual;
}
return false;
}

How does Symfony Security work?

Please, I need to understand how security works and if it can be overriden.
I've read a lot of Symfony Book and Cookbook, and I'd like to implement my own security access check, can that be done? Because it lacks some functionality in roles, like having a constraint of type "if is.author then canedit"
Is it hard to implement? Does FOS UserBundle have this functionality? (Not shown in Docs).
Thanks!
You can implement symfony2 Voters to define access right :
http://symfony.com/doc/2.0/cookbook/security/voters.html
http://kriswallsmith.net/post/15994931191/symfony2-security-voters
Lets create our Voter class :
class PostAuthorVoter implements VoterInterface
{
public function supportsAttribute($attribute)
{
return 'POST_AUTHOR' === $attribute;
}
public function supportsClass($class)
{
return $class instanceof Post;
}
public function vote(TokenInterface $token, $object, array $attributes)
{
// $attributes is an array so we do a foreach loop
foreach ($attributes as $attribute)
{
// if $attribute is POST_AUTHOR and $object is an instance of Post
if ($this->supportsAttribute($attribute) && $this->supportsClass($object))
{
$user = $token->getUser();
// assuming that $posts in an \Doctrine\Common\Collections\ArrayCollection
// we check that user's posts contains the current $object
if ($user->getPosts()->contains($object))
{
return VoterInterface::ACCESS_GRANTED;
}
else
{
return VoterInterface::ACCESS_DENIED;
}
}
}
return VoterInterface::ACCESS_ABSTAIN;
}
}
Then you will be able to call the isGranted method of the security component in your controller like this :
if (!$this->get('security.context')->isGranted('POST_AUTHOR', $post)) {
throw new AccessDeniedException();
}

Categories