Skip to content

Commit

Permalink
Merge pull request #1804 from erikn69/teams_feature
Browse files Browse the repository at this point in the history
Teams/Groups feature
  • Loading branch information
drbyte authored Aug 31, 2021
2 parents baeee79 + 8049909 commit c54a494
Show file tree
Hide file tree
Showing 23 changed files with 689 additions and 37 deletions.
17 changes: 17 additions & 0 deletions config/permission.php
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,25 @@
*/

'model_morph_key' => 'model_id',

/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/

'team_foreign_key' => 'team_id',
],

/*
* When set to true the package implements teams using the 'team_foreign_key'. If you want
* the migrations to register the 'team_foreign_key', you must set this to true
* before doing the migration. If you already did the migration then you must make a new
* migration to also add 'team_foreign_key' to 'roles', 'model_has_roles', and
* 'model_has_permissions'(view the latest version of package's migration file)
*/

'teams' => false,

/*
* When set to true, the required permission names are added to the exception
* message. This could be considered an information leak in some contexts, so
Expand Down
73 changes: 73 additions & 0 deletions database/migrations/add_teams_fields.php.stub
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddTeamsFields extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');

if (! $teams) {
return;
}
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if (empty($columnNames['team_foreign_key'] ?? null)) {
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}

Schema::table($tableNames['roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
if (! Schema::hasColumn($tableNames['roles'], $columnNames['team_foreign_key'])) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
});

Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');

$table->dropPrimary();
$table->primary([$columnNames['team_foreign_key'], 'permission_id', $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});

Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) {
$table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');

$table->dropPrimary();
$table->primary([$columnNames['team_foreign_key'], 'role_id', $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});

app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{

}
}
40 changes: 33 additions & 7 deletions database/migrations/create_permission_tables.php.stub
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ class CreatePermissionTables extends Migration
{
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$teams = config('permission.teams');

if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}

Schema::create($tableNames['permissions'], function (Blueprint $table) {
$table->bigIncrements('id');
Expand All @@ -30,16 +34,23 @@ class CreatePermissionTables extends Migration
$table->unique(['name', 'guard_name']);
});

Schema::create($tableNames['roles'], function (Blueprint $table) {
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
$table->bigIncrements('id');
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MySQL 8.0 use string('name', 125);
$table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125);
$table->timestamps();

$table->unique(['name', 'guard_name']);
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});

Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) {
$table->unsignedBigInteger(PermissionRegistrar::$pivotPermission);

$table->string('model_type');
Expand All @@ -50,12 +61,20 @@ class CreatePermissionTables extends Migration
->references('id')
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');

$table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
$table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}

});

Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) {
$table->unsignedBigInteger(PermissionRegistrar::$pivotRole);

$table->string('model_type');
Expand All @@ -66,9 +85,16 @@ class CreatePermissionTables extends Migration
->references('id')
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');

$table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
$table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});

Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) {
Expand Down
7 changes: 7 additions & 0 deletions docs/basic-usage/artisan.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@ When creating roles you can also create and link permissions at the same time:
php artisan permission:create-role writer web "create articles|edit articles"
```

When creating roles with teams enabled you can set the team id by adding the `--team-id` parameter:

```bash
php artisan permission:create-role --team-id=1 writer
php artisan permission:create-role writer api --team-id=1
```

## Displaying roles and permissions in the console

There is also a `show` command to show a table of roles and permissions per guard:
Expand Down
68 changes: 68 additions & 0 deletions docs/basic-usage/teams-permissions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
---
title: Teams permissions
weight: 3
---

NOTE: Those changes must be made before performing the migration. If you have already run the migration and want to upgrade your solution, you can run the artisan console command `php artisan permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/master/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables.

When enabled, teams permissions offers you a flexible control for a variety of scenarios. The idea behind teams permissions is inspired by the default permission implementation of [Laratrust](https://laratrust.santigarcor.me/).


Teams permissions can be enabled in the permission config file:

```php
// config/permission.php
'teams' => true,
```

Also, if you want to use a custom foreign key for teams you must change in the permission config file:
```php
// config/permission.php
'team_foreign_key' => 'custom_team_id',
```

## Working with Teams Permissions

After implements on login a solution for select a team on authentication (for example set `team_id` of the current selected team on **session**: `session(['team_id' => $team->team_id]);` ),
we can set global `team_id` from anywhere, but works better if you create a `Middleware`, example:

```php
namespace App\Http\Middleware;

class TeamsPermission{

public function handle($request, \Closure $next){
if(!empty(auth()->user())){
// session value set on login
app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(session('team_id'));
}
// other custom ways to get team_id
/*if(!empty(auth('api')->user())){
// `getTeamIdFromToken()` example of custom method for getting the set team_id
app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(auth('api')->user()->getTeamIdFromToken());
}*/

return $next($request);
}
}
```
NOTE: You must add your custom `Middleware` to `$middlewarePriority` on `app/Http/Kernel.php`.

## Roles Creating

When creating a role you can pass the `team_id` as an optional parameter

```php
// with null team_id it creates a global role, global roles can be used from any team and they are unique
Role::create(['name' => 'writer', 'team_id' => null]);

