Remove specific routes per user role in Sonata without SonataUserBundle - php

I make use of SonataAdminBundle in Symfony 3. Because I use Symfony 3, I still can't make use of SonataUserBundle. So I am using SonataAdminBundle with FOSUserBundle only.
Now what I try to achieve is to hide specific routes per role. For example, I only have three roles;
Super Admin
Admin
Another role
Super Admin has all the roles admin has, admin has all of the third one, and the third one has ROLE_USER obviously. Super Admin should be able to create new users and assign a role to him. The Super Admin should also be able to change user's passwords. The users should be able to change the passwords of their own accounts. And finally, other roles that Super Admin should not be able to change their own roles and to create new users.
How can I achieve this without using SonataUserBundle. For the removing of routes part I tried something like this:
protected function configureRoutes(RouteCollection $collection)
{
$securityContext = $this->getConfigurationPool()->getContainer()->get('security.authorization_checker');
if (!$securityContext->isGranted('ROLE_SUPER_ADMIN')) {
$collection->remove('create');
$collection->remove('edit');
}
}
But I guess there is a better solution. I am completely aware of the official documentation about security but I'm confused with that, does that mean I have to hard code each and every single role for all different Admins in my security.yml file? Does this even work without SonataUserBundle? I don't want to add extra database tables for ACL.
Can somebody please assist and/or provide a good example? I'll really appreciate it a lot.

