PHP 7.3
Laravel 5.8
Laravel Backpack 3.6
I am trying to use the middlware 'role:admin' within my routes/backpack/permissionmanager.php file, to restrict access to the User, Roles and Permissions areas of Backpack to a subset of users with certain roles.
I have made sure that my User account has been granted the correct role.
My 'user' model in config/backpack/permissionmanager.php is set to App\User::class and my User model has and uses the necessary traits as outlined in the documentation.
I have placed a role Middleware into my app, as follows:
<?php
namespace App\Http\Middleware;
use Closure;
class RoleMiddleware
{
/**
* Handle an incoming request.
*
* #param \Illuminate\Http\Request $request
* #param \Closure $next
* #return mixed
*/
public function handle($request, Closure $next, $role)
{
if (backpack_auth()->guest()) {
return redirect('login');
}
if (! backpack_user()->hasRole($role)) {
abort(403);
}
return $next($request);
}
}
However, it seems that this middleware's backpack_user(), while knowing who I am through the correct return of the ->name property, has absolutely no idea of the roles or permissions that I am supposed to have assigned to myself. I have checked this using the ->getRoleNames() method and it returns an empty collection.
Within the database, the correct entries and IDs are set within the model_has_roles table for my User account and the Role I want.
However, navigating to myapp.dev/admin/user results in a 403 Forbidden.
I think this might be a bug, or something I must not be seeing correctly...?
Related
Backpack controllers do not contain Rest methods as is typical with Laravel, but use traits to implement CRUD operations, and occasionally (but not always - delete does not for example) setup methods (setupListOperation for example).
For authorization, for the rest of my app I use Gate declarations in AppServiceProvider, and declare $this->authorize() to check authorization in each of my controllers.
Where can I use authorize() to check each of the operations I implement from Backpack? I couldn't find a method that seemed appropriate to override in order to run that authorization before proceeding.
You will normally do this in your FormRequest classes, see https://backpackforlaravel.com/docs/4.1/crud-tutorial#the-request
Example:
<?php
namespace App\Http\Requests;
use App\Http\Requests\Request;
use Illuminate\Foundation\Http\FormRequest;
class TagRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
// only allow updates if the user is logged in
return backpack_auth()->check();
}
}
Then you'd set the request as a validator for the given opperation:
Example
protected function setupCreateOperation()
{
$this->crud->setValidation(TagRequest::class);
// TODO: remove setFromDb() and manually define Fields
$this->crud->setFromDb();
}
NOTE: While its not clear in the documentation or generated controllers (if you use the command line generator) you can in fact set a setup method for ALL opperations:
If you look at the packages allin.com/vendor/backpack/crud/src/app/Http/Controllers/CrudController.php file, in the setupConfigurationForCurrentOperation method you'll find:
/**
* Load configurations for the current operation.
*
* Allow developers to insert default settings by creating a method
* that looks like setupOperationNameOperation (aka setupXxxOperation).
*/
protected function setupConfigurationForCurrentOperation()
{
$operationName = $this->crud->getCurrentOperation();
$setupClassName = 'setup'.Str::studly($operationName).'Operation';
//.....
/*
* THEN, run the corresponding setupXxxOperation if it exists.
*/
if (method_exists($this, $setupClassName)) {
$this->{$setupClassName}();
}
}
This means that if your controller defines a setupDeleteOperation function, it WILL be called during the setup of the delete route for your CRUD.
After making use of #Wesley Smith's answer, I discovered a one-step approach to this.
As Wesley mentions, you can create setup methods for all of the crud operations, and this works as an excellent place to pass an auth. However, it does not update the other operation's links. For example, list will still contain a link to "edit," even if it's unauthorized. You can remove these with individual lines, but there's an easier way.
Instead, you can use the Setup method to pass allow/deny methods. Here's what my setup() now appears as.
public function setup()
{
CRUD::setModel(Workshop::class);
CRUD::setRoute(config('backpack.base.route_prefix') . '/workshop');
CRUD::setEntityNameStrings('workshop', 'workshops');
if (Gate::denies('admin.workshop.list'))
$this->crud->denyAccess('list');
if (Gate::denies('admin.workshop.show'))
$this->crud->denyAccess('show');
if (Gate::denies('admin.workshop.create'))
$this->crud->denyAccess('create');
if (Gate::denies('admin.workshop.update'))
$this->crud->denyAccess('update');
if (Gate::denies('admin.workshop.delete'))
$this->crud->denyAccess('delete');
}
This will not only deny access to the methods, but update each method with the appropriate #can blade directives, meaning unauthorized methods won't appear as links.
on official Api-Platform website there is a General Design Considerations page.
Last but not least, to create Event Sourcing-based systems, a convenient approach is:
to persist data in an event store using a custom data persister
to create projections in standard RDBMS (Postgres, MariaDB...) tables or views
to map those projections with read-only Doctrine entity classes and to mark those classes with #ApiResource
You can then benefit from the built-in Doctrine filters, sorting, pagination, auto-joins, etc provided by API Platform.
So, I tried to implement this approach with one simplification (one DB is used, but with separated reads and writes).
But failed... there is a problem, which I don't know how to resolve, so kindly asking you for a help!
I created a User Doctrine entity and annotated fields I want to expose with #Serializer\Groups({"Read"}). I will omit it here as it's very generic.
User resource in yaml format for api-platform:
# config/api_platform/entities/user.yaml
App\Entity\User\User:
attributes:
normalization_context:
groups: ["Read"]
itemOperations:
get: ~
collectionOperations:
get:
access_control: "is_granted('ROLE_ADMIN')"
So, as it's shown above User Doctrine entity is read-only, as only GET methods are defined.
Then I created a CreateUser DTO:
# src/Dto/User/CreateUser.php
namespace App\Dto\User;
use App\Validator as AppAssert;
use Symfony\Component\Validator\Constraints as Assert;
final class CreateUser
{
/**
* #var string
* #Assert\NotBlank()
* #Assert\Email()
* #AppAssert\FakeEmailChecker()
*/
public $email;
/**
* #var string
* #Assert\NotBlank()
* #AppAssert\PlainPassword()
*/
public $plainPassword;
}
CreateUser resource in yaml format for api-platform:
# config/api_platform/dtos/create_user.yaml
App\Dto\User\CreateUser:
itemOperations: {}
collectionOperations:
post:
access_control: "is_anonymous()"
path: "/users"
swagger_context:
tags: ["User"]
summary: "Create new User resource"
So, here you can see that only one POST method is defined, exactly for creation of a new User.
And here what router shows:
$ bin/console debug:router
---------------------------------- -------- -------- ------ -----------------------
Name Method Scheme Host Path
---------------------------------- -------- -------- ------ -----------------------
api_create_users_post_collection POST ANY ANY /users
api_users_get_collection GET ANY ANY /users.{_format}
api_users_get_item GET ANY ANY /users/{id}.{_format}
I also added a custom DataPersister to handle POST to /users. In CreateUserDataPersister::persist I used Doctrine entity to write data, but for this case it doesn't matter as Api-platform do not know anything about how DataPersister will write it.
So, from the concept - it's a separation of reads and writes.
Reads are performed by Doctrine's DataProvider shipped with Api-platform, and writes are performed by custom DataPersister.
# src/DataPersister/CreateUserDataPersister.php
namespace App\DataPersister;
use ApiPlatform\Core\DataPersister\DataPersisterInterface;
use App\Dto\User\CreateUser;
use App\Entity\User\User;
use Doctrine\ORM\EntityManagerInterface;
class CreateUserDataPersister implements DataPersisterInterface
{
private $manager;
public function __construct(EntityManagerInterface $manager)
{
$this->manager = $manager;
}
public function supports($data): bool
{
return $data instanceof CreateUser;
}
public function persist($data)
{
$user = new User();
$user
->setEmail($data->email)
->setPlainPassword($data->plainPassword);
$this->manager->persist($user);
$this->flush();
return $user;
}
public function remove($data)
{
}
}
When I perform a request to create new User:
POST https://{{host}}/users
Content-Type: application/json
{
"email": "test#custom.domain",
"plainPassword": "123qweQWE"
}
Problem!
I'm getting a 400 response ... "hydra:description": "No item route associated with the type "App\Dto\User\CreateUser"." ...
However, a new record is added to database, so custom DataPersister works ;)
According to General Design Considerations separations of writes and reads are implemented, but not working as expected.
I'm pretty sure, that I could be missing something to configure or implement. So, that's why it's not working.
Would be happy to get any help!
Update 1:
The problem is in \ApiPlatform\Core\Bridge\Symfony\Routing\RouteNameResolver::getRouteName(). At lines 48-59 it iterates through all routes trying to find appropriate route for:
$operationType = 'item'
$resourceClass = 'App\Dto\User\CreateUser'
But $operationType = 'item' is defined only for $resourceClass = 'App\Entity\User\User', so it fails to find the route and throws an exception.
Update 2:
So, the question could sound like this:
How it's possible to implement separation of reads and writes (CQS?) using Doctrine entity for reads and DTO for writes, both residing on the same route, but with different methods?
Update 3:
Data Persisters
store data to other persistence layers (ElasticSearch, MongoDB, external web services...)
not publicly expose the internal model mapped with the database through the API
use a separate model for read operations and for updates by implementing patterns such as CQRS
Yes! I want that... but how to achieve it in my example?
Short Answer
The problem is that the Dto\User\CreateUser object is getting serialized for the response, when in fact, you actually want the Entity\User to be returned and serialized.
Long Answer
When API Platform serializes a resource, they will generate an IRI for the resource. The IRI generation is where the code is puking. The default IRI generator uses the Symfony Router to actually build the route based on the API routes created by API Platform.
So for generating an IRI on an entity, it will need to have a GET item operation defined because that is the route that will be the IRI for the resource.
In your case, the DTO doesn't have a GET item operation (and shouldn't have one), but when API Platform tries to serialize your DTO, it throws that error.
Steps to Fix
From your code sample, it looks like the User is being returned, however, it's clear from the error that the User entity is not the one being serialized.
One thing to do would be to install the debug-pack, start the dump server with bin/console server:dump, and add a few dump statements in the API Platform WriteListener: ApiPlatform\Core\EventListener\WriteListener near line 53:
dump(["Controller Result: ", $controllerResult]);
$persistResult = $this->dataPersister->persist($controllerResult);
dump(["Persist Result: ", $persistResult]);
The Controller Result should be an instance of your DTO, the Persist Result should be an instance of your User entity, but I'm guessing it's returning your DTO.
If it is returning your DTO, you need to just debug and figure out why the DTO is being returned from the dataPersister->persist instead of the User entity. Maybe you have other data persisters or things in your system that can be causing conflict.
Hopefully this helps!
You need to send the "id" in your answer.
If User is Doctrine entity, use:
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
*/
private $id;
If User isn't Doctrine entity, use:
/**
* #Assert\Type(type="integer")
* #ApiProperty(identifier=true)
*/
private $id;
Anyway, your answer would be like this:
{
"id": 1, // Your unique id of User
"email": "test#custom.domain",
"plainPassword": "123qweQWE"
}
P.S.: sorry for my english :)
Only Work in the 2.4 version but really helpful.
Just add
output_class=false for the CreateUserDTO and everything will be fine for POST|PUT|PATCH
output_class to false allow you to bypass the get item operation. You can see that in the ApiPlatform\Core\EventListener#L68.
I am trying to create access restrictions to a folder or resource inside of a section in a Moodle course using a Script. This restriction must be for groups of users and if you don't belong to that group you can not see the existing resources.
The operation programmatically I want to do is as follows:
I searched for information and there is very little documentation, in the documentation only puts how to do just that by the web link.
I know how to create groups, folders and sections programmatically from script, but I can not identify the tables to be used for these restrictions or what are the steps to follow.
So if anyone knows how to do it or have any examples or documentation that may be useful, it would be helpful.
Thanks in advance.
The classes which will be used to create the UI and check whether a user has access to your resource are located at:
availability/condition/group/classes/condition.php
availability/condition/group/classes/frontend.php
The data related to the conditions are formatted to JSON from the UI in Javascript, then sent and saved. My first guess would be that you need to recreate the JSON structure and save it into the table/column course_modules::availability. Once that is done I think you'll have to purge the cache from cm_info where the availability data is used to confirm whether the current user can access your resource.
update_moduleinfo - Saves the module form submitted data
cm_info - Checks whether the conditions are met
I followed your advice #FMC and I have done this function to my script that is responsible for giving permits to a group for a particular section of a course
and this is the code:
/**
* giving permits to a group for a particular section of a course
*
* #param $course course that contains the section to restrict access
* #param $sectionid id of the section to restrict access
* #param $groupid id of the group will have access
* #param $module id of the mdl_module to restrict access
*
*/
function grantPermission($course, $sectionid, $groupid, $module ){
global $DB;
$restriction = '{"op":"&","c":[{"type":"group","id":'. $groupid .'}],"showc":[true]}';
$cm= $DB->get_record('course_modules', array('course' => $course , 'section' => $sectionid, 'module' => $module ), '*', MUST_EXIST);
$course_module = new stdClass();
$course_module->id = $cm->id;
$course_module->course = $course;
$course_module->section = $sectionid;
$course_module->availability = $restriction;
$res = $DB->update_record('course_modules', $course_module);
if($res)
rebuild_course_cache($course, true);
return $res;
}
Can you check if is this what you meant #FMC? It works but I don't know if this is the best way.
Thank you!
An Enrol Plugin in Moodle 1.9x could have a setup_enrolments(&$user) method to set user enrolments. This method was called by the lib when user log in.
Does Moodle 2.x uses a similar method?
I couldn't find any call to setup_enrolments() method. Must it have another name?
Yes, I believe enrol_plugin::sync_user_enrolments() does it. From lib/enrollib.php:
/**
* Forces synchronisation of user enrolments.
*
* This is important especially for external enrol plugins,
* this function is called for all enabled enrol plugins
* right after every user login.
*
* #param object $user user record
* #return void
*/
public function sync_user_enrolments($user) {
// override if necessary
}
I want to create multiple instances of Zend_Auth class as I have two modules
Admin
Front
What's happening is when I login into Admin, it automatically get logged into
Front or vice-versa.
What I want is to work in both modules separately after simultaneous authentication.
Zend_Auth is a singleton, so you can't. What I do is to use Zend_Acl ensure that only users with a role of "admin" can get at the administration stuff.
To create a second Auth object, in principle, you could derive Zend_Auth to App_Auth and use a different session namespace. I've never tried this, but my starting code would look like this:
class App_Auth
{
/**
* Returns the persistent storage handler
*
* Session storage is used by default unless a different storage adapter has been set.
*
* #return Zend_Auth_Storage_Interface
*/
public function getStorage()
{
if (null === $this->_storage) {
/**
* #see Zend_Auth_Storage_Session
*/
require_once 'Zend/Auth/Storage/Session.php';
$this->setStorage(new Zend_Auth_Storage_Session('App_Auth'));
}
return $this->_storage;
}
}
It's possible that you have to override more.