// creates a role with team_id = 1, team roles can have the same name on different teams
Role::create(['name' => 'reader', 'team_id' => 1]);

// creating a role without team_id makes the role take the default global team_id
Role::create(['name' => 'reviewer']);
```

## Roles/Permissions Assignment & Removal

The role/permission assignment and removal are the same, but they take the global `team_id` set on login for sync.
1 change: 1 addition & 0 deletions docs/installation-laravel.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ This package can be used with Laravel 6.0 or higher.
```
6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability.
If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`.
7. Clear your config cache. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands:
Expand Down
2 changes: 2 additions & 0 deletions docs/installation-lumen.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ $app->register(App\Providers\AuthServiceProvider::class);

Ensure the application's database name/credentials are set in your `.env` (or `config/database.php` if you have one), and that the database exists.

NOTE: If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`.

Run the migrations to create the tables for this package:

```bash
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/CreatePermission.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@ public function handle()

$permission = $permissionClass::findOrCreate($this->argument('name'), $this->argument('guard'));

$this->info("Permission `{$permission->name}` created");
$this->info("Permission `{$permission->name}` ".($permission->wasRecentlyCreated ? 'created' : 'already exists'));
}
}
20 changes: 18 additions & 2 deletions src/Commands/CreateRole.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,41 @@
use Illuminate\Console\Command;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Contracts\Role as RoleContract;
use Spatie\Permission\PermissionRegistrar;

class CreateRole extends Command
{
protected $signature = 'permission:create-role
{name : The name of the role}
{guard? : The name of the guard}
{permissions? : A list of permissions to assign to the role, separated by | }';
{permissions? : A list of permissions to assign to the role, separated by | }
{--team-id=}';

protected $description = 'Create a role';

public function handle()
{
$roleClass = app(RoleContract::class);

$teamIdAux = app(PermissionRegistrar::class)->getPermissionsTeamId();
app(PermissionRegistrar::class)->setPermissionsTeamId($this->option('team-id') ?: null);

if (! PermissionRegistrar::$teams && $this->option('team-id')) {
$this->warn("Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter");
return;
}

$role = $roleClass::findOrCreate($this->argument('name'), $this->argument('guard'));
app(PermissionRegistrar::class)->setPermissionsTeamId($teamIdAux);

$teams_key = PermissionRegistrar::$teamsKey;
if (PermissionRegistrar::$teams && $this->option('team-id') && is_null($role->$teams_key)) {
$this->warn("Role `{$role->name}` already exists on the global team; argument --team-id has no effect");
}

$role->givePermissionTo($this->makePermissions($this->argument('permissions')));

$this->info("Role `{$role->name}` created");
$this->info("Role `{$role->name}` ".($role->wasRecentlyCreated ? 'created' : 'updated'));
}

/**
Expand Down
35 changes: 27 additions & 8 deletions src/Commands/Show.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Contracts\Role as RoleContract;
use Symfony\Component\Console\Helper\TableCell;

class Show extends Command
{
Expand All @@ -19,6 +20,7 @@ public function handle()
{
$permissionClass = app(PermissionContract::class);
$roleClass = app(RoleContract::class);
$team_key = config('permission.column_names.team_foreign_key');

$style = $this->argument('style') ?? 'default';
$guard = $this->argument('guard');
Expand All @@ -32,20 +34,37 @@ public function handle()
foreach ($guards as $guard) {
$this->info("Guard: $guard");

$roles = $roleClass::whereGuardName($guard)->orderBy('name')->get()->mapWithKeys(function ($role) {
return [$role->name => $role->permissions->pluck('name')];
});
$roles = $roleClass::whereGuardName($guard)
->when(config('permission.teams'), function ($q) use ($team_key) {
$q->orderBy($team_key);
})
->orderBy('name')->get()->mapWithKeys(function ($role) use ($team_key) {
return [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key ]];
});

$permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name');
$permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id');

$body = $permissions->map(function ($permission) use ($roles) {
return $roles->map(function (Collection $role_permissions) use ($permission) {
return $role_permissions->contains($permission) ? '' : ' ·';
$body = $permissions->map(function ($permission, $id) use ($roles) {
return $roles->map(function (array $role_data) use ($id) {
return $role_data['permissions']->contains($id) ? '' : ' ·';
})->prepend($permission);
});

if (config('permission.teams')) {
$teams = $roles->groupBy($team_key)->values()->map(function ($group, $id) {
return new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]);
});
}

$this->table(
$roles->keys()->prepend('')->toArray(),
array_merge([
config('permission.teams') ? $teams->prepend('')->toArray() : [],
$roles->keys()->map(function ($val) {
$name = explode('_', $val);
return $name[0];
})
->prepend('')->toArray()
]),
$body->toArray(),
$style
);
Expand Down
Loading

0 comments on commit c54a494

Please sign in to comment.