448 lines
10 KiB
Markdown
448 lines
10 KiB
Markdown
|
|
---
|
||
|
|
title: Teams and Permissions
|
||
|
|
description: Guide to workspace teams and permission management
|
||
|
|
updated: 2026-01-29
|
||
|
|
---
|
||
|
|
|
||
|
|
# Teams and Permissions
|
||
|
|
|
||
|
|
The team system provides fine-grained access control within workspaces through role-based teams with configurable permissions.
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
```
|
||
|
|
Workspace
|
||
|
|
├── Teams (permission groups)
|
||
|
|
│ ├── Owners (system team)
|
||
|
|
│ ├── Admins (system team)
|
||
|
|
│ ├── Members (system team, default)
|
||
|
|
│ └── Custom teams...
|
||
|
|
└── Members (users in workspace)
|
||
|
|
└── assigned to Team (or custom_permissions)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Quick Start
|
||
|
|
|
||
|
|
### Check Permissions
|
||
|
|
|
||
|
|
```php
|
||
|
|
use Core\Tenant\Services\WorkspaceTeamService;
|
||
|
|
|
||
|
|
$teamService = app(WorkspaceTeamService::class);
|
||
|
|
$teamService->forWorkspace($workspace);
|
||
|
|
|
||
|
|
// Single permission
|
||
|
|
if ($teamService->hasPermission($user, 'social.write')) {
|
||
|
|
// User can create/edit social content
|
||
|
|
}
|
||
|
|
|
||
|
|
// Any of multiple permissions
|
||
|
|
if ($teamService->hasAnyPermission($user, ['admin', 'owner'])) {
|
||
|
|
// User is admin or owner
|
||
|
|
}
|
||
|
|
|
||
|
|
// All permissions required
|
||
|
|
if ($teamService->hasAllPermissions($user, ['social.read', 'social.write'])) {
|
||
|
|
// User has both permissions
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Via Middleware
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Single permission
|
||
|
|
Route::middleware('workspace.permission:social.write')
|
||
|
|
->group(function () {
|
||
|
|
Route::post('/posts', [PostController::class, 'store']);
|
||
|
|
});
|
||
|
|
|
||
|
|
// Multiple permissions (OR logic)
|
||
|
|
Route::middleware('workspace.permission:admin,owner')
|
||
|
|
->group(function () {
|
||
|
|
Route::get('/settings', [SettingsController::class, 'index']);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
## System Teams
|
||
|
|
|
||
|
|
Three system teams are created by default:
|
||
|
|
|
||
|
|
### Owners
|
||
|
|
|
||
|
|
```php
|
||
|
|
[
|
||
|
|
'slug' => 'owner',
|
||
|
|
'permissions' => ['*'], // All permissions
|
||
|
|
'is_system' => true,
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
Workspace owners have unrestricted access to all features and settings.
|
||
|
|
|
||
|
|
### Admins
|
||
|
|
|
||
|
|
```php
|
||
|
|
[
|
||
|
|
'slug' => 'admin',
|
||
|
|
'permissions' => [
|
||
|
|
'workspace.read',
|
||
|
|
'workspace.manage_settings',
|
||
|
|
'workspace.manage_members',
|
||
|
|
'workspace.manage_billing',
|
||
|
|
// ... all service permissions
|
||
|
|
],
|
||
|
|
'is_system' => true,
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
Admins can manage workspace settings and members but cannot delete the workspace or transfer ownership.
|
||
|
|
|
||
|
|
### Members
|
||
|
|
|
||
|
|
```php
|
||
|
|
[
|
||
|
|
'slug' => 'member',
|
||
|
|
'permissions' => [
|
||
|
|
'workspace.read',
|
||
|
|
'social.read', 'social.write',
|
||
|
|
'bio.read', 'bio.write',
|
||
|
|
// ... basic service access
|
||
|
|
],
|
||
|
|
'is_system' => true,
|
||
|
|
'is_default' => true,
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
Default team for new members. Can use services but not manage workspace settings.
|
||
|
|
|
||
|
|
## Permission Structure
|
||
|
|
|
||
|
|
### Workspace Permissions
|
||
|
|
|
||
|
|
| Permission | Description |
|
||
|
|
|------------|-------------|
|
||
|
|
| `workspace.read` | View workspace details |
|
||
|
|
| `workspace.manage_settings` | Edit workspace settings |
|
||
|
|
| `workspace.manage_members` | Invite/remove members |
|
||
|
|
| `workspace.manage_billing` | View/manage billing |
|
||
|
|
|
||
|
|
### Service Permissions
|
||
|
|
|
||
|
|
Each service follows the pattern: `service.read`, `service.write`, `service.delete`
|
||
|
|
|
||
|
|
| Service | Permissions |
|
||
|
|
|---------|-------------|
|
||
|
|
| Social | `social.read`, `social.write`, `social.delete` |
|
||
|
|
| Bio | `bio.read`, `bio.write`, `bio.delete` |
|
||
|
|
| Analytics | `analytics.read`, `analytics.write` |
|
||
|
|
| Notify | `notify.read`, `notify.write` |
|
||
|
|
| Trust | `trust.read`, `trust.write` |
|
||
|
|
| API | `api.read`, `api.write` |
|
||
|
|
|
||
|
|
### Wildcard Permission
|
||
|
|
|
||
|
|
The `*` permission grants access to everything. Only used by the Owners team.
|
||
|
|
|
||
|
|
## WorkspaceTeamService API
|
||
|
|
|
||
|
|
### Team Management
|
||
|
|
|
||
|
|
```php
|
||
|
|
$teamService = app(WorkspaceTeamService::class);
|
||
|
|
$teamService->forWorkspace($workspace);
|
||
|
|
|
||
|
|
// List teams
|
||
|
|
$teams = $teamService->getTeams();
|
||
|
|
|
||
|
|
// Get specific team
|
||
|
|
$team = $teamService->getTeam($teamId);
|
||
|
|
$team = $teamService->getTeamBySlug('content-creators');
|
||
|
|
|
||
|
|
// Get default team for new members
|
||
|
|
$defaultTeam = $teamService->getDefaultTeam();
|
||
|
|
|
||
|
|
// Create custom team
|
||
|
|
$team = $teamService->createTeam([
|
||
|
|
'name' => 'Content Creators',
|
||
|
|
'slug' => 'content-creators',
|
||
|
|
'description' => 'Team for content creation staff',
|
||
|
|
'permissions' => ['social.read', 'social.write', 'bio.read', 'bio.write'],
|
||
|
|
'colour' => 'blue',
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Update team
|
||
|
|
$teamService->updateTeam($team, [
|
||
|
|
'permissions' => [...$team->permissions, 'analytics.read'],
|
||
|
|
]);
|
||
|
|
|
||
|
|
// Delete team (non-system only)
|
||
|
|
$teamService->deleteTeam($team);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Member Management
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Get member record
|
||
|
|
$member = $teamService->getMember($user);
|
||
|
|
|
||
|
|
// List all members
|
||
|
|
$members = $teamService->getMembers();
|
||
|
|
|
||
|
|
// List team members
|
||
|
|
$teamMembers = $teamService->getTeamMembers($team);
|
||
|
|
|
||
|
|
// Assign member to team
|
||
|
|
$teamService->addMemberToTeam($user, $team);
|
||
|
|
|
||
|
|
// Remove from team
|
||
|
|
$teamService->removeMemberFromTeam($user);
|
||
|
|
|
||
|
|
// Set custom permissions (override team)
|
||
|
|
$teamService->setMemberCustomPermissions($user, [
|
||
|
|
'social.read',
|
||
|
|
'social.write',
|
||
|
|
// No social.delete
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Permission Checks
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Get effective permissions
|
||
|
|
$permissions = $teamService->getMemberPermissions($user);
|
||
|
|
// Returns: ['workspace.read', 'social.read', 'social.write', ...]
|
||
|
|
|
||
|
|
// Check single permission
|
||
|
|
$teamService->hasPermission($user, 'social.write');
|
||
|
|
|
||
|
|
// Check any permission (OR)
|
||
|
|
$teamService->hasAnyPermission($user, ['admin', 'owner']);
|
||
|
|
|
||
|
|
// Check all permissions (AND)
|
||
|
|
$teamService->hasAllPermissions($user, ['social.read', 'social.write']);
|
||
|
|
|
||
|
|
// Role checks
|
||
|
|
$teamService->isOwner($user);
|
||
|
|
$teamService->isAdmin($user);
|
||
|
|
```
|
||
|
|
|
||
|
|
## WorkspaceMember Model
|
||
|
|
|
||
|
|
The `WorkspaceMember` model represents the user-workspace relationship:
|
||
|
|
|
||
|
|
```php
|
||
|
|
$member = WorkspaceMember::where('workspace_id', $workspace->id)
|
||
|
|
->where('user_id', $user->id)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
// Properties
|
||
|
|
$member->role; // 'owner', 'admin', 'member'
|
||
|
|
$member->team_id; // Associated team
|
||
|
|
$member->custom_permissions; // Override permissions (JSON)
|
||
|
|
$member->joined_at;
|
||
|
|
$member->invited_by;
|
||
|
|
|
||
|
|
// Relationships
|
||
|
|
$member->user;
|
||
|
|
$member->team;
|
||
|
|
$member->inviter;
|
||
|
|
|
||
|
|
// Permission methods
|
||
|
|
$member->getEffectivePermissions(); // Team + custom permissions
|
||
|
|
$member->hasPermission('social.write');
|
||
|
|
$member->hasAnyPermission(['admin', 'owner']);
|
||
|
|
$member->hasAllPermissions(['social.read', 'social.write']);
|
||
|
|
|
||
|
|
// Role checks
|
||
|
|
$member->isOwner();
|
||
|
|
$member->isAdmin();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Permission Resolution
|
||
|
|
|
||
|
|
Effective permissions are resolved in order:
|
||
|
|
|
||
|
|
1. **Role-based**: Owner role grants `*`, Admin role grants admin permissions
|
||
|
|
2. **Team permissions**: Permissions from assigned team
|
||
|
|
3. **Custom permissions**: If set, completely override team permissions
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function getEffectivePermissions(): array
|
||
|
|
{
|
||
|
|
// 1. Owner has all permissions
|
||
|
|
if ($this->isOwner()) {
|
||
|
|
return ['*'];
|
||
|
|
}
|
||
|
|
|
||
|
|
// 2. Custom permissions override team
|
||
|
|
if (!empty($this->custom_permissions)) {
|
||
|
|
return $this->custom_permissions;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Team permissions
|
||
|
|
return $this->team?->permissions ?? [];
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Workspace Invitations
|
||
|
|
|
||
|
|
### Invite Users
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Via Workspace model
|
||
|
|
$invitation = $workspace->invite(
|
||
|
|
email: 'newuser@example.com',
|
||
|
|
role: 'member',
|
||
|
|
invitedBy: $currentUser,
|
||
|
|
expiresInDays: 7
|
||
|
|
);
|
||
|
|
|
||
|
|
// Invitation sent via WorkspaceInvitationNotification
|
||
|
|
```
|
||
|
|
|
||
|
|
### Accept Invitation
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Find and accept
|
||
|
|
$invitation = WorkspaceInvitation::findPendingByToken($token);
|
||
|
|
|
||
|
|
if ($invitation && $invitation->accept($user)) {
|
||
|
|
// User added to workspace
|
||
|
|
}
|
||
|
|
|
||
|
|
// Or via Workspace static method
|
||
|
|
Workspace::acceptInvitation($token, $user);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Invitation States
|
||
|
|
|
||
|
|
```php
|
||
|
|
$invitation->isPending(); // Not accepted, not expired
|
||
|
|
$invitation->isExpired(); // Past expires_at
|
||
|
|
$invitation->isAccepted(); // Has accepted_at
|
||
|
|
```
|
||
|
|
|
||
|
|
## Custom Teams
|
||
|
|
|
||
|
|
### Creating Custom Teams
|
||
|
|
|
||
|
|
```php
|
||
|
|
$team = $teamService->createTeam([
|
||
|
|
'name' => 'Social Media Managers',
|
||
|
|
'slug' => 'social-managers',
|
||
|
|
'description' => 'Team for managing social media accounts',
|
||
|
|
'permissions' => [
|
||
|
|
'workspace.read',
|
||
|
|
'social.read',
|
||
|
|
'social.write',
|
||
|
|
'social.delete',
|
||
|
|
'analytics.read',
|
||
|
|
],
|
||
|
|
'colour' => 'purple',
|
||
|
|
'is_default' => false,
|
||
|
|
]);
|
||
|
|
```
|
||
|
|
|
||
|
|
### Making Team Default
|
||
|
|
|
||
|
|
```php
|
||
|
|
$teamService->updateTeam($team, ['is_default' => true]);
|
||
|
|
// Other teams automatically have is_default set to false
|
||
|
|
```
|
||
|
|
|
||
|
|
### Deleting Teams
|
||
|
|
|
||
|
|
```php
|
||
|
|
// Only non-system teams can be deleted
|
||
|
|
// Teams with members cannot be deleted
|
||
|
|
|
||
|
|
if ($team->is_system) {
|
||
|
|
throw new \RuntimeException('Cannot delete system teams');
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($teamService->countTeamMembers($team) > 0) {
|
||
|
|
throw new \RuntimeException('Remove members first');
|
||
|
|
}
|
||
|
|
|
||
|
|
$teamService->deleteTeam($team);
|
||
|
|
```
|
||
|
|
|
||
|
|
## Seeding Default Teams
|
||
|
|
|
||
|
|
When creating a new workspace:
|
||
|
|
|
||
|
|
```php
|
||
|
|
$teamService->forWorkspace($workspace);
|
||
|
|
$teams = $teamService->seedDefaultTeams();
|
||
|
|
|
||
|
|
// Or ensure they exist (idempotent)
|
||
|
|
$teams = $teamService->ensureDefaultTeams();
|
||
|
|
```
|
||
|
|
|
||
|
|
### Migrating Existing Members
|
||
|
|
|
||
|
|
If migrating from role-based to team-based:
|
||
|
|
|
||
|
|
```php
|
||
|
|
$migrated = $teamService->migrateExistingMembers();
|
||
|
|
// Assigns members to teams based on their role:
|
||
|
|
// owner -> Owners team
|
||
|
|
// admin -> Admins team
|
||
|
|
// member -> Members team
|
||
|
|
```
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
### Use Middleware for Route Protection
|
||
|
|
|
||
|
|
```php
|
||
|
|
Route::middleware(['auth', 'workspace.required', 'workspace.permission:social.write'])
|
||
|
|
->group(function () {
|
||
|
|
Route::resource('posts', PostController::class);
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Check Permissions in Controllers
|
||
|
|
|
||
|
|
```php
|
||
|
|
public function store(Request $request)
|
||
|
|
{
|
||
|
|
$teamService = app(WorkspaceTeamService::class);
|
||
|
|
$teamService->forWorkspace($request->attributes->get('workspace_model'));
|
||
|
|
|
||
|
|
if (!$teamService->hasPermission($request->user(), 'social.write')) {
|
||
|
|
abort(403, 'You do not have permission to create posts');
|
||
|
|
}
|
||
|
|
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Use Policies with Teams
|
||
|
|
|
||
|
|
```php
|
||
|
|
class PostPolicy
|
||
|
|
{
|
||
|
|
public function create(User $user): bool
|
||
|
|
{
|
||
|
|
$teamService = app(WorkspaceTeamService::class);
|
||
|
|
return $teamService->hasPermission($user, 'social.write');
|
||
|
|
}
|
||
|
|
|
||
|
|
public function delete(User $user, Post $post): bool
|
||
|
|
{
|
||
|
|
$teamService = app(WorkspaceTeamService::class);
|
||
|
|
return $teamService->hasPermission($user, 'social.delete');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Permission Naming Conventions
|
||
|
|
|
||
|
|
Follow the pattern: `service.action`
|
||
|
|
|
||
|
|
- `service.read` - View resources
|
||
|
|
- `service.write` - Create/edit resources
|
||
|
|
- `service.delete` - Delete resources
|
||
|
|
- `workspace.manage_*` - Workspace admin actions
|