In my Laravel application, i have several policies working, but one will not work.
Controller
public function store(Project $project, CreateActionRequest $request)
{
$this->authorize('store', $project);
Action::create([
'name' => $request->name,
]);
return redirect()->route('projects.show', $project->id)->withSuccess('Massnahme erfolgreich gespeichert');
}
Policy
namespace App\Policies\Project;
use App\Models\Project\Project;
use App\Models\User;
use App\Models\Project\Action;
use Illuminate\Auth\Access\HandlesAuthorization;
class ActionPolicy
{
use HandlesAuthorization;
public function store(User $user, Project $project)
{
return $user->company_id === $project->company_id;
}
}
AuthServiceProvider
protected $policies = [
'App\Models\User' => 'App\Policies\CompanyAdmin\UserPolicy',
'App\Models\Company' => 'App\Policies\CompanyAdmin\CompanyPolicy',
'App\Models\Team' => 'App\Policies\CompanyAdmin\TeamPolicy',
'App\Models\Department' => 'App\Policies\CompanyAdmin\DepartmentPolicy',
'App\Models\Location' => 'App\Policies\CompanyAdmin\LocationPolicy',
'App\Models\Division' => 'App\Policies\CompanyAdmin\DivisionPolicy',
'App\Models\Costcenter' => 'App\Policies\CompanyAdmin\CostcenterPolicy',
'App\Models\Workplace' => 'App\Policies\CompanyAdmin\WorkplacePolicy',
'App\Models\Product' => 'App\Policies\CompanyAdmin\ProductPolicy',
'App\Models\Project\Action' => 'App\Policies\Project\ActionPolicy',
'App\Models\Project\Project' => 'App\Policies\Project\ProjectPolicy',
];
CreateActionRequest
namespace App\Http\Requests\Project;
use Illuminate\Foundation\Http\FormRequest;
class CreateActionRequest extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
'name' => 'required|min:3',
];
}
}
All policies are working except ActionPolicy and ProjectPolicy.
I added in the policy a __construct() method to check if the policy is called. But ActionPolicy and ProjectPolicy are not working.
How can i search the error? I tried with dd() but i got only allways the message: This action is unauthorized
Since you are injecting CreateActionRequest instead of Request that means you are defining your own set of rules to authorize the FormRequest which comes inside of your method. Further it means that you gotta define a few rules which the "FormRequest" has to pass in order to EVEN reach your controller, this is a nice concept that I like about Laravel since the code is not centralized, but rather spread and every layer has it's own responsibility. Now, you don't have to call any method from your CreateActionRequest nor you have to write any code regarding that class in your controller, because Laravel runs authorize method by default before allowing the Request to reach your controller, before running authorizemethod in your CreateActionRequest it runs rules method which verifies that all the given fields pass the expressions you assigned them, so the execution is something like this CreateActionRequest => rules => authorize => IF(authorized) Controller ELSE Not authorized, hope that makes sense. In order to fix your code:
1.) Remove $this->authorize('store', $project);
This will allow you to pass not authorized error in case your name passes the truth test inside of rules method inside of your CreateActionRequest. If you wish to utilize your Action Policy you will need to hook up your custom Request(CreateActionRequest) with it and this is how:
public function authorize()
{
$store = $this->route('project');
//The above line will return Project object if your mapping is correct
//If it's not it will return the value you passed to your route for {project}
return $this->user() && $this->user()->can('store', $store);
}
EDIT:
Here is the link where you can see how to properly authorize and connect policy with CreateActionRequest
Do you have all your controller methods defined with the Request object last?
public function store(Project $project, CreateActionRequest $request)
The Request object should be the first parameter in the methods signature:
public function store(CreateActionRequest $request, Project $project)
Dependency Injection & Route Parameters
If your controller method is also expecting input from a route parameter you should list your route parameters after your other dependencies.
Most Laravel authorization mechanisms have identical method signatures allowing them to work across varying classes.
Related
I am trying to create a test for a feature I've written.
The logic is quite simple:
From the api.php I am calling the store method:
Route::group(['prefix' => '/study/{study}/bookmark_list'], function () {
...
Route::post('/{bookmarkList}/bookmark', 'BookmarkController#store');
...
});
thus I am injecting the study and the bookmark list.
My controller passes down the parameters
public function store(Study $study, BookmarkList $bookmarkList)
{
return $this->serve(CreateBookmarkFeature::class);
}
And I am using them in the Feature accordingly
'bookmark_list_id' => $request->bookmarkList->id,
class CreateBookmarkFeature extends Feature
{
public function handle(CreateBookmarkRequest $request)
{
//Call the appropriate job
$bookmark = $this->run(CreateBookmarkJob::class, [
'bookmark_list_id' => $request->bookmarkList->id,
'item_id' => $request->input('item_id'),
'type' => $request->input('type'),
'latest_update' => $request->input('latest_update'),
'notes' => $request->input('notes')
]);
//Return
return $this->run(RespondWithJsonJob::class, [
'data' => [
'bookmark' => $bookmark
]
]);
}
}
I am also using a custom request (CreateBookmarkRequest) which practically verifies if the user is authorised and imposes some rules on the input.
class CreateBookmarkRequest extends JsonRequest
{
/**
* Determine if the user is authorized to make this request.
*
* #return bool
*/
public function authorize()
{
return $this->getAuthorizedUser()->canAccessStudy($this->study->id);
}
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
"item_id" => ["integer", "required"],
"type" => [Rule::in(BookmarkType::getValues()), "required"],
"latest_update" => ['date_format:Y-m-d H:i:s', 'nullable'],
"text" => ["string", "nullable"]
];
}
}
Now, here comes the problem. I want to write a test for the feature that tests that the correct response is being returned (it would be good to verify the CreateBookmarkJob is called but not that important). The problem is that although I can mock the request, along with the input() method, I cannot mock the injected bookmarkList.
The rest of the functions are mocked properly and work as expected.
My test:
class CreateBookmarkFeatureTest extends TestCase
{
use WithoutMiddleware;
use DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
// seed the database
$this->seed();
}
public function test_createbookmarkfeature()
{
//GIVEN
$mockRequest = $this->mock(CreateBookmarkRequest::class);
$mockRequest->shouldReceive('authorize')->once()->andReturnTrue();
$mockRequest->shouldReceive('rules')->once()->andReturnTrue();
$mockRequest->shouldReceive('input')->once()->with('item_id')->andReturn(1);
$mockRequest->shouldReceive('input')->once()->with('type')->andReturn("ADVOCATE");
$mockRequest->shouldReceive('input')->once()->with('latest_update')->andReturn(Carbon::now());
$mockRequest->shouldReceive('input')->once()->with('notes')->andReturn("acs");
$mockRequest->shouldReceive('bookmark_list->id')->once()->andReturn(1);
//WHEN
$response = $this->postJson('/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark', [
"type"=> "ADVOCATE",
"item_id"=> "12",
"text"=> "My first bookmark"
]);
//THEN
$this->assertEquals("foo", $response['data'], "das");
}
One potential solution that I though would be to not mock the request, but this way I cannot find a way to mock the "returnAuthorisedUser" in the request.
Any ideas on how to mock the injected model would be appreciated, or otherwise any idea on how to properly test the feature in case I am approaching it wrong.
It is worth mentioning that I have separate unit tests for each of the jobs (CreateBookmarkJob and RespondWithJSONJob).
Thanks in advance
A feature test, by definition, will be imitating an end-user action. There's no need to mock the request class, you just make the request as a user would.
Assuming a Study with ID 1 and a BookmarkList with ID 1 have been created by your seeder, Laravel will inject appropriate dependencies via route model binding. If not, you should use a factory method to create models and then substitute the appropriate ID in the URL.
<?php
namespace Tests\Feature;
use Tests\TestCase;
class CreateBookmarkFeatureTest extends TestCase
{
use WithoutMiddleware;
use DatabaseMigrations;
public function setUp(): void
{
parent::setUp();
$this->seed();
}
public function TestCreateBookmarkFeature()
{
$url = '/api/recruitment_toolkit/study/1/bookmark_list/1/bookmark';
$data = [
"type"=> "ADVOCATE",
"item_id"=> "12",
"text"=> "My first bookmark"
];
$this->postJson($url, $data)
->assertStatus(200)
->assertJsonPath("some.path", "some expected value");
}
}
I agree with #miken32's response - that a feature should indeed imitate a user interaction - however the dependency injection via route model binding still did not work.
After spending some hours on it, I realised that the reason for it is that
use WithoutMiddleware;
disables all middleware, even the one responsible for route model binding, therefore the object models were not injected in the request.
The actual solution for this is that (for laravel >=7) we can define the middleware we want to disable, in this case:
$this->withoutMiddleware(\App\Http\Middleware\Authenticate::class);
Then we just use
$user = User::where('id',1)->first(); $this->actingAs($user);
And everything else works as expected.
DISCLAIMER: I am not implying that miken32's response was incorrect; it was definitely in the right direction - just adding this as a small detail.
I have an Observer set up to Listen to a Model's events in order to keep my Controller clean of Logging messages. My implementation is as follows:
First, a store method that does just what it's supposed to do. Create and save a new model from valid parameters.
# app/Http/Controllers/ExampleController.php
namespace App\Http\Controllers;
use App\Http\Requests\StoreExample;
use App\Example;
class ExampleController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
/**
* Create and save an Example from validated form parameters.
* #param App\Http\Requests\StoreExample $request
*/
public function store(StoreExample $request)
{
Example::create($request->validated());
return back();
}
}
The StoreExample Form Request isn't important. It just validates and checks a gate to authorize the action.
The Observer I have set up logs this action.
# app/Observers/ExampleObserver.php
namespace App\Observers;
use App\Example;
class ExampleObserver
{
public function created(Example $example): void
{
\Log::info(auth()->id()." (".auth()->user()->full_name.") has created Example with params:\n{$example}");
}
}
The problem I have, is the way my logs depend on the auth() object to be set. Given the auth middleware and the gate it has to check in order to store an Example, there is no way a guest user will set off this code.
However, I do like to use tinker in my local and staging environments to check the behavior of the site but that can set off an error (Well, PHP notice to be more precise) because I can create Example models without being authenticated and the logger will try to fetch the property full_name from the non-object auth()->user().
So my question is as follows: Is there a way to catch when I'm specifically using the Laravel tinker session to handle my models in the Observer class?
Okay, replying to my own question: There IS a way. It requires using a Request object. Since observers do not deal with requests on their own, I injected one in the constructor. request() can be used instead, so no DI is needed.
Why is a Request important?
Because a request object has an accessible $server attribute that has the information I want. This is the relevant information I get by returning a dd($request->server) (I'm not gonna paste the whole thing. My Request's ServerBag has over 100 attributes!)
Symfony\Component\HttpFoundation\ServerBag {#37
#parameters: array:123 [
"SERVER_NAME" => "localhost"
"SERVER_PORT" => 8000
"HTTP_HOST" => "localhost:8000"
"HTTP_USER_AGENT" => "Symfony" // Relevant
"REMOTE_ADDR" => "127.0.0.1"
"SCRIPT_NAME" => "artisan" // Relevant
"SCRIPT_FILENAME" => "artisan" // Relevant
"PHP_SELF" => "artisan" // Relevant
"PATH_TRANSLATED" => "artisan" // Relevant
"argv" => array:2 [ // Relevant
0 => "artisan"
1 => "tinker"
]
"argc" => 2
]
}
So there's all these attributes I can filter by using $request->server('attribute') (returns $request->server->attribute or null, so no risk of accessing an undefined property). I can also do $request->server->has('attribute') (returns true or false)
# app/Observers/ExampleObserver.php
namespace App\Observers;
use App\Example;
class ExampleObserver
{
/* Since we can use request(), there's no need to inject a Request into the constructor
protected $request;
public function __construct(Request $request)
{
$this->request = $request;
}
*/
public function created(Example $example): void
{
\Log::info($this->getUserInfo()." has created Example with params:\n{$example}");
}
private function getUserInfo(): string
{
// My logic here.
}
}
I'm calling cloud APIs using token authentication with php-openstack-sdk.
$openstack = new OpenStack\OpenStack([
'authUrl' => '{authUrl}',
'region' => '{region}',
'user' => [
'id' => '{userId}',
'password' => '{password}'
],
'scope' => ['project' => ['id' => '{projectId}']]
]);
However, every API call requires me to be authenticated (as shown in the code above). Instead of repeating the same auth code in every controller function, how do I do it once and be able to call $openstack in my controller's functions? i.e., in my controller, I can directly use $openstack.
public function listServers()
{
$openstack->computeV2()->listServers();
}
Write the logic in the __construct() of your Controller.php if you want that to be accessible for all the controllers. If not, write the __construct() within the controller you need.
Controller.php
class Controller extends BaseController
{
protected $openstack;
public function __construct()
{
$this->openstack = new OpenStack\OpenStack([
...
]);
}
}
NetworkController.php
class NetworkController extends Controller
{
public function getNetworkDetails() {
$network = $this->openstack->networking();
}
}
You can place the code shown in the __construct function of your controller and provide it as a protected variable to the class.
I think the best way is to use laravel middlewares
In laravel 5.5 I create the policy
public function view()
{
return true;
}
and register it in the AuthServiceProvider
protected $policies = [
// 'App\Model' => 'App\Policies\ModelPolicy',
Post::class => PostPolicy::class,
];
In the controller I use the policy like this:
$this->authorize('view');
I get the error This action is unauthorized whether the function view() returns true or false.
Your policy is registered for the Post model.
I assume your view() method is inside the PostPolicy class. It appears as if you'd want to use it without a model instance.
Use $this->authorize('view', Post:class); if the policy code does not require a model instance.
Your view method should furthermore receive a user model.
public function view(User $user) { ... };
Otherwise, for whom would you want to check permissions.
I am using form request validation and there are some rules that needs external values as a parameters.
Here are my validation rules for editing a business profile inside a form request class,
public function rules()
{
return [
'name' => 'required|unique:businesses,name,'.$business->id,
'url' => 'required|url|unique:businesses'
];
}
I can use this on the controller by type hinting it.
public function postBusinessEdit(BusinessEditRequest $request, Business $business)
{
//
}
But how to pass the $business object as a parameter to the rules method?
Lets say this is your model binding:
$router->model('business', 'App\Business');
Then you can reference the Business class from within the FormRequest object like this:
public function rules()
{
$business = $this->route()->getParameter('business');
// rest of the code
}
Note that if you use your form request both for create and update validation, while creating the record, the business variable will be null because your object does not exists yet. So take care to make the needed checks before referencing the object properties or methods.
There can be many ways to achieve this. I do it as below.
You can have a hidden field 'id' in your business form like bellow,
{!! Form::hidden('id', $business->id) !!}
and you can retrieve this id in FormRequest as below,
public function rules()
{
$businessId = $this->input('id');
return [
'name' => 'required|unique:businesses,name,'.$businessId,
'url' => 'required|url|unique:businesses'
];
}
For those who switched to laravel 5 :
public function rules()
{
$business = $this->route('business');
// rest of the code
}
Let say if we have a scenario like we want to change our validation rules depends on the type that we pass in with the route. For example:
app.dev/business/{type}
For different type of business, we have different validation rules. All we need to do is type-hint the request on your controller method.
public function store(StoreBusiness $request)
{
// The incoming request is valid...
}
For the custom form request
class StoreBussiness extends FormRequest
{
public function rules()
{
$type = $this->route()->parameter('type');
$rules = [];
if ($type === 'a') {
}
return rules;
}
}
In Laravel 5.5 at least (haven't checked older versions), once you did your explicit binding (https://laravel.com/docs/5.5/routing#route-model-binding), you can get your model directly through $this:
class StoreBussiness extends FormRequest
{
public function rules()
{
$rules = [];
if ($this->type === 'a') {
}
return rules;
}
}
Since Laravel 5.6 you may type hint it in the rules method:
public function rules(Business $business)
{
return [
'name' => 'required|unique:businesses,name,'.$business->id,
'url' => 'required|url|unique:businesses'
];
}
See the docs for more:
You may type-hint any dependencies you need within the rules method's signature. They will automatically be resolved via the Laravel service container.