How does Symfony Security work? - php

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();
}

Related

dynamic category level in laravel

I have 3 level deep categories in my laravel application like
Parent
- Child One
-- Child Two
When I use this nested categories in different parts such as menu, posts details etc. everything is just fine but recently I came cross an issue and I need guide to solve it.
The issue
If any of my posts includes child one or child two level category it's a bit hard to provide correct route path for it, EXAMPLE:
Parent route : site.com/p/slug
Child one route: site.com/parent_slug/childOne_slug
Child two route: site.com/parent_slug/childOne_slug/childTwo_slug
creating this much if statement in blade to make sure we get the right route for the right categories to me doesn't seem right. I was thinking about model function which returns final route depend on category parenting level in database but I wasn't sure if it's possible or not? and if it is, how?
Question
Is it possible I pass category routes to the categories from model?
How to do that?
Code
Category model
protected $fillable = [
'title', 'slug', 'thumbnail', 'publish','mainpage', 'parent_id', 'color', 'template'
];
public function posts(){
return $this->belongsToMany(Post::class, 'post_categories');
}
public function parent() {
return $this->belongsTo(Category::class, 'parent_id');
}
public function children() {
return $this->hasMany(Category::class, 'parent_id');
}
this is how currently i'm getting my posts categories:
#foreach($post->categories as $category)
<a class="post-cat" href="#">{{$category->title}}</a>
#endforeach
Any idea?
UPDATE
Well I solved it :-D here is the code I've made
public function getCatSlug(){
if($this->parent_id != ''){ //child one
if($this->parent->parent_id != ''){ //child two
if($this->parent->parent->parent_id != ''){ //child three
return $this->parent->parent->parent->slug. '/'. $this->parent->parent->slug. '/'. $this->parent->slug. '/'. $this->slug;
}
return $this->parent->parent->slug. '/'. $this->parent->slug. '/'. $this->slug;
}
return $this->parent->slug. '/'. $this->slug;
}
return $this->slug;
}
This does exactly what I needed it return slugs by orders like parent/child1/child2
Issue
the issue here is now routing this dynamic path as the result of this function I can now have any deep level of path and this needs to be dynamic in routes as well.
Route
my current route is like:
Route::get('/category/{slug}', 'Front\CategoryController#parent')->name('categoryparent');
which returns this path:
site.com/category/parent
but it doesn't return:
site.com/category/parent/child_one
site.com/category/parent/child_one/child_two
Controller
public function parent($slug){
$category = Category::where('slug', $slug)->with('children')->first();
$category->addView();
$posts = $category->posts()->paginate(8);
return view('front.categories.single', compact('category','posts'));
}
any idea?
Update 2
based on Matei Mihai answer I've made custom classes in App/Helpers folder with details below:
CategoryRouteService.php
<?php
namespace App\Helpers;
use App\Category;
class CategoryRouteService
{
private $routes = [];
public function __construct()
{
$this->determineCategoriesRoutes();
}
public function getRoute(Category $category)
{
return $this->routes[$category->id];
}
private function determineCategoriesRoutes()
{
$categories = Category::all()->keyBy('id');
foreach ($categories as $id => $category) {
$slugs = $this->determineCategorySlugs($category, $categories);
if (count($slugs) === 1) {
$this->routes[$id] = url('p/' . $slugs[0]);
}
else {
$this->routes[$id] = url('/' . implode('/', $slugs));
}
}
}
private function determineCategorySlugs(Category $category, Collection $categories, array $slugs = [])
{
array_unshift($slugs, $category->slug);
if (!is_null($category->parent_id)) {
$slugs = $this->determineCategorySlugs($categories[$category->parent_id], $categories, $slugs);
}
return $slugs;
}
}
and CategoryServiceProvider.php
<?php
namespace App\Helpers;
use App\Helpers\CategoryRouteService;
class CategoryServiceProvider
{
public function register()
{
$this->app->singleton(CategoryRouteService::class, function ($app) {
// At this point the categories routes will be determined.
// It happens only one time even if you call the service multiple times through the container.
return new CategoryRouteService();
});
}
}
then I registered my provider to composer.json file like:
"autoload": {
"classmap": [
"database/seeds",
"database/factories"
],
"psr-4": {
"App\\": "app/"
},
"files": [
"app/helpers/CategoryServiceProvider.php" //added here
]
},
I also dumped autoloads and added this to my Category model
use App\Helpers\CategoryRouteService;
public function getRouteAttribute()
{
$categoryRouteService = app(CategoryRouteService::class);
return $categoryRouteService->getRoute($this);
}
then I used {{$category->route}} as my categries href attribute and I got this:
Argument 2 passed to App\Helpers\CategoryRouteService::determineCategorySlugs() must be an instance of App\Helpers\Collection, instance of Illuminate\Database\Eloquent\Collection given
which is refers to:
private function determineCategorySlugs(Category $category, Collection $categories, array $slugs = [])
{
ideas?
It is possible, but you must take care of the script performance because it could end up having a lot of DB queries done to determine each category route.
My recommendation would be to create a CategoryRouteService class which will be registered as singleton to keep the database queries as low as possible. It could be something like:
class CategoryRouteService
{
private $routes = [];
public function __construct()
{
$this->determineCategoriesRoutes();
}
public function getRoute(Category $category)
{
return $this->routes[$category->id];
}
private function determineCategoriesRoutes()
{
$categories = Category::all()->keyBy('id');
foreach ($categories as $id => $category) {
$slugs = $this->determineCategorySlugs($category, $categories);
if (count($slugs) === 1) {
$this->routes[$id] = url('/p/' . $slugs[0]);
}
else {
$this->routes[$id] = url('/' . implode('/', $slugs));
}
}
}
private function determineCategorySlugs(Category $category, Collection $categories, array $slugs = [])
{
array_unshift($slugs, $category->slug);
if (!is_null($category->parent_id)) {
$slugs = $this->determineCategorySlugs($categories[$category->parent_id], $categories, $slugs);
}
return $slugs;
}
}
Now as I said before, you need a service provider to register this service. It should look like this:
class CategoryServiceProvider
{
public function register()
{
$this->app->singleton(CategoryRouteService::class, function ($app) {
// At this point the categories routes will be determined.
// It happens only one time even if you call the service multiple times through the container.
return new CategoryRouteService();
});
}
}
This service provider must be added in the app configuration file.
The Category model could have a method which defines the route attribute:
class Category extends Model
{
public function getRouteAttribute()
{
$categoryRouteService = app(CategoryRouteService::class);
return $categoryRouteService->getRoute($this);
}
/** Your relationships methods **/
}
Finally, you can use it in your blade views simply by using $category->route.
#foreach($post->categories as $category)
<a class="post-cat" href="{{$category->route}}">{{$category->title}}</a>
#endforeach
Please note that there could be other solutions better than this. I just came across this one without thinking too much. Also, the code above was not tested so please be aware that it might need some minor changes to make it work properly.

check with voters single field in symfony

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

Filtering Eloquent

I am trying to filter my Eloquent model collections based on if a user has access to the model or not.
My current method works, but it is really slow so I am wondering if there is a more performant way to do it?
I have a userHasAccess() method on every model in the collection.
It uses Laravel's ACL Features to determine if the user has access to the model:
public function userHasAccess()
{
if (Auth::user()->can('show', $this)) {
return true;
}
return false;
}
I then override the newCollection() method on the model:
public function newCollection(array $models = Array())
{
$collection = new Collection($models);
$collection = $collection->filter(function($model)
{
if($model->userHasAccess())
return true;
});
return $collection;
}
The policy method looks like this:
public function show(User $user, Quote $quote)
{
if(!$quote->customer)
return false;
if(($user->id === $quote->user_id))
return true;
if($user->hasRole(['super-admin','admin']))
return true;
return false;
}
Is there a better way to do this? Especially in terms of performance?
You could add the logic to the query and speed it up dramatically
$query = User::query();
if(!Auth::user()->hasRole(['super-admin','admin'])){
$query->where('user_id','=',Auth::id);
}
$data = $query->get();
You could do this on a wider scale using a scope
class User extends Model
{
public function scopeLimitByUser($query)
{
if(!Auth::user()->hasRole(['super-admin','admin'])){
$query->where('user_id','=',Auth::id);
}
}
}
Then for the quote customer you can add a where to the query
$query->whereNotNull('customer_id');

Laravel mass assignment to model in Update method in abstract class using foreach loops to fill keys with values

How can I be able to create a generic method of the update() function that belongs to an abstract class. To be precise I want it to be able to generate the array $keys and link with the updated $values dynamically, so that different repositories can use this function from this abstract class.
public function update($id, array $data)
{
$related = $this->modelClassInstance->find($id);
$related->name = $data['name'];
$related->description = $data['description'];
$related->started_at = $data['started_at'];
$related->ended_at = $data['ended_at'];
if($related->save()) {
$response = MyResponse::error(201, true, 'Event sucessfully updated');
return $response;
}
else {
$response = MyResponse::error(200, false, 'Event unsucessfully updated');
return $response;
}
}
I need something like this
$data = array(1,2,3,4,5,6,7,8,9);
foreach($data as $key=>$value)
{
.....
}
My keys are the name, description, started_at and ended_at, but that means this function is specific to a model . I want this keys to be generated dynamicaly and to get their new values from the data passed. So that any model with any keys can use it.
The for loop was unneccesary with Laravel methods which are quite reliable, after doing research in various sites. I came up with this simple solution
public function update($id, array $data)
{
$related = $this->modelClassInstance->find($id);
$related->fill($data);
if($related->save())
{
$response = MyResponse::error(201, true, 'Event sucessfully updated');
return $response;
}
else
{
$response = MyResponse::error(200, false, 'Event unsucessfully updated');
return $response;
}
}
This is how the fill() method is implemented in Laravel Eloquent Model, in case you seek similar implementation.
public function fill(array $attributes)
{
$totallyGuarded = $this->totallyGuarded();
foreach ($this->fillableFromArray($attributes) as $key => $value)
{
$key = $this->removeTableFromKey($key);
// The developers may choose to place some attributes in the "fillable"
// array, which means only those attributes may be set through mass
// assignment to the model, and all others will just be ignored.
if ($this->isFillable($key))
{
$this->setAttribute($key, $value);
}
elseif ($totallyGuarded)
{
throw new MassAssignmentException($key);
}
}
return $this;
}

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;
}

Categories