How to manage users and roles in Sonata without SonataUserBundle?
Answer: we need to do the same as SonataUserBundle. (But let's simplify a little)
An analogy about security based on ROLE_ in Symfony flat:
The house: A building that has doors and keys (the system).
The door: Place in the house where access is restricted - isGranted():
// the door is here, we need the key to open it.
if ($this->isGranted('ROLE_FOO')) {
// restricted access to do something
}
The key: Granted permission to access a restricted door - ROLE_*:
class User extends FOSUser
{
public function getRoles()
{
// the keys comes from DB or manually.
// e.g:
return ['ROLE_FOO'];
}
}
The master key: A key that can open several doors:
# app/config/security.yml
security:
role_hierarchy:
# other than opening the door "isGranted('ROLE_BAR')"
# we can also opening the door "isGranted('ROLE_FOO')" with this single key.
ROLE_BAR: ROLE_FOO
Following this analogy, SonataAdminBundle already has created the doors to restrict access to each default action (e.g. list action) across an entity managed.
So our job is to assign the keys to users "only" (unless you need to create your own doors). There are many ways to achieve this (it'll depend on what you need).
Note: If you don't have a role hierarchy, you have single keys only (i.e. you don't have master keys), which makes it less flexible assignment of roles (keys).
Now, SonataAdminBundle uses a particular way to check the keys in a context of admin class, just doing the following: $admin->isGranted('list'), this is because he has his own isGranted() function (where 'list' is the action name), but really what it does is build the role name (by using the current admin code) before check it, so he verify this finally: isGranted('ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_LIST') -this key it's what we need "give" to the user-.
How to get the role list from Sonata admin system?
In a controller context:
public function getSonataRoles()
{
$roles = [];
// the sonata admin container
$pool = $this->get('sonata.admin.pool');
foreach ($pool->getAdminServiceIds() as $id) {
// gets the registered admin instance from id service name
$admin = $pool->getInstance($id);
// the role security handler instance (must be configured)
$securityHandler = $admin->getSecurityHandler();
// gets the base role name from admin code
// e.g. 'ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_%s'
$baseRole = $securityHandler->getBaseRole($admin);
// gets the access actions (e.g. LIST, CREATE, EDIT, etc.)
foreach (array_keys($admin->getSecurityInformation()) as $action) {
// add the final role name
// e.g. 'ROLE_APP_BUNDLE_ADMIN_FOO_ADMIN_LIST'
$roles[] = sprintf($baseRole, $action);
}
}
return $roles;
}
Next, you can do anything with that (e.g. create a custom form type to manage the user roles property). You could to sort, grouping these roles to show the user this list in the simplest possible way.
Up here, we can assign roles and work without using even the role_hierarchy.
More details http://symfony.com/doc/current/bundles/SonataAdminBundle/reference/security.html

You can define a custom user permission Voter for your User entity, see here.
namespace AppBundle\Security;
use AppBundle\Entity\User;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
class UserVoter extends Voter
{
private $decisionManager;
public function __construct(AccessDecisionManagerInterface $decisionManager)
{
$this->decisionManager = $decisionManager;
}
protected function supports($attribute, $subject)
{
// only vote on User objects inside this voter
if (!$subject instanceof User) {
return false;
}
return true;
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
// ROLE_SUPER_ADMIN can do anything! The power!
if ($this->decisionManager->decide($token, array('ROLE_SUPER_ADMIN'))) {
return true;
}
$user = $token->getUser();
if (!$user instanceof User) {
// the user must be logged in; if not, deny access
return false;
}
/** #var User $targetUser */
$targetUser = $subject;
// Put your custom logic here
switch ($attribute) {
case "ROLE_SONATA_ADMIN_USER_VIEW":
return true;
case "ROLE_SONATA_ADMIN_USER_EDIT":
return ($user === $targetUser);
}
return false;
}
}
Then you create the service
sonata_admin.user_voter:
class: AppBundle\Security\UserVoter
arguments: ['#security.access.decision_manager']
public: false
tags:
- { name: security.voter }
Be carefull of the access decision strategy, I may not work depending on your configuration if it's defined to unanimous or consensus
You may also add a direct link/route to the user's own edit page if you don't want to give every user access to the user list.
EDIT
To restrict user role edition, as you don't want a user to edit its own role, you can simply edit the configureFormFields function :
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
->add('username')
->add('plainPassword', 'text', array(
'required' => false,
)
)
/* your other fields */
;
if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$formMapper->add('roles', \Symfony\Component\Form\Extension\Core\Type\CollectionType::class, array(
'entry_type' => \Symfony\Component\Form\Extension\Core\Type\ChoiceType::class,
'entry_options' => array(
'choices' => array(
"ROLE_OPTICKSB2B" => "ROLE_OPTICKSB2B",
"ROLE_ADMIN" => "ROLE_ADMIN",
"ROLE_SUPER_ADMIN" => "ROLE_SUPER_ADMIN"
),
)
));
}
$formMapper
->add('isActive')
->add('title')
->add('firstname')
->add('lastname')
;
}
Obviously, Symfony forms component will check for you than no other field are added.

Related

Dynamically add roles on Symfony 5.4

I'm working on a PHP application where there are two levels of authorizations.
On the first and classic level : the user logs in the application, the authentication system sets his roles and registers them in the auth token all right. Let's say out user has ROLE_USER and ROLE_ADMIN.
Now, for some reason I've been asked to dynamically add another role after authentication...
After the official documentation : https://symfony.com/doc/5.4/security/user_providers.html#creating-a-custom-user-provider
I tried to achieve this with a custom user provider :
security.yaml :
providers:
users_in_memory: { memory: null }
app_user_provider:
id: App\Security\UserProvider
The UserProvider class has its refreshUser() method called with every request :
class UserProvider implements UserProviderInterface, PasswordUpgraderInterface
{
//...
public function refreshUser(UserInterface $user): UserInterface|User
{
if (!$user instanceof User) {
throw new UnsupportedUserException('nope');
}
$refreshedUser = $this->userRepository->find($user->getId());
if (!$refreshedUser) {
throw new UserNotFoundException('nope nope');
}
$refreshedUser->addRole('ROLE_BONUS'); // new role added !
return $refreshedUser;
}
}
Of course, the User class implements the EquatableInterface so the user is not de-authenticated and logged out by Symfony after the refresh.
Unfortunately, even if the auth token has refreshed its user as expected, it has not refreshed the list of roles given by AbstractToken::getRoleNames(). And when I check the granted new role in a controller, the check fails :
class DashboardInfosController extends AbstractController
{
public function renderPageWithBonusRole(): Response
{
$user = $this->getUser(); // this is the refreshed user
$roles = $user->getRoles(); // gives the correct roles, including ROLE_BONUS
$test = $this->isGranted('ROLE_BONUS'); // fails : only ROLE_USER & ROLE_ADMIN are checked here
return $this->render('site/_mypartial.html.twig');
}
}
After the code I see in the AbstractToken class, rolenames are only set in the constructor and never updated after this point. I've been struggling with this for a day and more, now, and still stuck... So, how can I tell the Security layer of Symfony 5.4 that it has to check the new roles list for my user ?

