Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

$guarded value on Role/Permission class causes failure in latest laravel 7.x and 6.x #1542

Closed
ReeceM opened this issue Aug 10, 2020 · 36 comments · Fixed by #1548
Closed

$guarded value on Role/Permission class causes failure in latest laravel 7.x and 6.x #1542

ReeceM opened this issue Aug 10, 2020 · 36 comments · Fixed by #1548

Comments

@ReeceM
Copy link
Contributor

ReeceM commented Aug 10, 2020

With regards the latest laravel update that is documented here: Security Release: Laravel 6.18.35, 7.24.0

And also the code change happened here in the 6.x branch laravel/framework#33777

These two updates have broken the functionality when you try to perform a with() query on the Roles model.

For example I have the code:

Role::orderBy('type')
    ->withCount('users')
    ->paginate();

This gives the error:

PHP Notice:  Undefined index: guard_name in app/vendor/spatie/laravel-permission/src/Models/Role.php on line 62
TypeError: Argument 1 passed to getModelForGuard() must be of the type string, null given, called in app/vendor/spatie/laravel-permission/src/Models/Role.php on line 62

It seems the solution is to just set the $guarded property to [] in the model classes.

If someone is extending the class this is easy to fix, but unfortunately it isn't very clear immediately what error caused this.

If it is alright with the maintainers I don't mind making the change to the model files to the code that will make it functional.

System Info:

Laravel: 6.18.35
PHP: 7.3.8
Permissions: 3.13

@drbyte
Copy link
Collaborator

drbyte commented Aug 11, 2020

It seems the solution is to just set the $guarded property to [] in the model classes.

By removing id from the list of $guarded fields, are you suggesting that it's wise to allow mass-assignment of the id field?
I wonder: would it be more sensible to at least set $fillable = ['name', 'guard_name']; instead? I'm concerned that that's a breaking change for apps though.

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 11, 2020

@drbyte sorry, my mistake,$fillable would be a much better choice, I was just writing what was the immediate suggestion from the blog articles, but Taylor did say $fillable is better.

I actually did us fillable on my class that extends the role and permission class and it works as expected.

@drbyte
Copy link
Collaborator

drbyte commented Aug 11, 2020

While I much prefer using $guarded to explicitly indicate the fields I wish to protect, I guess moving to $fillable isn't that bad.
I worry that it causes a breaking change though. :( I've tested it on various apps I have in production and it's fine, but I worry that some more complex apps that have done extensive inheritance may have troubles if we replace $guarded with $fillable. Thoughts?

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 11, 2020

@drbyte I am just going to test what those changes would do too, as I have extended the Role and Permission classes to add extra stuff and columns. That would give me a better position to give my thoughts if that is alright :)

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 11, 2020

[edit: sorry, wrong key combo on enter 🤦]

@drbyte from what I can tell if $fillable is set in the base models from the package, and then overridden by a class that extends there doesn't seem to be any conflict.

I think though that if anyone has extended Role or Permission class extensively they would only be affected if they have added extra columns to the table.

From what I could tell just extending the class and not adding $fillable and did get an error obviously that there wasn't a default value.

It would maybe be a good idea then to note in the documentation that if a class is extended or extra columns added to the tables they should be accounted for in their model that is extending the packages class?

I just did a search, although it probably isn't really representative of what the use of the package is but there are quite a few repos that extend the spate permission models, but most of them don't define any new columns or methods. One did add new fillable columns to the model this one (it also seems a recent project)

Most though seem to be based on this template repo which extends the base Role/Permission classes and doesn't add anything: https://github.com/rappasoft/laravel-boilerplate/blob/master/app/Domains/Auth/Models/Role.php

I think most people should be able to figure out what to add if there is something amiss?

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 11, 2020

I also think though that for each time that a user does add columns the tables in the migration, and they haven't extended the classes, they may face issues.

Unless there is a configurable value in the permissions config file, there is already a key column_names. Columns could be added there and then they are loaded via the $this->fillable(array $fillable) method in the model. It would override anything sit in the top level, but that could possibly be accounted for before doing that.

