So I have created a policy and registered it in the AuthServicePRovider, but it always returns false. It is my first time working with policies so I am sure I am doing it wrong, but following a few examples, nothing has worked for me.
I am logged in with user that has an id of 1. I try to edit a label that has a user_id of 1, returns false, and also when trying to edit a label that has a user_id of 2. This last one works as expected, but f the user_id and label->user_id match, I should ave a form displayed. Instead, I get this each time:
This action is unauthorized.
Any ideas?
AuthServiceProvider: (Tried both but both don't work):
protected $policies = [
'App\Label' => 'App\Policies\LabelPolicy'
];
And this one also did not do the trick:
protected $policies = [
Label::class => LabelPolicy::class
];
LabelsController#edit:
public function edit(Label $label)
{
// $this->authorize('edit', $label); // This also returns false
if (auth()->user()->cannot('edit', $label)) {
dd('NO'); // This is always shown
}
}
LabelPolicy:
public function edit(Label $label)
{
dd('test'); // This is never shown anywhere
return auth()->user()->id === $label->user_id;
}
The policies expects actually two inputs, the first input is always the User class, the second input is the Model and defaults to the Model class. So in your case:
LabelPolicy
public function edit(User $user, Label $label)
{
return $user->id === $label->user_id;
}
LabelsController#edit:
public function edit(Label $label)
{
$this->authorize($label);
}
if your $this->authorize inside Controllers always returns false, then double check if your model and model policy namespace was imported in AuthServiceProvider and also your model has been imported into your controller.
Related
In my application I want to keep track of who has performed certain operations on different models in my application.
Default Laravel model with timestamps automatically updates fields like created_at and updated_at. I can modify this behavior to set the created_by field automatically by calling the static::updating() function as mentioned in this answer: https://stackoverflow.com/a/64241347/4112883 . This works very well. Additionally, I came across this package (https://github.com/WildsideUK/Laravel-Userstamps), but that is limited to only created, updated, and deleted.
For my Post model, I have more timestamps: created_at, updated_at, completed_at, checked_at, and published_at. When a user ends the post, it must be verified by that user's manager. If all is well, some logic will publish the message, but if not, the manager can create one or more actions for the user to complete the message, which will undo the finishing attributes. An action is created with the following timestamps: created, updated, and completed (null). When the user completes an action, the actions.finished_at and actions.finished_by fields are set.
Now comes the challenge. For each custom timestamp, I want to set the relationship and three functions to handle certain states of the timestamp: set, undo and check for isset:
class Post extends Model
{
//…
public function finishedBy() //relationship belongsTo User::class
{
return $this->belongsTo(User::class, 'finished_by');
}
public function finish() { //function to finish post (SET)
$this->update([
'finished_by' => auth()->id(),
'finished_at' => now(),
]);
}
public function undoFinish() { //function to undo finishing (UNSET)
$this->update([
'finished_at' => null,
'finished_by' => null,
]);
}
public function isFinished() { //function to check if is finished (ISSET)
return !empty($this->finished_by) && !empty($this->finished_at);
}
//…
All four functions must be repeated for ‘checked’ and ‘published’ in the Post model, and for the ‘finished’ attribute in Action model, leading to a lot of almost-duplicate code. (Maybe in the future I want to repeat this logic in other models.)
Is there a possibility to make this more elegant with a Trait or something?
E.g. create something like an protected array $timestamps_with_user by which the application automatically adds the relationship and the three functions?
protected $timestamps_with_users = [
'finish', 'check', 'publish'
];
// foreach in a trait?? Need your help here :D
foreach($timestamps_with_users as $perform) {
public function $perform() { … } //$post->finish()
public function $perform.edBy() :User { … } //$post->finishedBy()
public function undo.$perform() { … } //$post->undoFinish()
public function is.$perform.ed() { … } //$post->isFinished()
}
Thanks in advance and looking forward to your answers.
Just create a new trait and create functions that works with any timestamp:
<?php
namespace App\Traits;
trait CustomTimestamps {
public function perform(string $action)
{
$this->update([
$action . 'ed_by' => auth()->id(),
$action . 'ed_at' => now(),
]);
}
public function undo(string $action)
{
$this->update([
$action . 'ed_by' => null,
$action . 'ed_at' => null,
]);
}
public function check(string $action)
{
$at = $action . 'ed_at';
$by = $action . 'ed_by';
return !empty($this->{$by}) && !empty($this->{$at});
}
}
I am wanting to create a redirect feature as it stands right now my current function in my auth controller is if a film doesn't exist then it redirects to the page to create one. But I am wanting to change this now as this route is no longer needed. What I need instead is if a USER doesn't have an age or gender configured/set it redirects them to the user page /user with the new route name as user_update
Current function:
private function redirect(): RedirectResponse
{
if (!$this->user->film()->exists()) {
return redirect()->route('film_create');
}
return redirect()->route('home');
}
user model: the fields are set as constants with an array of values at the top of the model, e.g types of gender, age range
protected $fillable = [
'id',
'age',
'gender',
];
do you know how i can modify the function above to create an IF statement to check if these fields are set for the user or not if not redirect them to the /user page?
having trouble trying to find resources on how to do this.
I am now writing a test for this function but having a little trouble on doing it..
this is the current test
/**
* #test
* Create mocked user, send them to user page and assert they exists in database
*/
public function it_creates_user_on_redirect()
{
$email = 'foo#bar.com';
$this->mockSocialite($email);
$this->get(route('auth_callback'))
->assertLocation(route('film_create'));
$this->assertDatabaseHas('users', [
'email' => $email,
]);
}
How do i go about modifying this test to ensure that it covers the new function ? #Saly 3301
Should be straightforward
private function redirect(): RedirectResponse
{
if ($this->user->age || $this->user->gender) {
return redirect()->route('home');
}
return redirect()->route('user_update');
}
And here's the boolean logic in tinker
>>> NULL || NULL
=> false
>>> 25 || NULL
=> true
>>> NULL || 'female'
=> true
>>> 25 || 'female'
=> true
Hope this helps
Should be:
private function redirect(): RedirectResponse{
if (!($this->user->age && $this->user->gender)) {
return redirect()->route('user_update');
}
return redirect()->route('home');
}
Only if both fields are configured, you are redirected to 'home'
Update Method:
public function update(UserUpdateRequest $request, Users $uzytkownik)
{
$this->authorize('update', $uzytkownik);
if ( $uzytkownik->update([
'birth' => $request->birth,
'sex' => $request->sex,
'about' => $request->about,
]) )
{
return 1;
}
return 0;
}
On update here in page 1 appears. Like it did the thing.
But in db nothing has changed.
$uzytkownik is proper user, and
This is the dd($uzytkownik);
And below dd($request->birth.'---'.$request->sex.'---'.$request->about); which shows proper inputs
Why it doesn't work properly?
As per the documentation
Mass Assignment
You may also use the create method to save a new model in a single line. The inserted model instance will be returned to you from the method. However, before doing so, you will need to specify either a fillable or guarded attribute on the model, as all Eloquent models protect against mass-assignment by default.
You need to make sure $fillable or $guarded is correctly set otherwise changes may not be persistant.
You can do what you want like this too:
public function update(UserUpdateRequest $request, Users $uzytkownik)
{
$this->authorize('update', $uzytkownik);
$uzytkownik->birth = $request->birth;
$uzytkownik->sex = $request->sex;
$uzytkownik->about = $request->about;
if ( $uzytkownik->save() )
{
return 1;
}
return 0;
}
When a user visits a particular url in my yii 2.0 application without required parameters, I want to present a form to collect the required missing parameters.
for this purpose, I need the names of missing parameters, e.g. I have a function
public function actionBlast ($bomb, $building) {
}
I expect the results as an array like this
$args = [0=>'bomb', 1=>'building'];
I tried func_get_args() but it returns null, and the undocumented ReflectionFunctionAbstract::getParameters ( void ) etc. Any other way out?
I think the best way to achieve what you want is to override the default ErrorAction.
Inside your controllers directory, create:
controllers
actions
ErrorAction.php
In ErrorAction.php, add:
<?php
namespace frontend\controllers\actions;
use Yii;
use yii\web\ErrorAction as DefaultErrorAction;
class ErrorAction extends DefaultErrorAction
{
public function run()
{
$missing_msg = 'Missing required parameters:';
$exception = Yii::$app->getErrorHandler()->exception;
if (substr($exception->getMessage(), 0, strlen($missing_msg)) === $missing_msg) {
$parameters = explode(',', substr($exception->getMessage(), strlen($missing_msg)));
return $this->controller->render('missing_params_form' ?: $this->id, [
'parameters' => $parameters,
]);
}
return parent::run();
}
}
In your controller add:
public function actions()
{
return [
'error' => [
'class' => 'frontend\controllers\actions\ErrorAction',
],
];
}
and create a view "missing_params_form.php" in your controller `s view directory, where you can generate your form fields.
I believe this to be your best option, though you may need to update it in case a Yii update changes the error message.
I'm using a REST API to receive the data.
The data model is polymorphic related, similar to the one on the documentation:
https://laravel.com/docs/5.4/eloquent-relationships#polymorphic-relations
posts
id - integer
title - string
body - text
videos
id - integer
title - string
url - string
comments
id - integer
body - text
commentable_id - integer
commentable_type - string
Let's say, for example, the API is receiving this new comment:
{
"body": "This a test comment",
"commentable_type": "posts",
"commentable_id": "1"
}
How can I validate if the received commentable_type exists and is valid?
If I correctly understand your question, you are trying to validate that the object of the polymorphic relation exists, for the given commentable_type and commentable_id.
If that is the case, there is no existing validation rule to do so, but you can create one.
Based on the documentation, here is what you could do:
First, add the new rule in the boot method of a service provider (e.g. AppServiceProvider):
Validator::extend('poly_exists', function ($attribute, $value, $parameters, $validator) {
if (!$objectType = array_get($validator->getData(), $parameters[0], false)) {
return false;
}
return !empty(resolve($objectType)->find($value));
});
And this is how you would use it:
'commentable_id' => 'required|poly_exists:commentable_type
What the rule does is it tries and fetches the commentable type from the input values (based on the parameter passed on to the rule, i.e. commentable_type in our case), and then resolves the object and tries to find a record for the given ID ($value).
Please note that for this to work however, the value of commentable_type must be the fully qualified class name (e.g. App\Models\Post).
Hope this helps!
Better approach that includes morphs map:
Validator::extend('poly_exists', function ($attribute, $value, $parameters, $validator) {
if (! $type = array_get($validator->getData(), $parameters[0], false)) {
return false;
}
if (Relation::getMorphedModel($type)) {
$type = Relation::getMorphedModel($type);
}
if (! class_exists($type)) {
return false;
}
return ! empty(resolve($type)->find($value));
});
You can dynamically define a model_exists rule in your Request class. Something like this:
public function rules()
{
$polymorphExistsRule = '';
if ($this->has('commentable_type')) {
$polymorphExistsRule .= '|exists:' . $this->commentable_type . ',id';
}
return [
'commentable_type' => 'required_with:commentable_id',
'commentable_id' => 'required_with:commentable_type' . $polymorphExistsRule,
];
}
Edit
I might've misunderstood the first time. If you want to check that the model saved in commentable_type exists you could do something like this:
$type = $comment->commentable_type;
if(class_exists($type)) echo "it exists";
Depending on your needs you could do additional checking for it's inheritance (for example that it extends class Model). Or anything else that fits your needs really.
Edit2
This is what I would do if I were you. I would add property protected $allRelations to your Comment model and manually put all the relationships in. Then make some helper models to check if it's in the array.
Simple example:
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
// ..
protected $allRelations= [
'posts' => '\App\Post',
'videos' => '\App\Video',
];
public static function validateRelationNs($ns) {
return in_array($ns, $this->allRelations);
}
public static function validateRelationName($name) {
return array_key_exists($name, $this->allRelations);
}
// ...
}
Old answer:
Laravel expects full namespace name of the model for polymorphic type columns (in your case commentable_type should be \Full\Ns\Post, not posts).
The easiest way to ensure correctness is to always save it through the relationship. For example:
$post = Post::first();
$comment = new Comment($attributes);
$post->comments()->save($comment).
This will automatically set both commentable_id and commentable_type correctly (assuming your relationsare correctly defined).
Additional checking
Other then that you could check through model events. You could validate it before saving to the database.
My final version work for validate type and id:
Validator::extend('poly_exists', function ($attribute, $value, $parameters, $validator) {
if (!$objectType = array_get($validator->getData(), $parameters[0], false)) {
return false;
}
if (!class_exists($objectType)) {
return false;
}
return !empty(resolve($objectType)->find($value));
});