Laravel Spatie permissions how to define set of permission for each user based upon role?

I have 4 types of users using my system, 1. superadmin 2. superadmin team, 3. admin and 4th. admin team members,
as I'm using spatie for roles and permissions, I have set of modules (permissions)which are common for all types of users and there is other set of modules(permissions) which is only for superadmin like payment methods etc.
Now, once seeding my database for permission should I have have to seed all once? ['contacts','email','bids'] with web guard (however I'm bit confuse about exact usage of guards and how it works), so admin can assign permissions to his team only from these allowed permissions however, for superadmin should I create other set of permission with superadmin guard? I would like to know what's best practice.
Use Case: Superadmin firstly login to the system and will decide which permission should have to be given to admin from his list.
2. Admin login to the system and will assign which set of permission will be given to his team but admin won't be able to see list of permissions which superadmin have.
I hope I would have cleared my point please let me know appropriate way for its implementation.
I suppose you are using one model i.e. User and assign permissions directly to users. Here is my approach So, what you can do is, you can first create a role and give appropriate permission to a role and then assign that role to a user.
First, Assign permissions to a role
$role->syncPermissions(['permission-1', 'permission-2', '...']);
Now, sync the role with a user
$user->assignRole('writer');
// Or, you can also assign multiple roles
$user->assignRole('writer', 'admin');
These are the built-in spatie middleware you can write in app/Http/Kernel.php
protected $routeMiddleware = [
// ...
'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class,
];
Now, you can use the 'role' middleware in the routes to protect like,
// for superadmin
Route::group(['middleware' => ['role:superadmin']], function () {
//
});
// for admin
Route::group(['middleware' => ['role:admin']], function () {
//
});
// Or with multiple roles
Route::group(['middleware' => ['role:superadmin|admin']], function ()
{
//
});
...
So, now you need to get permissions for a specific role i.e. superadmin or admin. Here is what you can do,
// get all permissions associated with a role
$role->permissions;
// get specific columns of permissions
$role->permissions->pluck('name');
Also, you can get user roles in this way
auth()->user()->roles;
// Or get only role names
auth()->user()->getRoleNames();
// Or check if user has a specific role
auth()->user()->hasRole('admin')
One More Thing, for superadmin you don't need to get permissions from the role you can get all the permissions directly. And because the superadmin can have access to the whole system then you can bypass the permissions check for the superadmin by doing this,
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
public function boot()
{
$this->registerPolicies();
// Implicitly grant "Super Admin" role all permissions
// This works in the app by using gate-related functions like
// auth()->user->can() and #can()
Gate::before(function ($user, $ability) {
return $user->hasRole('superadmin') ? true : null;
});
}
}
I hope it may help you :)

Symfony2 with FOSUserBundle : Access based on group rights

