booni3
3 months ago
189
0
Laravel

Belongs to many with Spatie Permissions - solution feedback

Posted 3 months ago by booni3

I have a situation where a user can belong to many teams/companies and within that team/company they can have different roles and permissions depending on which one they are signed into. I have come up with the following solution and would love some feedback!

Note: Currently I am only using the model_has_roles table with Spatie permissions and always use $user->can('Permission') to check permissions.

  1. Our company model has the following relationships and method
class Company extends Model
{
    public function owner(): HasOne
    {
        return $this->hasOne(User::class, 'id', 'user_id');
    }

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(
            User::class, 'company_users', 'company_id', 'user_id'
        )->using(CompanyUser::class);
    }

    public function addTeamMember(User $user)
    {
        $this->users()->detach($user);

        $this->users()->attach($user);
    }
}
  1. We modify the pivot model to have the Spatie HasRoles trait. This allows us to assign a role to the CompanyUser as opposed to the Auth User. You also need to specify the default guard or Spatie permissions squarks.
class CompanyUser extends Pivot
{
    use HasRoles;

    protected $guard_name = 'web';
}
  1. On the user model, I have created the HasCompanies Trait. This provides the relationships and provides a method for assigning the roles to the new company user. Additionally, it overwrites the gate can() method.

A user can belong to many companies, but can only have one active company at a time (i.e. the one they are viewing). We define this with the current_company_id column.

It is also important to ensure the pivot table ID is pulled across (which it will not be as standard) as this is now what we are using in the Spatie model_has_roles table.

trait HasCompanies
{
    public function companies(): HasMany
    {
        return $this->hasMany(Company::class);
    }

    public function currentCompany(): HasOne
    {
        return $this->hasOne(Company::class, 'id', 'current_company_id');
    }

    public function teams(): BelongsToMany
    {
        return $this->belongsToMany(
            Company::class, 'company_users', 'user_id', 'company_id'
        )->using(CompanyUser::class)->withPivot('id');
    }

    public function switchCompanies(Company $company): void
    {
        $this->current_company_id = $company->id;
        $this->save();
    }

    public function assignRolesForCompany(Company $company, ...$roles)
    {
        if($company = $this->teams()->where('companies.id', $company->id)->first()){
            /** @var CompanyUser $companyUser */
            $companyUser = $company->pivot;
            $companyUser->assignRole($roles);
            return;
        }

        throw new Exception('Roles could not be assigned to company user');
    }

    public function can($ability, $arguments = [])
    {
        if(isset($this->current_company_id)){
            /** @var CompanyUser $companyUser */
            $companyUser = $this->teams()->where('companies.id', $this->current_company_id)->first()->pivot;

            if($companyUser->hasPermissionTo($ability)){
                return true;
            }

            // We still run through the gate on fail, as this will check for gate bypass. i.e. Super User
            return app(Gate::class)->forUser($this)->check('InvalidPermission');
        }

        return app(Gate::class)->forUser($this)->check($ability, $arguments);
    }
}

Now we can do something like this:

  1. Create the role & permission
/** @var Role $ownerRoll */
$ownerRoll = Role::create(['name' => 'Owner']);

/** @var Permission $permission */
$permission = Permission::create([
    'name' => 'Create Company',
    'guard_name' => 'web',
]);

$ownerRoll->givePermissionTo($permission);
  1. Create a new company with an owning user and then switch this company to that owner's active company.
public function store(CompanyStoreRequest $request)
{
    DB::transaction(function () use($request) {
        /** @var User $owner */
        $owner = User::findOrFail($request->user_id);

        /** @var Company $company */
        $company = $owner->companies()->create($request->validated());
        $company->addTeamMember($owner);

        $owner->assignRolesForCompany($company, 'Owner');
        $owner->switchCompanies($company);
    });

    return redirect()->back();
}

So this all works, my main concerns are that:

  1. We are overwriting the can method. There may be other authorization methods/gate functions that are not caught.

  2. We have 2 sets of model_permissions. The Auth user and the company user. I think I need to build in some checks to ensure that only the correct kinds of users can be assigned to the roles. At this stage, all administrator users would have permissions assigned to their auth user, while any users who own a company should only have permissions on the company user model

Please sign in or create an account to participate in this conversation.