Not sure on the performance effect it would have on instantiating the class each time, although the config helper is used in the __construct() method.

Could be an alternative approach to not 'break' anything.

@drbyte
Copy link
Collaborator

drbyte commented Aug 11, 2020

Hmmm....

The last thing I want to do is break anything for everyone!

I also don't want to introduce a lot of complexity.

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 11, 2020

Yeah, breaking things are not really going to be any good, especially as it works as long as a class is extended. Could maybe let those who read the 'readme' know to make that change for now if they have the problem?

Maybe this specific things isn't such a big problem at the current moment? I don't seem to see any other people having this issue so maybe from they are setting a $fillable value whenever they are using this?

If the change is made to use fillable instead of guarded, could it be set as a pre-release and see if other users are able to test it?

But also transitioning towards the $fillable way of setting the column names it would be more inline with the framework suggestions and possibly what people are expecting?

@josephsong
Copy link

I don't seem to see any other people having this issue

We're having it, too. We're not extending Permission ourselves, so this breaks it for us. Thanks for looking into it!

@jimbojsb
Copy link

I think this is the same issue we're seeing with the following code that works in Laravel 7.23.3 but not in 7.24.0 and later:

We have this code in set setup method of a test class
Permission::create(['name' => 'manage-test', 'guard_name' => 'luna']);

And from what I can tell, inside the actual query builder actions encapsulated by a create, somewhere the values being passed in the array here are dropped and result in a sql error:

Caused by
PDOException: SQLSTATE[HY000]: General error: 1364 Field 'name' doesn't have a default value

/app/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:127
/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:464
/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:664
/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:631
/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:465
/app/vendor/laravel/framework/src/Illuminate/Database/Connection.php:417
/app/vendor/laravel/framework/src/Illuminate/Database/Query/Processors/Processor.php:32
/app/vendor/laravel/framework/src/Illuminate/Database/Query/Builder.php:2829
/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:1403
/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:900
/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:865
/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:728
/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:767
/app/vendor/laravel/framework/src/Illuminate/Support/helpers.php:433
/app/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Builder.php:768
/app/vendor/spatie/laravel-permission/src/Models/Permission.php:43
/app/tests/Unit/Policies/PublishablePolicyTest.php:22

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

I think this is the same issue we're seeing with the following code that works in Laravel 7.23.3 but not in 7.24.0 and later:

@jimbojsb from 7.24.0 I think would be correct as that is the version from when the change in the framework came in.

The behavior you are seeing is the same I got. So it is the same bug

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

@drbyte do you think it would be possible to make the needed changes that makes use of the $fillable attribute, then use a pre-release tag that can be tested?

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

Latest thoughts:

  • I'm still not fully comfortable moving to using $fillable instead of just specifying that id should be guarded. But I'm also open to being convinced that it is. It just seems to me that if that's the only "answer" then there's actually no point in ever using guarded. So clearly I'm missing something.
  • To investigate: what's the purpose of the newly-added undocumented $guardableColumns property? (eg: does it have any usefulness here?)
  • Is the issue actually related to fillable/guarded or is it something amuck with our users relation which requires passing (and verifying) a guard_name value?

@BastianErler
Copy link

Taylor Otwell described the reason for this change in this Blogpost

https://blog.laravel.com/security-release-laravel-61835-7240

and yes in fact he recommends "As a personal recommendation, I recommend always using $fillable instead of $guarded."

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

@BastianErler wrote:

Taylor Otwell described the reason for this change in this Blogpost

https://blog.laravel.com/security-release-laravel-61835-7240

and yes in fact he recommends "As a personal recommendation, I recommend always using $fillable instead of $guarded."

Yes, and I agree at the "app" level.
But packages are complicated: we can set default $fillable properties, but then if you extend the model and set your own but forget to include the defaults in your own then it wipes them out and then you come asking for support about why it's not working... It's hard to find the right balance between making it easy for you to use and also "protecting you from yourself!"

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

@ReeceM ... Maybe this would work on the models? (Unfortunately my initial testing is leaving name blank for some reason, but maybe it's something "else" in my extended models. I'm tied up with another project so might not get to further investigating of it today.)

