A comprehensive Laravel package designed for advanced team-based permission management in multi-tenant applications based on [Jurager/Teams]. This package provides a flexible and powerful system for organizing users into teams, assigning granular permissions through roles and groups, and managing entity-specific access controls.
Core Functionality:
-
Team Management: Create and manage teams with owners and members. Each team operates as an independent workspace with its own set of roles, permissions, and members.
-
Role-Based Access Control (RBAC): Define custom roles for each team with specific permission sets. Roles can be assigned to users within a team, providing a flexible way to manage access levels. Team owners automatically have full access to all permissions.
-
Permission System: Implement fine-grained permissions using a code-based system (e.g.,
posts.create,users.edit). Permissions are global entities that can be assigned to roles and groups across multiple teams. Supports wildcard permissions for flexible access patterns. -
Group Management: Organize users into groups within teams or globally. Groups can have their own permission sets, and permissions assigned to a group take precedence over individual user permissions within a team. This allows for efficient permission management when multiple users need the same access level.
-
Global Groups: Create groups without team association to grant users access across all teams with the group's permissions. Perfect for scenarios like support teams, administrators, or auditors who need consistent access across multiple teams without being individually added to each one.
-
Entity-Specific Abilities: Grant or deny permissions for specific model instances (e.g., allowing a user to edit a particular post but not others). This provides the most granular level of access control, enabling fine-tuned permissions for individual resources.
-
Multi-Tenant Support: Built from the ground up for multi-tenant applications where each team represents a tenant. Teams are completely isolated, ensuring data security and access control between different tenants.
-
Caching & Performance: Optional intelligent caching system to optimize permission checks, reducing database queries and improving application performance.
-
Audit Logging: Optional comprehensive audit trail that logs all team-related actions including role assignments, permission changes, and member additions/removals.
-
REST API: Optional complete REST API for team management, enabling frontend applications and third-party integrations to manage teams programmatically.
-
Laravel Integration: Seamlessly integrates with Laravel's built-in authorization system, including Policies, Blade directives, and middleware for route protection.
- ✅ Team Management: Create and manage teams with owners and members
- ✅ Roles & Permissions: Flexible role system with granular permissions
- ✅ Groups: Organize users into groups with shared permissions
- ✅ Abilities: Entity-specific permissions for individual models
- ✅ Smart Caching: Caching system to optimize permission checks
- ✅ Audit Logging: Complete action logging for teams (optional)
- ✅ REST API: Complete API for team management (optional)
- ✅ Blade Directives: Blade directives for permission checks in views
- ✅ Policies: Integration with Laravel's Policy system
- ✅ Rate Limiting: Protection against invitation spam
- ✅ Middleware: Middleware for route protection
- ✅ Artisan Commands: CLI tools for management
- PHP >= 8.1
- Laravel 8.x, 9.x, 10.x, 11.x or 12.x
composer require squareetlabs/laravel-teams-permissionsphp artisan vendor:publish --provider="Squareetlabs\LaravelTeamsPermissions\TeamsServiceProvider"This will publish:
config/teams.php- Configuration file- Database migrations
Add the HasTeams trait to your User model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Squareetlabs\LaravelTeamsPermissions\Traits\HasTeams;
class User extends Model
{
use HasTeams;
// ... rest of your code
}
⚠️ IMPORTANT: Always do backups before running migrations.
php artisan migrateNote
If you wish to use custom foreign keys and table names, modify config/teams.php before running migrations.
To improve performance, enable caching in .env:
TEAMS_CACHE_ENABLED=true
TEAMS_CACHE_DRIVER=redis
TEAMS_CACHE_TTL=3600To log all team actions:
TEAMS_AUDIT_ENABLED=true
TEAMS_AUDIT_LOG_CHANNEL=teamsNote
If you enable audit logging after running migrations, you'll need to publish and run the audit migration:
php artisan vendor:publish --tag=teams-migrations
php artisan migrateTo expose a REST API for team management:
TEAMS_API_ENABLED=trueThe configuration file config/teams.php contains all options:
'models' => [
'user' => App\Models\User::class,
'team' => Squareetlabs\LaravelTeamsPermissions\Models\Team::class,
// ... other models
],'cache' => [
'enabled' => env('TEAMS_CACHE_ENABLED', true),
'driver' => env('TEAMS_CACHE_DRIVER', 'redis'),
'ttl' => env('TEAMS_CACHE_TTL', 3600),
'prefix' => 'teams_permissions',
'tags' => true,
],'audit' => [
'enabled' => env('TEAMS_AUDIT_ENABLED', false),
'log_channel' => env('TEAMS_AUDIT_LOG_CHANNEL', 'teams'),
'events' => [
'role_assigned',
'permission_granted',
'permission_revoked',
'team_member_added',
'team_member_removed',
],
],See config/teams.php for all available options.
use Squareetlabs\LaravelTeamsPermissions\Models\Team;
$team = Team::create([
'name' => 'My Team',
'user_id' => auth()->id(),
]);// Add role with permissions
$team->addRole('admin', [
'posts.*',
'users.*',
'settings.edit',
], 'Administrator', 'Role with all permissions');
$team->addRole('editor', [
'posts.view',
'posts.create',
'posts.edit',
], 'Editor', 'Can manage posts');// Add user with a role
$team->addUser($user, 'editor');
// Update user's role
$team->updateUser($user, 'admin');
// Remove user
$team->deleteUser($user);// Check if user has a permission
if ($user->hasTeamPermission($team, 'posts.create')) {
// User can create posts
}
// Check if user has a role
if ($user->hasTeamRole($team, 'admin')) {
// User is admin
}
// Check specific ability on an entity
if ($user->hasTeamAbility($team, 'edit', $post)) {
// User can edit this specific post
}// Access the team's owner
$team->owner
// Get all team users (excluding owner)
$team->users()
// Get all users including owner
$team->allUsers()
// Check if a user belongs to the team
$team->hasUser($user)
// Add user with a role (by ID or code)
$team->addUser($user, 'admin')
// Update user's role
$team->updateUser($user, 'editor')
// Remove user from team
$team->deleteUser($user)
// Invite user by email
$team->inviteUser('[email protected]', 'member')
// Accept invitation
$team->inviteAccept($invitation_id)
// Get all team abilities
$team->abilities()
// Get all team roles
$team->roles()
// Get user's role in the team
$team->userRole($user)
// Check if team has a role
$team->hasRole('admin') // or null to check if has any role
// Get role by ID or code
$team->getRole('admin')
// Add new role
$team->addRole($code, $permissions, $name, $description)
// Update role
$team->updateRole('admin', $newPermissions, $name, $description)
// Delete role
$team->deleteRole('admin')
// Get all groups
$team->groups()
// Get group by ID or code
$team->getGroup('moderators')
// Add new group
$team->addGroup($code, $permissions, $name)
// Update group
$team->updateGroup('moderators', $newPermissions, $name)
// Delete group
$team->deleteGroup('moderators')
// Check if team has user with email
$team->hasUserWithEmail('[email protected]')
// Check if user has permission in team
$team->userHasPermission($user, 'posts.create', $require = false)
// Get all invitations
$team->invitations()The HasTeams trait provides the following methods:
// Get teams the user belongs to
$user->teams
// Get teams the user owns
$user->ownedTeams
// Get all teams (owned and belongs to)
$user->allTeams()
// Check if user owns a team
$user->ownsTeam($team)
// Check if user belongs to a team
$user->belongsToTeam($team)
// Get user's role in a team
$user->teamRole($team)
// Check if user has a role (or roles) in a team
// $require = true: all roles are required
// $require = false: at least one of the roles
$user->hasTeamRole($team, 'admin', $require = false)
$user->hasTeamRole($team, ['admin', 'editor'], $require = false)
// Get all user's permissions for a team
// $scope: 'role', 'group', or null for all
$user->teamPermissions($team, $scope = null)
// Check if user has a permission (or permissions) in a team
// $require = true: all permissions are required
// $require = false: at least one of the permissions
// $scope: 'role', 'group', or null for all
$user->hasTeamPermission($team, 'posts.create', $require = false, $scope = null)
$user->hasTeamPermission($team, ['posts.create', 'posts.edit'], $require = false)
// Get user's abilities for a specific entity
$user->teamAbilities($team, $entity, $forbidden = false)
// Check if user has an ability on an entity
$user->hasTeamAbility($team, 'edit', $post)
// Allow ability for user on an entity
$user->allowTeamAbility($team, 'edit', $post)
// Forbid ability for user on an entity
$user->forbidTeamAbility($team, 'edit', $post)
// Delete ability
$user->deleteTeamAbility($team, 'edit', $post)
// Scope for eager loading permissions
User::withTeamPermissions()->get()$team->addRole('admin', [
'posts.*', // All post permissions
'users.view', // View users
'users.create', // Create users
'users.edit', // Edit users
'users.delete', // Delete users
'settings.*', // All settings permissions
], 'Administrator', 'Role with full access');You can use wildcards for permissions:
posts.*- All permissions starting withposts.*- All permissions (if enabled in config)
// Check simple permission
if ($user->hasTeamPermission($team, 'posts.create')) {
// User can create posts
}
// Check multiple permissions (OR)
if ($user->hasTeamPermission($team, ['posts.create', 'posts.edit'], false)) {
// User can create OR edit posts
}
// Check multiple permissions (AND)
if ($user->hasTeamPermission($team, ['posts.create', 'posts.edit'], true)) {
// User can create AND edit posts
}You can enable wildcard permissions in configuration:
'wildcards' => [
'enabled' => true,
'nodes' => [
'*',
'*.*',
'all'
]
]Users with these permissions will have full access to the team.
Abilities allow specific permissions for individual entities.
// Allow user to edit a specific post
$user->allowTeamAbility($team, 'edit', $post);
// Forbid user to edit a specific post
$user->forbidTeamAbility($team, 'edit', $post);if ($user->hasTeamAbility($team, 'edit', $post)) {
// User can edit this specific post
}Abilities use an access level system:
| Level | Value | Description |
|---|---|---|
DEFAULT |
0 | No explicit permissions |
FORBIDDEN |
1 | Access denied |
ROLE_ALLOWED |
2 | Allowed by role |
ROLE_FORBIDDEN |
3 | Forbidden by role |
GROUP_ALLOWED |
4 | Allowed by group |
GROUP_FORBIDDEN |
5 | Forbidden by group |
USER_ALLOWED |
5 | Specifically allowed to user |
USER_FORBIDDEN |
6 | Specifically forbidden to user |
GLOBAL_ALLOWED |
6 | Global permissions |
Access is granted if the allowed level >= forbidden level.
Groups allow organizing users with shared permissions.
// Add group
$team->addGroup('moderators', [
'posts.moderate',
'comments.moderate',
], 'Moderators');
// Update group
$team->updateGroup('moderators', [
'posts.moderate',
'comments.moderate',
'users.moderate',
], 'Moderators');
// Delete group
$team->deleteGroup('moderators');
// Get group
$group = $team->getGroup('moderators');
// Add users to group
$group->users()->attach($user);
// or multiple users
$group->users()->attach([$user1->id, $user2->id]);
// Remove users from group
$group->users()->detach($user);Groups without team_id are global and apply to all teams:
// Create global group (team_id = null)
$globalGroup = Group::create([
'code' => 'support',
'name' => 'Support Team',
'team_id' => null,
]);The package provides middleware for route protection.
Middleware is automatically registered as role, permission, and ability.
// Check role
Route::middleware(['role:admin,team_id'])->group(function () {
Route::get('/admin', [AdminController::class, 'index']);
});
// Check permission
Route::middleware(['permission:posts.create,team_id'])->group(function () {
Route::post('/posts', [PostController::class, 'store']);
});
// Check ability
Route::middleware(['ability:edit,App\Models\Post,post_id'])->group(function () {
Route::put('/posts/{post_id}', [PostController::class, 'update']);
});// User must have admin OR root
Route::middleware(['role:admin|root,team_id'])->group(function () {
// ...
});// User must have admin AND editor
Route::middleware(['role:admin|editor,team_id,require'])->group(function () {
// ...
});The package includes Blade directives for permission checks in views:
{{-- Check role --}}
@teamRole($team, 'admin')
<button>Admin Panel</button>
@endteamRole
{{-- Check permission --}}
@teamPermission($team, 'posts.create')
<a href="{{ route('posts.create') }}">New Post</a>
@endteamPermission
{{-- Check ability --}}
@teamAbility($team, 'edit', $post)
<button>Edit Post</button>
@endteamAbilityThe package integrates with Laravel's Policy system.
php artisan teams:policy PostPolicy --model=PostThis generates a policy extending TeamPolicy:
namespace App\Policies;
use App\Models\Post;
use Squareetlabs\LaravelTeamsPermissions\Policies\TeamPolicy;
class PostPolicy extends TeamPolicy
{
public function view(User $user, Post $post): bool
{
$team = $this->getTeamFromModel($post);
return $this->checkTeamPermission($user, $team, 'posts.view');
}
public function update(User $user, Post $post): bool
{
$team = $this->getTeamFromModel($post);
return $this->checkTeamAbility($user, $team, 'posts.update', $post);
}
}// In a controller
if ($user->can('view', $post)) {
// User can view the post
}
// In a view
@can('update', $post)
<button>Edit</button>
@endcanIf you enable the REST API, you'll have access to complete endpoints for team management.
TEAMS_API_ENABLED=trueGET /api/teams - List teams
POST /api/teams - Create team
GET /api/teams/{team} - View team
PUT /api/teams/{team} - Update team
DELETE /api/teams/{team} - Delete team
GET /api/teams/{team}/members - List members
POST /api/teams/{team}/members - Add member
PUT /api/teams/{team}/members/{user} - Update member role
DELETE /api/teams/{team}/members/{user} - Remove member
GET /api/teams/{team}/roles - List roles
POST /api/teams/{team}/roles - Create role
PUT /api/teams/{team}/roles/{role} - Update role
DELETE /api/teams/{team}/roles/{role} - Delete role
GET /api/teams/{team}/groups - List groups
POST /api/teams/{team}/groups - Create group
PUT /api/teams/{team}/groups/{group} - Update group
DELETE /api/teams/{team}/groups/{group} - Delete group
GET /api/teams/{team}/permissions - List permissions
The API requires Sanctum authentication:
// In your frontend application
axios.get('/api/teams', {
headers: {
'Authorization': 'Bearer ' + token
}
});The package includes several useful commands:
# List all teams
php artisan teams:list
# View team details
php artisan teams:show {team}
# View team permissions
php artisan teams:permissions {team}
# Add member to team
php artisan teams:add-member {team} {user} {role}# Sync permissions from configuration
php artisan teams:sync-permissions
# Export team permissions
php artisan teams:export-permissions {team} --format=json
# Import permissions to team
php artisan teams:import-permissions {team} --file=permissions.json# Clear permissions cache
php artisan teams:clear-cache
# Generate a policy
php artisan teams:policy PostPolicy --model=PostThe caching system significantly improves permission check performance.
'cache' => [
'enabled' => true,
'driver' => 'redis',
'ttl' => 3600, // 1 hour
'prefix' => 'teams_permissions',
'tags' => true,
],php artisan teams:clear-cacheOr programmatically:
use Squareetlabs\LaravelTeamsPermissions\Support\Services\PermissionCache;
$cache = new PermissionCache();
$cache->flush();The audit system logs all important team actions.
TEAMS_AUDIT_ENABLED=true
TEAMS_AUDIT_LOG_CHANNEL=teamsrole_assigned- Role assignmentpermission_granted- Permission grantedpermission_revoked- Permission revokedteam_member_added- Member addedteam_member_removed- Member removed
use Squareetlabs\LaravelTeamsPermissions\Models\TeamAuditLog;
// Get logs for a team
$logs = TeamAuditLog::where('team_id', $team->id)->get();
// Get logs for a user
$logs = TeamAuditLog::where('user_id', $user->id)->get();
// Get logs for a specific action
$logs = TeamAuditLog::where('action', 'team_member_added')->get();Note
If you enable audit logging after running migrations, you'll need to run:
php artisan vendor:publish --tag=teams-migrations
php artisan migrateThe package fires events for important actions:
use Squareetlabs\LaravelTeamsPermissions\Events\TeamMemberAdded;
use Squareetlabs\LaravelTeamsPermissions\Events\TeamMemberRemoved;
Event::listen(TeamMemberAdded::class, function ($team, $user) {
// Notify user they were added
});
Event::listen(TeamMemberRemoved::class, function ($team, $user) {
// Notify user they were removed
});TeamCreating/TeamCreatedTeamUpdating/TeamUpdatedTeamDeletedTeamMemberAdding/TeamMemberAddedTeamMemberRemoving/TeamMemberRemovedTeamMemberUpdatedTeamMemberInviting/TeamMemberInvited
The package includes validation rules:
use Squareetlabs\LaravelTeamsPermissions\Rules\ValidPermission;
$request->validate([
'permission' => ['required', new ValidPermission()],
]);// Create team
$team = Team::create([
'name' => 'Blog Team',
'user_id' => auth()->id(),
]);
// Add roles
$team->addRole('editor', ['posts.*', 'comments.moderate'], 'Editor');
$team->addRole('author', ['posts.create', 'posts.edit'], 'Author');
$team->addRole('viewer', ['posts.view'], 'Viewer');
// In a controller
public function store(Request $request)
{
$team = Team::find($request->team_id);
if (!auth()->user()->hasTeamPermission($team, 'posts.create')) {
abort(403);
}
// Create post...
}// Each client has their own team
$clientTeam = Team::create([
'name' => $client->name,
'user_id' => $client->owner_id,
]);
// Client-specific roles
$clientTeam->addRole('admin', ['*'], 'Administrator');
$clientTeam->addRole('user', ['dashboard.view', 'reports.view'], 'User');
// Check access in middleware
Route::middleware(['permission:dashboard.view,team_id'])->group(function () {
Route::get('/dashboard', [DashboardController::class, 'index']);
});The package includes factories and seeders for testing:
use Squareetlabs\LaravelTeamsPermissions\Database\Factories\TeamFactory;
$team = TeamFactory::new()
->withRoles()
->withGroups()
->create();Make sure your User model is configured in config/teams.php:
'models' => [
'user' => App\Models\User::class,
],If you enable audit logging after running migrations:
php artisan vendor:publish --tag=teams-migrations
php artisan migrateOr disable audit logging in config/teams.php:
'audit' => [
'enabled' => false,
],Clear cache manually:
php artisan teams:clear-cacheMethods available on User model:
ownsTeam(Team $team): bool- Check if user owns the teamallTeams(): Collection- Get all teams (owned and belongs to)ownedTeams(): HasMany- Get teams user ownsteams(): BelongsToMany- Get teams user belongs tobelongsToTeam(Team $team): bool- Check if user belongs to teamteamRole(Team $team): ?Role- Get user's role in teamhasTeamRole(Team $team, string|array $roles, bool $require = false): bool- Check if user has role(s)teamPermissions(Team $team, ?string $scope = null): array- Get user's permissions for teamhasTeamPermission(Team $team, string|array $permissions, bool $require = false, ?string $scope = null): bool- Check if user has permission(s)teamAbilities(Team $team, Model $entity, bool $forbidden = false): Collection- Get user's abilities for entityhasTeamAbility(Team $team, string $permission, Model $action_entity): bool- Check if user has abilityallowTeamAbility(Team $team, string $permission, Model $action_entity, ?Model $target_entity = null): void- Allow abilityforbidTeamAbility(Team $team, string $permission, Model $action_entity, ?Model $target_entity = null): void- Forbid abilitydeleteTeamAbility(Team $team, string $permission, Model $action_entity, ?Model $target_entity = null): void- Delete abilityscopeWithTeamPermissions($query)- Eager load team permissions
Methods available on Team model:
owner(): BelongsTo- Get team ownerusers(): BelongsToMany- Get team membersabilities(): HasMany- Get team abilitiesroles(): HasMany- Get team rolesgroups(): HasMany- Get team groupsinvitations(): HasMany- Get pending invitationsallUsers(): Collection- Get all users including ownerhasUser(User $user): bool- Check if user is memberaddUser(User $user, string $role_keyword): void- Add user to teamupdateUser(User $user, string $role_keyword): void- Update user's roledeleteUser(User $user): void- Remove user from teaminviteUser(string $email, int|string $keyword): void- Invite user by emailinviteAccept(int $invitation_id): void- Accept invitationhasUserWithEmail(string $email): bool- Check if team has user with emailuserRole(User $user): ?Role- Get user's role in teamuserHasPermission(User $user, string|array $permissions, bool $require = false): bool- Check if user has permissionhasRole(int|string|null $keyword = null): bool- Check if team has rolegetRole(int|string $keyword): ?Role- Get role by ID or codeaddRole(string $code, array $permissions, ?string $name = null, ?string $description = null): Role- Add roleupdateRole(int|string $keyword, array $permissions, ?string $name = null, ?string $description = null): Role- Update roledeleteRole(int|string $keyword): bool- Delete rolehasGroup(int|string|null $keyword = null): bool- Check if team has groupgetGroup(int|string $keyword): ?Group- Get group by ID or codeaddGroup(string $code, array $permissions = [], ?string $name = null): Group- Add groupupdateGroup(int|string $keyword, array $permissions = [], ?string $name = null): Group- Update groupdeleteGroup(int|string $keyword): bool- Delete grouppurge(): void- Delete team and all relationsgetPermissionIds(array $codes): array- Get permission IDs for codes
remember(string $key, callable $callback, ?int $ttl = null): mixed- Cache a valueflush(): void- Flush all cacheforget(string $key): void- Forget specific keyget(string $key, mixed $default = null): mixed- Get cached value
log(string $action, mixed $team, mixed $user, mixed $subject = null, ?array $oldValues = null, ?array $newValues = null): void- Log audit eventlogRoleAssigned(mixed $team, mixed $user, mixed $role): void- Log role assignmentlogPermissionGranted(mixed $team, mixed $user, string $permission): void- Log permission grantedlogPermissionRevoked(mixed $team, mixed $user, string $permission): void- Log permission revokedlogTeamMemberAdded(mixed $team, mixed $user, mixed $member, mixed $role): void- Log member addedlogTeamMemberRemoved(mixed $team, mixed $user, mixed $member): void- Log member removed
checkTeamPermission(Model $user, Model $team, string $permission): bool- Check team permissioncheckTeamAbility(Model $user, Model $team, string $ability, Model $model): bool- Check team abilitycheckTeamRole(Model $user, Model $team, string|array $roles): bool- Check team rolegetTeamFromModel(Model $model): ?Model- Get team from model
Contributions are welcome. Please:
- Fork the project
- Create your feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
See CHANGELOG.md for the full list of changes.
This package is open-sourced software licensed under the MIT license.
For support, please open an issue on GitHub.
- Squareetlabs - [email protected]