I can't seem to find documentation on how to use the rules that are linked to a fos_group for access control.
In this project I want to be able to define new groups later on that use predefined roles like: ROLE_USER, ROLE_AMDIN and ROLE_SUPERAMDIN.
On each page is defined what a role can or cannot do.
Normally I use the is_granted function in twig to check the roles, but since I want the system to check the roles of the group first and if the user has no group check the user specific roles, than I won't be able to use it.
Any ideas on how to achieve this in Symfony2 with the FOSUserBundle groups?
I have been trying to make such a work. This is what I found :merging the group roles with default user roles by overriding the getRoles method.
I Hope that this would help someone.
class Users extends BaseUser
{
[...]
public function getRoles()
{
$roles = $this->roles;
foreach ($this->getGroups() as $group) {
$roles = array_merge($roles, $group->getRoles());
}
// we need to make sure to have at least one role
$roles[] = static::ROLE_DEFAULT;
return array_unique($roles);
}
}

Multiple prefixed routes and DRY principle

I use CakePHP 2.2.7
In my app I have a public area and admin area.
I use prefixed routes so for admin actions I use
admin_index() etc.
Now I need to add additional admin area for managers. This manager area will be different in some cases against the admin area.
Different layout, not all actions allowed.
My question is:
Should I simply duplicate actions which already implemented for admin area (and add another prefix, for example manager_index() ) or there is a more simple and DRY solution?
You can do this for example
public function manager_edit($fooId = null) {
$this->admin_edit($fooId);
}
But if you did a good job most of your code should be already in the model and your code look like this (just a basic example);
public function manager_edit($fooId = null) {
if ($this->Foo->edit($fooId, $this->request->params, $this->Auth->user('id')) { /*....*/ }
}

cakephp adding ACL to functional login system

i have built upon this tutorial http://www.jamesfairhurst.co.uk/posts/view/creating_an_admin_section_with_cakephp_updated
and currently have a functional and quite well fleshed out admin section for my application.
Due to poor foresight I haven't taken into account regular users who need to be able to login to their own home page, where they can view bookings etc.
I have an appropriate database set up and have included a 'roles' field for authentication. I have also followed cakePHP's own 'auth' examples however have failed to get them to implement without throwing various errors, at this stage i'm not wanting to go changing the structure of the login system too much, that kind of thing can become a headache quick!!
I have spoken to the original author of the tutorial and he agrees that some simple logic added to the user_controller.php file should suffice.
basically i need something along the lines of an: "if user == 'user' THEN redirect to 'user_index.php' put simply.
below is the current LOGIN function for user_controller.php
function login() {
if(!empty($this->data)) {
// unset unrequired validation rules
unset($this->User->validate['username']['check_username_exists']);
// validate form
$this->User->set($this->data);
if($this->User->validates()) {
// update Last Login date
$this->User->id = $this->User->_user['User']['id'];
$this->User->saveField('last_login',date("Y-m-d H:i:s"));
// save User to Session and redirect
$this->Session->write('User', $this->User->_user);
$this->Session->setFlash('You have successfully logged in.','default',array('class'=>'flash_good'));
$this->redirect(array('action'=>'index','admin'=>TRUE));
}
}
}
All validation is handled in the user.php model and there is some logic in app_controller.php to redirect authentication, it is included below;
app_controller.php
class AppController extends Controller {
// class variables
var $_User = array();
/**
* Before any Controller action
*/
function beforeFilter() {
// if admin url requested
if(isset($this->params['admin']) && $this->params['admin']) {
// check user is logged in
if( !$this->Session->check('User') ) {
$this->Session->setFlash('You must be logged in for that action.','flash_bad');
$this->redirect('/login');
}
// save user data
$this->_User = $this->Session->read('User');
$this->set('user',$this->_User);
// change layout
$this->layout = 'admin';
}
}
}
I faced a similar problem in my application. In my User model, I created a getRole() method which just pulled the role out of the database, and then I used a switch statement to redirect users to the correct controller.
As a different approach, you could just add in an isAdmin column (default 0, 1 would indicate an admin) to your users table. Assuming there are not too many admins already, you could just manually set the admins. In your controller you would just need to check the field and the redirect accordingly.

Categories