It sets $fillable to this package's expected fieldnames, but then if the model is extended and $fillable is set in the extended model (which replaces this default) but forgets to keep the name/guard_name entries, then this override of getFillable will reinstate them.

/src/Models/Role.php and Permission.php

    protected $fillable = ['name', 'guard_name'];

    public function getFillable()
    {
        $this->mergeFillable(['name', 'guard_name']);

        return $this->fillable;
    }

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

Maybe this would work on the models? (Unfortunately my initial testing is leaving name blank for some reason, but maybe it's something "else" in my extended models. I'm tied up with another project so might not get to further investigating of it today.)

It sets $fillable to this package's expected fieldnames, but then if the model is extended and $fillable is set in the extended model (which replaces this default) but forgets to keep the name/guard_name entries, then this override of getFillable will reinstate them.

/src/Models/Role.php and Permission.php

    protected $fillable = ['name', 'guard_name'];

    public function getFillable()
    {
        $this->mergeFillable(['name', 'guard_name']);

        return $this->fillable;
    }

@drbyte, that is along the lines of what I was thinking could be done to account for the extended class.

I have been testing again on my side and have found some other odd things related to the earlier comment you posted today, just wanting to finish checking before saying anything, there seems to be a bug when extending a class with guarded and fillable, but not on the base class??

But that idea looks like it would be easy to maintain in the future and basically self-explanatory

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

Yes, and this hopefully avoids adding more stuff to the config file.

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

LOL, I think I found the problem that would make it possible not to worry about adding a new function.

But will just check, as it is interesting bug that is maybe a implementation detail on the framework function too 🤷‍♂️

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

re: #1542 (comment) by @drbyte

From what I understand in looking at the code and running it when I first found the bug I found this:

  • specifying 'id' as the guarded value just makes it impossible to fill any new roles or permissions when creating them if you have custom columns added to the role/permission table. So it does seem that guarded is more of a pain at this point than any help.

It does appear that having $guarded set with just 'id' it still functions as expected with a single class?

Maybe it is the addition of the 'guard_name' in the construct method that is messing with things here?

  • $guardableColumns seems to be populated with values of the actual schema from the models table, and then that is used for the guarded values. So leaving the $fillable or $guarded blank in that instance would just let nothing be filled in at all. So you would have to '*' wildcard the guarded array, but that's a free for all. And it is static across all Model instances, not sure of future performance issues there when you got a few hundred models going... Either way, if the guarded or fillable values are not set, there is an extra DB query done on the instantiation of the class, but it is only once. But don't think it is meant to be publicly fiddled with.

  • I think maybe the users relation could do with a check for if the value is actually set in the attributes array? It seems to be assuming that it is set but maybe not related.

What I have found in testing is that the one problem is that because the Role and Permission class don't use the normal Role === 'roles' table guessing of eloquent, the schema check that looks for the table columns actually doesn't work as the table is only set after the constructor method of the parent Model class.

So what happens is you get this:

Internally of the model class you get this in the guarded columns thing
It is querying the schema before the table name from the config files are set
Because of this it basically gets nothing as the column names and kicks all data
And the problem is even worse, if the table is named something else, or if the class that extends is called something else.

$guardableColumns = array:2 [
  "Spatie\Permission\Models\Role" => []
];

What I noticed is that the 'simple' fix is to define a getTable method on the Role or Permission class.

This way it makes use of the Laravel feature to get the table columns, but still guard the id. This means that if the user does extend the class nothing actually changes and they wouldn't notice even if they had an old version of laravel.

This would make sense as then whenever eloquent was looking for the table it is a central method and is also matched?

As it stands even in my App level logic, I have to get the table name from the config file to adjust for the Rule validation because the name isn't just roles/permissions.

So the change can be two ways:

/**
 * Get the table associated with the model.
 *
 * @return string
 */
public function getTable()
{
    return config('permission.table_names.roles');
    // or
    return config('permission.table_names.permissions');
}

^ Setting it this way would mean that even if the class is extended it would work.

