php-tenant/docs/teams-permissions.md

448 lines
10 KiB
Markdown
Raw Permalink Normal View History

---
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