Skip to content

Commit c54a494

Browse files
authored
Merge pull request #1804 from erikn69/teams_feature
Teams/Groups feature
2 parents baeee79 + 8049909 commit c54a494

23 files changed

+689
-37
lines changed

config/permission.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,25 @@
8787
*/
8888

8989
'model_morph_key' => 'model_id',
90+
91+
/*
92+
* Change this if you want to use the teams feature and your related model's
93+
* foreign key is other than `team_id`.
94+
*/
95+
96+
'team_foreign_key' => 'team_id',
9097
],
9198

99+
/*
100+
* When set to true the package implements teams using the 'team_foreign_key'. If you want
101+
* the migrations to register the 'team_foreign_key', you must set this to true
102+
* before doing the migration. If you already did the migration then you must make a new
103+
* migration to also add 'team_foreign_key' to 'roles', 'model_has_roles', and
104+
* 'model_has_permissions'(view the latest version of package's migration file)
105+
*/
106+
107+
'teams' => false,
108+
92109
/*
93110
* When set to true, the required permission names are added to the exception
94111
* message. This could be considered an information leak in some contexts, so
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
use Illuminate\Support\Facades\Schema;
4+
use Illuminate\Database\Schema\Blueprint;
5+
use Illuminate\Database\Migrations\Migration;
6+
7+
class AddTeamsFields extends Migration
8+
{
9+
/**
10+
* Run the migrations.
11+
*
12+
* @return void
13+
*/
14+
public function up()
15+
{
16+
$teams = config('permission.teams');
17+
$tableNames = config('permission.table_names');
18+
$columnNames = config('permission.column_names');
19+
20+
if (! $teams) {
21+
return;
22+
}
23+
if (empty($tableNames)) {
24+
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
25+
}
26+
if (empty($columnNames['team_foreign_key'] ?? null)) {
27+
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
28+
}
29+
30+
Schema::table($tableNames['roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
31+
if (! Schema::hasColumn($tableNames['roles'], $columnNames['team_foreign_key'])) {
32+
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
33+
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
34+
}
35+
});
36+
37+
Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
38+
if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) {
39+
$table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
40+
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
41+
42+
$table->dropPrimary();
43+
$table->primary([$columnNames['team_foreign_key'], 'permission_id', $columnNames['model_morph_key'], 'model_type'],
44+
'model_has_permissions_permission_model_type_primary');
45+
}
46+
});
47+
48+
Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
49+
if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) {
50+
$table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
51+
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
52+
53+
$table->dropPrimary();
54+
$table->primary([$columnNames['team_foreign_key'], 'role_id', $columnNames['model_morph_key'], 'model_type'],
55+
'model_has_roles_role_model_type_primary');
56+
}
57+
});
58+
59+
app('cache')
60+
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
61+
->forget(config('permission.cache.key'));
62+
}
63+
64+
/**
65+
* Reverse the migrations.
66+
*
67+
* @return void
68+
*/
69+
public function down()
70+
{
71+
72+
}
73+
}

database/migrations/create_permission_tables.php.stub

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,14 @@ class CreatePermissionTables extends Migration
1616
{
1717
$tableNames = config('permission.table_names');
1818
$columnNames = config('permission.column_names');
19+
$teams = config('permission.teams');
1920

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

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

33-
Schema::create($tableNames['roles'], function (Blueprint $table) {
37+
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
3438
$table->bigIncrements('id');
39+
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
40+
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
41+
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
42+
}
3543
$table->string('name'); // For MySQL 8.0 use string('name', 125);
3644
$table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125);
3745
$table->timestamps();
38-
39-
$table->unique(['name', 'guard_name']);
46+
if ($teams || config('permission.testing')) {
47+
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
48+
} else {
49+
$table->unique(['name', 'guard_name']);
50+
}
4051
});
4152

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

4556
$table->string('model_type');
@@ -50,12 +61,20 @@ class CreatePermissionTables extends Migration
5061
->references('id')
5162
->on($tableNames['permissions'])
5263
->onDelete('cascade');
64+
if ($teams) {
65+
$table->unsignedBigInteger($columnNames['team_foreign_key']);
66+
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
5367

54-
$table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
68+
$table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
5569
'model_has_permissions_permission_model_type_primary');
70+
} else {
71+
$table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
72+
'model_has_permissions_permission_model_type_primary');
73+
}
74+
5675
});
5776

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

6180
$table->string('model_type');
@@ -66,9 +85,16 @@ class CreatePermissionTables extends Migration
6685
->references('id')
6786
->on($tableNames['roles'])
6887
->onDelete('cascade');
88+
if ($teams) {
89+
$table->unsignedBigInteger($columnNames['team_foreign_key']);
90+
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
6991

70-
$table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
92+
$table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
93+
'model_has_roles_role_model_type_primary');
94+
} else {
95+
$table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
7196
'model_has_roles_role_model_type_primary');
97+
}
7298
});
7399

74100
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) {

docs/basic-usage/artisan.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,13 @@ When creating roles you can also create and link permissions at the same time:
3131
php artisan permission:create-role writer web "create articles|edit articles"
3232
```
3333

34+
When creating roles with teams enabled you can set the team id by adding the `--team-id` parameter:
35+
36+
```bash
37+
php artisan permission:create-role --team-id=1 writer
38+
php artisan permission:create-role writer api --team-id=1
39+
```
40+
3441
## Displaying roles and permissions in the console
3542

3643
There is also a `show` command to show a table of roles and permissions per guard:
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
title: Teams permissions
3+
weight: 3
4+
---
5+
6+
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.
7+
8+
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/).
9+
10+
11+
Teams permissions can be enabled in the permission config file:
12+
13+
```php
14+
// config/permission.php
15+
'teams' => true,
16+
```
17+
18+
Also, if you want to use a custom foreign key for teams you must change in the permission config file:
19+
```php
20+
// config/permission.php
21+
'team_foreign_key' => 'custom_team_id',
22+
```
23+
24+
## Working with Teams Permissions
25+
26+
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]);` ),
27+
we can set global `team_id` from anywhere, but works better if you create a `Middleware`, example:
28+
29+
```php
30+
namespace App\Http\Middleware;
31+
32+
class TeamsPermission{
33+
34+
public function handle($request, \Closure $next){
35+
if(!empty(auth()->user())){
36+
// session value set on login
37+
app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(session('team_id'));
38+
}
39+
// other custom ways to get team_id
40+
/*if(!empty(auth('api')->user())){
41+
// `getTeamIdFromToken()` example of custom method for getting the set team_id
42+
app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId(auth('api')->user()->getTeamIdFromToken());
43+
}*/
44+
45+
return $next($request);
46+
}
47+
}
48+
```
49+
NOTE: You must add your custom `Middleware` to `$middlewarePriority` on `app/Http/Kernel.php`.
50+
51+
## Roles Creating
52+
53+
When creating a role you can pass the `team_id` as an optional parameter
54+
55+
```php
56+
// with null team_id it creates a global role, global roles can be used from any team and they are unique
57+
Role::create(['name' => 'writer', 'team_id' => null]);
58+
59+
// creates a role with team_id = 1, team roles can have the same name on different teams
60+
Role::create(['name' => 'reader', 'team_id' => 1]);
61+
62+
// creating a role without team_id makes the role take the default global team_id
63+
Role::create(['name' => 'reviewer']);
64+
```
65+
66+
## Roles/Permissions Assignment & Removal
67+
68+
The role/permission assignment and removal are the same, but they take the global `team_id` set on login for sync.

docs/installation-laravel.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ This package can be used with Laravel 6.0 or higher.
3333
```
3434
3535
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.
36+
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`.
3637
3738
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:
3839

docs/installation-lumen.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ $app->register(App\Providers\AuthServiceProvider::class);
5252

5353
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.
5454

55+
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`.
56+
5557
Run the migrations to create the tables for this package:
5658

5759
```bash

src/Commands/CreatePermission.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public function handle()
1919

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