Or we set the table before calling the parent::__construct(). BUT that would mean that when we invoke the static create like menthods, it is still possible to work, but they call the set tables further on in the function public function newInstance($attributes = [], $exists = false).

public function __construct(array $attributes = [])
{
    $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard');
    $this->setTable(config('permission.table_names.roles'));
    
    parent::__construct($attributes);
}

But getTable is called and it would make sense then that it is overridden in the packages model, then the user can extend as much as they need?

I tested, and you can keep the protected $guarded = ['id'] on the Models.

Please let me know if this came across clear :)

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

Yes, I had a similar hunch when I was digging as well: ie: is there a chicken/egg situation where the lookup of table columns for guarded verification is firing too late?

Declaring getTable() in the base models makes sense, especially with this new framework change. And, as you say, could remove the need for setTable(), but still allow extended models to set a $table property if needed (granted, the config is probably a better place for people to be setting it).

Haven't had a chance to test it myself though. Cases I want to test: base setup without extension; extended models; extended with added custom fields; declaring those custom fields as fillable (does it break anything); custom tablenames; changing config settings at runtime (eg: multitenancy dynamic update/set of config values such as db-connection, tablenames, etc).

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

Of the things you want to test, I currently have the following going on my app that is using the package

  • extended the base class,
  • added column names,
  • $fillable on the extended model,
  • dynamic connections for the DB (database switch in Service Providers).
  • I have changed the table names in the config file, but not the extended model as it was more practical to adjust it in the config.

But as you said, a batch of tests would be useful to go through with different implementations, for my case it is probably a rather 'crazy' end user modifications possibility covered 🤣

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

for my case it is probably a rather 'crazy' end user modifications possibility covered 🤣

Those are the best ones to know about! It's the wild and crazy uses that are hard to anticipate!

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

@ReeceM So, to be sure that I'm reading your posts completely, are you saying that in your app merely declaring getTable in the base model is enough to meet the needs for accommodating this security patch?

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

So, to be sure that I'm reading your posts completely, are you saying that in your app merely declaring getTable in the base model is enough to meet the needs for accommodating this security patch?

@drbyte yip, I added that snippet that’s a few comments up into the base Role table model and it sorted it. Didn’t set id either if $guarded was left in the base model

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

Thanks. That helps me determine where to focus some of my tests when I can focus on it later today.
I appreciate your interactive exploration here!

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

Pleasure to do so 👍😁

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

Oh ... and remove the setTable call from the model constructor? or is it still needed after calling parent::__construct() ?

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

In my test I removed it in the __constructor

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

Thinking aloud: Given the default getTable has some fallbacks, should we retain those?
IF the user hasn't published the config file we fallback to the package's config. But if they've set it to blank, perhaps we should fallback to the default behavior?

https://github.com/illuminate/database/blob/c3f2b9b5ec998714bc5089612e509e5402d8d0fc/Eloquent/Model.php#L1375-L1378

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

(You can tell I'd rather be working on this instead of my project deadline! LOL)

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

Thinking aloud: Given the default getTable has some fallbacks, should we retain those?
IF the user hasn't published the config file we fallback to the package's config. But if they've set it to blank, perhaps we should fallback to the default behavior?

https://github.com/illuminate/database/blob/c3f2b9b5ec998714bc5089612e509e5402d8d0fc/Eloquent/Model.php#L1375-L1378

@drbyte I think keeping the fallback would be good, I was think originally when changing it locally to use parent::getTable() incase something in the framework changes, but I doubt that line of code has changed since early days :D

@drbyte
Copy link
Collaborator

drbyte commented Aug 14, 2020

Agreed. I'm thinking:

return config('permission.table_names.roles', parent::getTable());

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 14, 2020

That looks good, that is very clear too which is cool :)

@ReeceM
Copy link
Contributor Author

ReeceM commented Aug 15, 2020

👏🥳

Awesome @drbyte, thanks for keeping the package working :)

@drbyte
Copy link
Collaborator

drbyte commented Aug 15, 2020

Thanks @ReeceM for your research and testing. Greatly appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants