I have a Symfony 3 app that uses Doctrine ORM for Entity management. Currently, I am working on enabling CRUD support. I've already found out that I can use security voters to restrict access to entities or controllers. For example, I configured it the way that only admins can create, update or delete entities of type A.
For instances of my entity type B I also want to give the respective owner the power to update (not create or delete), which I managed to do easily. However, an owner shouldn't be allowed to modify all of the entity's properties - just some of them. How can I realize this with Symfony? Also, I am using the Form Bundle to create and validate forms.
EDIT: I added some related code. The controller invokes denyAccessUnlessGranted, which triggers the voter. Just to clarify, that code works fine already. My question is related to code I don't yet have.
Controller:
public function editAction(Request $request, int $id) {
$em = $this->getDoctrine()->getManager();
$project = $em->getRepository(Project::class)->findOneBy(['id'=>$id]);
$this->denyAccessUnlessGranted(ProjectVoter::EDIT, $project);
$users = $em->getRepository(EntityUser::class)->findAll();
$groups = $em->getRepository(Group::class)->findAll();
$tags = $em->getRepository(Tag::class)->findAll();
$form = $this->createForm(ProjectType::class, $project, [
'possibleAdmins' => $users,
'possibleRequiredGroups' => $groups,
'possibleTags' => $tags,
]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$project = $form->getData();
$em->flush();
return $this->redirectToRoute('projects_show', ['id'=>$project->getId()]);
}
return $this->render('project/editor.html.twig',
['project'=>$project, 'form'=>$form->createView()]);
}
Voter:
protected function voteOnAttribute($attribute, $subject, TokenInterface $token) {
/** #var UserInterface $user */
$user = $token->getUser();
if (!$user instanceof UserInterface) {
// the user must be logged in; if not, deny access
return false;
}
else if ($this->decisionManager->decide($token, ['ROLE_ADMIN'])) {
return true; // system-wide admins shall always have access
}
switch($attribute) {
case self::SHOW:
return ($subject->isVisible() || $subject->getAdmins()->contains($user);
case self::EDIT:
return $subject->getAdmins()->contains($user);
case self::REMOVE:
return false;
}
return false;
}
As far as I know there is no access functionality specifically related to individual properties. Of course as soon as I post this, someone else will come by with exactly that.
What you might consider doing is to define two edit roles, EDIT_BY_ADMIN and EDIT_BY_OWNER. You could then test the condition and select which form type to use.
$projectTypeClass = null;
if ($this->isGranted(ProjectVoter::EDIT_BY_ADMIN,$project)) {
$projectTypeClass = ProjectAdminType::class);
}
elseif ($this->isGranted(ProjectVoter::EDIT_BY_OWNER,$project)) {
$projectTypeClass = ProjectOwnerType::class);
}
if (!$projectTypeClass) {
// throw access denied exception
}
$form = $this->createForm($projectTypeClass, $project, [
And that should do the trick. There are of course many variations. You could stick with one project type and do the access testing within the type class though that would require a form listener.
If you need more granularity then you could instead add some EDIT_PROP1, EDIT_PROP2 type roles.
And of course if you were really into it then you could move some of the access code into a database of some sort. Or maybe take a look at some of the Access Control List components out there.
I came up with this solution in the end:
Instead of having multiple FormTypes I stuck with only a single one and ended up enabling or disabling the property's form field based on the result of the voter. For that I defined a new switch case as Cerad suggested (named ProjectVoter::MODIFY_PROTECTED_PROPERTY in this answer for demonstration purposes) and added the business logic per my liking.
I just enabled or disabled the form field because I actually want the user to see that he/she can't edit that property. But it would likely easily be possible to not add the field in the first place as well, so it's not visible.
Form Type:
Info: $this->tokenStorage and $this->accessDecisionManager are injected services ("security.token_storage" and "security.access.decision_manager" respectively).
public function buildForm(FormBuilderInterface $builder, array $options) {
$token = $options['token'] ?? $this->tokenStorage->getToken();
$project = $builder->getData();
$builder
->add('name')
// ...
->add('protectedProperty', null, [
'disabled' => !$this->accessDecisionManager->decide($token, [ProjectVoter::MODIFY_PROTECTED_PROPERTY], $project),
])
;
}
I also added an option to the form type called token in its configureOptions function which defaults to null, so that the form can be built for an arbitrary user instead of the one currently logged-in, if required.
Related
I'm working on an app that requires multi-authentication, I used creative-tim preset for the dashboard (providing that creative-tim uses the "User" name for storing users in the dashboard) I created on top of that an Admin table that has a one-to-one relationship with the User table, and created the appropriate middleware to restrict access to my "Admin-only" views and used tinker to create an Admin and link it to a test everything.
the problem now is that I want to automate the creation of the admin relationship with the user through a registration form, only problem is, the creative-tim template I'm using doesn't follow the laravel docs so I couldn't understand it and I don't know how to link an "Admin" model to the created "User".
Here's a part of the database/model:
User(id,name,email,email_verified_at,password,rememberToken,timestamps)
Admin(id,user_id,role,timestamps)
the way they used is (obviously store) but they used a UserRequest and User Object, I tried doing this:
$Admin = new Admin();
$Admin->role="Admin";
$Admin->user_id = $model->id;
$Admin->save();
here's the original function I found
public function store(UserRequest $request, User $model)
{
$model->create($request->merge(['password' =>Hash::make($request->get('password'))])->all());
return redirect()->route('user.index')->withStatus(__('User successfully created.'));
}
I actually found a solution for the problem (that wasn't my preferred) so I created a user normally and got the id, used it to create an Admin, Works Perfectly after validation
public function store(UserRequest $request, User $model)
{
//validation parameters
$item = new User();
$item->name = request('name');
$item->email = request('email');
$item->password = Hash::make(request('password'));
$item->save();
$admin = new Admin();
$admin->user_id = $item->id;
$admin->role ="SuperAdmin";
$admin->save();
return redirect()->route('user.index')->withStatus(__('User successfully created.'));
}
I just started PHP
And in the first project (RSS)
I succeeded with the help of this post
https://github.com/ganjali89/RSSproject.git
I want to validate a User entity with custom constraint & validator. So far it's working when triggered by form workflow, but if I trigger it manually, I loose one relation I setup before calling validation :
UserController :
$user = new User();
$user->setRoles($roles);
$user->setSite($site);
...
$violations = $this->container->get('validator')->validate($user);
User entity with Site relation :
/**
* #var Site the site linked to the entity
* #ORM\ManyToOne(targetEntity="LCH\MultisiteBundle\Entity\Site", cascade={"all"})
* #ORM\JoinColumn(name="site_id", referencedColumnName="id", onDelete="CASCADE")
*/
protected $site;
Validator :
public function validate($user, Constraint $constraint)
{
$email = $user->getEmail();
// $site var is null while other "direct fields are filled
$site = $user->getSite();
$roles = $user->getRoles();
$username = $user->getUsername();
How can I manually validate this entity using preceeding set relation?
My problem found its origin in Symfony2 form validation structure. : as $form->handleRequest($request) indeed validates form, all hooked validators (groups, custom constrains and callbacks) are triggered.
My $site was null because validator was fired long before I set up my $user->site attribute...
// sumbit in this method trigger validation too early in my needs
$form->handleRequest($request);
$em = $this->getDoctrine()->getManager();
if ($form->isSubmitted() && $form->isValid() && !$isAjax) {
// custom processes to decide what to create
...
// Here is the user creation
$user->setRoles($roles);
$user->setSite($site);
...
// And the check
$violations = $this->container->get('validator')->validate($user);
}
The solution here lied in disabling validation groups in the main type. Doing so, $user is fully passed to validator.
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'validation_groups' => false,
));
}
The main drawback with this system is that I have to manually trigger validation for this very type in all interactions, but this type is complex enough to make sense here.
In Laravel 5, using a form request to act as a validation gate, and given the code below:
Controller
public function decline(Request $request, InviteDeclineRequest $validation, $id)
{
$invite = Invite::find($id);
$invite->status = 'declined';
$invite->save();
}
FormRequest
class InviteDeclineRequest extends Request {
public function rules()
{
return [
# this is referring to the incoming data,
# not the existing data
'status': 'pending',
];
}
}
How can I change the above validation ruleset to say, the incoming input is only valid if the existing record's status is set to 'pending'. I.e don't allow a declined invite unless the existing is pending.
Option 1:
Put this logic in the controller. Maybe the above isn't considered a part of validation, (although I would argue that it is), and so doesn't belong in the FormRequest.
Option 2:
Put the logic in the Authorise method of method of the FormRequest. The only downside to this is that the authorise should be for access control, not data validation.
Option 3:
Extend form request to include a third method that validates existing data as well as incoming data. Slightly painful as I need to make sure it gets called as part of the request cycle.
Option 4:
Add a custom validation rule:
http://laravel.com/docs/5.0/validation#custom-validation-rules
You can add to your custom InviteDeclineRequest class method all to add id to data that will be validated:
public function all()
{
$data = parent::all();
$segments = $this->segments();
$id = intval(end($segments));
if ($id != 0) {
$data['id'] = $id;
}
return $data;
}
and now in rules you can use it:
public function rules()
{
return [
'id' => ['required', 'exists:invites,id,status,pending']
];
}
to make sure record you edit has status pending.
I'm trying to register a user in my application while keeping all business logic in the model and as little as possible in the controller. To accomplish this, I'm running user validation in the model's boot() method when the Class::creating() event fires. If the validation fails, I simply return false, cancelling the event. You can see this here:
public static function boot() {
parent::boot();
User::creating(function(){
$validator = new Services\Validators\RUser;
if (! $validator->passes()) return false;
});
}
The validator class you see is simply rules and it contains a getErrors() function.
My question is, how can I rewrite this so that I can retrieve the validator's errors for use in a conditional redirect later?
My controller postRegister() (the function called when clicking submit on form) looks like this:
public function postRegister() {
$user = new User(Input::all());
$user->save();
}
I know I'm not handling that in the controller correctly, so I would appreciate some advice with that as well.
Thanks.
You would set a 'protected $errors;' property on the User model, and then
User::creating(function(){
$validator = new Services\Validators\RUser;
if (! $validator->passes()) {
$this->errors = $validation->getErrors();
return false;
}
});
Not a direct answer to your question, but you should check out the Ardent package which is great for automatic model validation and has some other nice accompanying features. Internally it uses native Laravel validators so it's easy to use and will do just what you ask about. It really saves a lot of work (DRY) and I find it very useful.
https://github.com/laravelbook/ardent
The basics from the docs:
Ardent models use Laravel's built-in Validator class. Defining
validation rules for a model is simple and is typically done in your
model class as a static variable:
class User extends \LaravelBook\Ardent\Ardent {
public static $rules = array(
'name' => 'required|between:4,16',
'email' => 'required|email',
'password' => 'required|alpha_num|between:4,8|confirmed',
'password_confirmation' => 'required|alpha_num|between:4,8',
);
}
Ardent models validate themselves automatically when Ardent->save() is
called. You can also validate a model at any time using the
Ardent->validate() method.
$user = new User;
$user->name = 'John doe';
$user->email = 'john#doe.com';
$user->password = 'test';
$success = $user->save(); // returns false if model is invalid
When an Ardent model fails to validate, a
Illuminate\Support\MessageBag object is attached to the Ardent object
which contains validation failure messages.
Retrieve the validation errors message collection instance with
Ardent->errors() method or Ardent->validationErrors property.
Retrieve all validation errors with Ardent->errors()->all(). Retrieve
errors for a specific attribute using
Ardent->validationErrors->get('attribute').
So in the end you can do:
$user = new User;
$user->name = 'John doe';
$user->email = 'john#doe.com';
$user->password = 'test';
if(!$user->save())
{
print_r($user->errors()->all()); //or whatever else you wish to do on failure
}
I installed Laravel for the first time less than 12 hours ago, so i may be acting a little prematurely, but to my knowledge...
You have two main options, return the validator class, or store the errors in the User model. I'm currently working with the former, so i have a validate() method which returns the Validator class, which i then do the if($v->passes()) in my controller and can output errors in the controller via $v->messages().
Using your method, you will want to store your errors in the User object if you want to continue returning false on failure. So you could change:
if (! $validator->passes()) return false;
to
if (! $validator->passes()) {
$this->errors = $validator->messages();
return false;
}
and in your controller do:
if(isset($user->errors)) {
//loop and print errors from validator
}
N.B: just to reiterate, im a complete newbie to laravel so i may have gotten something completely wrong. But if i have, someone will correct me and we'll both have learned something :)
I am trying to add a simple form to allow my users to edit their profile. My problem is:
Since the entity "linked" to the form is the same as the current user object ($user === $entity, see below), if the form validation fails, then the view is rendered with the modified user object (ie. with values of the non-valid form).
Here my (classic) controller:
public function profileAction()
{
$em = $this->getDoctrine()->getEntityManager();
$user = $this->get('security.context')->getToken()->getUser();
$entity = $em->getRepository('AcmeSecurityBundle:User')->find($user->id);
// $user === $entity => true
$form = $this->createForm(new ProfileType(), $entity);
$request = $this->getRequest();
if ($request->getMethod() === 'POST')
{
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('profile'));
}
}
return $this->render('AcmeSecurityBundle:User:profile.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
So I wondered how to have two distincts objects $user and $entity. I used clone() and it worked well for the view rendering part (the $user object was not modified), but it created a new record in database instead of updating the old one.
PS: I know I should use FOSUserBundle. But I would really like to understand my mistake here :)
I used the same solution as FOSUserBundle, which is calling $em->refresh() on my entity when the form validation fails:
public function profileAction()
{
$em = $this->getDoctrine()->getEntityManager();
$user = $this->get('security.context')->getToken()->getUser();
$entity = $em->getRepository('AcmeSecurityBundle:User')->find($user->id);
if (!$entity) {
throw $this->createNotFoundException('Unable to find User entity.');
}
$form = $this->createForm(new ProfileType(), $entity);
$request = $this->getRequest();
if ($request->getMethod() === 'POST')
{
$form->bindRequest($request);
if ($form->isValid()) {
$em->persist($entity);
$em->flush();
return $this->redirect($this->generateUrl('profile'));
}
$em->refresh($user); // Add this line
}
return $this->render('AcmeSecurityBundle:User:profile.html.twig', array(
'entity' => $entity,
'form' => $form->createView(),
));
}
Note that if you are using what is called a "virtual" field in "How to handle File Uploads with Doctrine" (in my case "picture_file" you will need to clear it by hand:
$em->refresh($user);
$user->picture_file = null; // here
One approach would be to always redirect:
if ($form->isValid()) {
$em->persist($entity);
$em->flush();
}
return $this->redirect($this->generateUrl('profile'));
Of course you lose error messages and changes.
Another approach would be to define an entity manager just for your UserProvider. $user would no longer be the same as $entity. Bit of extra overhead but it certainly makes the problem go and would prevent similar interactions with other forms that might modify all or part of an user entity.
In a similar fashion, you reduce the overhead by creating an entity manager just for your profile form. With this method, the overhead would only be incurred when editing the profile.
Finally, you could ask yourself if it really mattered that the display data was not quite right in this particular case. Would it really bother anything? Would anyone notice but you?
See How to work with Multiple Entity Managers in Symfony Cookbook
Another idea is to clone your user entity in your user provider. This will divorce it from the entity manager.
You could also use $entityManager->detach($user); to remove the user from the entity manager.
And why is the token user an entity anyways? Consider making a completely independent User class with minimal information pulled from the database by your user provider. That is what I do.