22-
$this->info("Permission `{$permission->name}` created");
22+
$this->info("Permission `{$permission->name}` ".($permission->wasRecentlyCreated ? 'created' : 'already exists'));
2323
}
2424
}

src/Commands/CreateRole.php

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,41 @@
55
use Illuminate\Console\Command;
66
use Spatie\Permission\Contracts\Permission as PermissionContract;
77
use Spatie\Permission\Contracts\Role as RoleContract;
8+
use Spatie\Permission\PermissionRegistrar;
89

910
class CreateRole extends Command
1011
{
1112
protected $signature = 'permission:create-role
1213
{name : The name of the role}
1314
{guard? : The name of the guard}
14-
{permissions? : A list of permissions to assign to the role, separated by | }';
15+
{permissions? : A list of permissions to assign to the role, separated by | }
16+
{--team-id=}';
1517

1618
protected $description = 'Create a role';
1719

1820
public function handle()
1921
{
2022
$roleClass = app(RoleContract::class);
2123

24+
$teamIdAux = app(PermissionRegistrar::class)->getPermissionsTeamId();
25+
app(PermissionRegistrar::class)->setPermissionsTeamId($this->option('team-id') ?: null);
26+
27+
if (! PermissionRegistrar::$teams && $this->option('team-id')) {
28+
$this->warn("Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter");
29+
return;
30+
}
31+
2232
$role = $roleClass::findOrCreate($this->argument('name'), $this->argument('guard'));
33+
app(PermissionRegistrar::class)->setPermissionsTeamId($teamIdAux);
34+
35+
$teams_key = PermissionRegistrar::$teamsKey;
36+
if (PermissionRegistrar::$teams && $this->option('team-id') && is_null($role->$teams_key)) {
37+
$this->warn("Role `{$role->name}` already exists on the global team; argument --team-id has no effect");
38+
}
2339

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

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

2945
/**

src/Commands/Show.php

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Illuminate\Support\Collection;
77
use Spatie\Permission\Contracts\Permission as PermissionContract;
88
use Spatie\Permission\Contracts\Role as RoleContract;
9+
use Symfony\Component\Console\Helper\TableCell;
910

1011
class Show extends Command
1112
{
@@ -19,6 +20,7 @@ public function handle()
1920
{
2021
$permissionClass = app(PermissionContract::class);
2122
$roleClass = app(RoleContract::class);
23+
$team_key = config('permission.column_names.team_foreign_key');
2224

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

35-
$roles = $roleClass::whereGuardName($guard)->orderBy('name')->get()->mapWithKeys(function ($role) {
36-
return [$role->name => $role->permissions->pluck('name')];
37-
});
37+
$roles = $roleClass::whereGuardName($guard)
38+
->when(config('permission.teams'), function ($q) use ($team_key) {
39+
$q->orderBy($team_key);
40+
})
41+
->orderBy('name')->get()->mapWithKeys(function ($role) use ($team_key) {
42+
return [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key ]];
43+
});
3844

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

41-
$body = $permissions->map(function ($permission) use ($roles) {
42-
return $roles->map(function (Collection $role_permissions) use ($permission) {
43-
return $role_permissions->contains($permission) ? '' : ' ·';
47+
$body = $permissions->map(function ($permission, $id) use ($roles) {
48+
return $roles->map(function (array $role_data) use ($id) {
49+
return $role_data['permissions']->contains($id) ? '' : ' ·';
4450
})->prepend($permission);
4551
});
4652

53+
if (config('permission.teams')) {
54+
$teams = $roles->groupBy($team_key)->values()->map(function ($group, $id) {
55+
return new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]);
56+
});
57+
}
58+
4759
$this->table(
48-
$roles->keys()->prepend('')->toArray(),
60+
array_merge([
61+
config('permission.teams') ? $teams->prepend('')->toArray() : [],
62+
$roles->keys()->map(function ($val) {
63+
$name = explode('_', $val);
64+
return $name[0];
65+
})
66+
->prepend('')->toArray()
67+
]),
4968
$body->toArray(),
5069
$style
5170
);

0 commit comments

Comments
 (0)