mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-11 20:22:58 +00:00
Merge remote-tracking branch 'origin/next' into fix/application-branch-validation
This commit is contained in:
@@ -137,9 +137,11 @@ class DeleteUserTeams
|
||||
|
||||
// Update the new owner's role to owner
|
||||
$team->members()->updateExistingPivot($newOwner->id, ['role' => 'owner']);
|
||||
RevokeUserTeamTokens::forUserTeam($newOwner, $team->id);
|
||||
|
||||
// Remove the current user from the team
|
||||
$team->members()->detach($this->user->id);
|
||||
RevokeUserTeamTokens::forUserTeam($this->user, $team->id);
|
||||
|
||||
$counts['transferred']++;
|
||||
} catch (\Exception $e) {
|
||||
@@ -152,6 +154,7 @@ class DeleteUserTeams
|
||||
foreach ($preview['to_leave'] as $team) {
|
||||
try {
|
||||
$team->members()->detach($this->user->id);
|
||||
RevokeUserTeamTokens::forUserTeam($this->user, $team->id);
|
||||
$counts['left']++;
|
||||
} catch (\Exception $e) {
|
||||
\Log::error("Failed to remove user from team {$team->id}: ".$e->getMessage());
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace App\Actions\User;
|
||||
|
||||
use App\Models\PersonalAccessToken;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class RevokeUserTeamTokens
|
||||
{
|
||||
public static function forUserTeam(User|int $user, int|string $teamId): int
|
||||
{
|
||||
return self::baseQuery()
|
||||
->where('tokenable_id', self::userId($user))
|
||||
->where('team_id', $teamId)
|
||||
->delete();
|
||||
}
|
||||
|
||||
public static function forUser(User|int $user): int
|
||||
{
|
||||
return self::baseQuery()
|
||||
->where('tokenable_id', self::userId($user))
|
||||
->delete();
|
||||
}
|
||||
|
||||
public static function forTeam(int|string $teamId): int
|
||||
{
|
||||
return self::baseQuery()
|
||||
->where('team_id', $teamId)
|
||||
->delete();
|
||||
}
|
||||
|
||||
private static function baseQuery(): Builder
|
||||
{
|
||||
return PersonalAccessToken::query()
|
||||
->where('tokenable_type', User::class);
|
||||
}
|
||||
|
||||
private static function userId(User|int $user): int
|
||||
{
|
||||
return $user instanceof User ? $user->id : $user;
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ use App\Http\Middleware\CheckForcePasswordReset;
|
||||
use App\Http\Middleware\DecideWhatToDoWithUser;
|
||||
use App\Http\Middleware\EncryptCookies;
|
||||
use App\Http\Middleware\EnsureMcpEnabled;
|
||||
use App\Http\Middleware\EnsureTokenBelongsToCurrentTeamMember;
|
||||
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||
use App\Http\Middleware\RedirectIfAuthenticated;
|
||||
use App\Http\Middleware\TrimStrings;
|
||||
@@ -104,6 +105,7 @@ class Kernel extends HttpKernel
|
||||
'ability' => CheckForAnyAbility::class,
|
||||
'api.ability' => ApiAbility::class,
|
||||
'api.sensitive' => ApiSensitiveData::class,
|
||||
'api.token.team' => EnsureTokenBelongsToCurrentTeamMember::class,
|
||||
'can.create.resources' => CanCreateResources::class,
|
||||
'can.update.resource' => CanUpdateResource::class,
|
||||
'can.access.terminal' => CanAccessTerminal::class,
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class EnsureTokenBelongsToCurrentTeamMember
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$user = $request->user();
|
||||
$token = $user?->currentAccessToken();
|
||||
$teamId = $token?->team_id;
|
||||
|
||||
if (! $user || ! $token || is_null($teamId)) {
|
||||
return response()->json(['message' => 'Invalid token.'], 401);
|
||||
}
|
||||
|
||||
$team = $user->teams()
|
||||
->where('teams.id', $teamId)
|
||||
->first();
|
||||
|
||||
if (! $team) {
|
||||
return response()->json(['message' => 'Invalid token.'], 401);
|
||||
}
|
||||
|
||||
$role = $team->pivot?->role;
|
||||
if (($token->can('root') || $token->can('write') || $token->can('write:sensitive'))
|
||||
&& ! in_array($role, ['admin', 'owner'], true)) {
|
||||
return response()->json(['message' => 'Missing required team role.'], 403);
|
||||
}
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -28,12 +28,11 @@ class DynamicConfigurationNavbar extends Component
|
||||
|
||||
// Decode filename: pipes are used to encode dots for Livewire property binding
|
||||
// (e.g., 'my|service.yaml' -> 'my.service.yaml')
|
||||
// This must happen BEFORE validation because validateShellSafePath() correctly
|
||||
// rejects pipe characters as dangerous shell metacharacters
|
||||
// This must happen BEFORE validation because validateFilenameSafe()
|
||||
// rejects pipe characters through validateShellSafePath().
|
||||
$file = str_replace('|', '.', $fileName);
|
||||
|
||||
// Validate filename to prevent command injection
|
||||
validateShellSafePath($file, 'proxy configuration filename');
|
||||
validateFilenameSafe($file, 'proxy configuration filename');
|
||||
|
||||
if ($proxy_type === 'CADDY' && $file === 'Caddyfile') {
|
||||
$this->dispatch('error', 'Cannot delete Caddyfile.');
|
||||
|
||||
@@ -43,8 +43,7 @@ class NewDynamicConfiguration extends Component
|
||||
'value' => 'required',
|
||||
]);
|
||||
|
||||
// Additional security validation to prevent command injection
|
||||
validateShellSafePath($this->fileName, 'proxy configuration filename');
|
||||
validateFilenameSafe($this->fileName, 'proxy configuration filename');
|
||||
|
||||
if (data_get($this->parameters, 'server_uuid')) {
|
||||
$this->server = Server::ownedByCurrentTeam()->whereUuid(data_get($this->parameters, 'server_uuid'))->first();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Team;
|
||||
|
||||
use App\Actions\User\RevokeUserTeamTokens;
|
||||
use App\Enums\Role;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
@@ -23,7 +24,9 @@ class Member extends Component
|
||||
|| Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
|
||||
throw new \Exception('You are not authorized to perform this action.');
|
||||
}
|
||||
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::ADMIN->value]);
|
||||
$teamId = currentTeam()->id;
|
||||
$this->member->teams()->updateExistingPivot($teamId, ['role' => Role::ADMIN->value]);
|
||||
RevokeUserTeamTokens::forUserTeam($this->member, $teamId);
|
||||
$this->dispatch('reloadWindow');
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
@@ -39,7 +42,9 @@ class Member extends Component
|
||||
|| Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
|
||||
throw new \Exception('You are not authorized to perform this action.');
|
||||
}
|
||||
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::OWNER->value]);
|
||||
$teamId = currentTeam()->id;
|
||||
$this->member->teams()->updateExistingPivot($teamId, ['role' => Role::OWNER->value]);
|
||||
RevokeUserTeamTokens::forUserTeam($this->member, $teamId);
|
||||
$this->dispatch('reloadWindow');
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
@@ -55,7 +60,9 @@ class Member extends Component
|
||||
|| Role::from($this->getMemberRole())->gt(auth()->user()->role())) {
|
||||
throw new \Exception('You are not authorized to perform this action.');
|
||||
}
|
||||
$this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::MEMBER->value]);
|
||||
$teamId = currentTeam()->id;
|
||||
$this->member->teams()->updateExistingPivot($teamId, ['role' => Role::MEMBER->value]);
|
||||
RevokeUserTeamTokens::forUserTeam($this->member, $teamId);
|
||||
$this->dispatch('reloadWindow');
|
||||
} catch (\Exception $e) {
|
||||
$this->dispatch('error', $e->getMessage());
|
||||
@@ -73,6 +80,7 @@ class Member extends Component
|
||||
}
|
||||
$teamId = currentTeam()->id;
|
||||
$this->member->teams()->detach(currentTeam());
|
||||
RevokeUserTeamTokens::forUserTeam($this->member, $teamId);
|
||||
// Clear cache for the removed user - both old and new key formats
|
||||
Cache::forget("team:{$this->member->id}");
|
||||
Cache::forget("user:{$this->member->id}:team:{$teamId}");
|
||||
|
||||
@@ -28,8 +28,14 @@ trait ResolvesTeam
|
||||
|
||||
protected function resolveTeamId(Request $request): ?int
|
||||
{
|
||||
$token = $request->user()?->currentAccessToken();
|
||||
$user = $request->user();
|
||||
$token = $user?->currentAccessToken();
|
||||
$teamId = $token?->team_id;
|
||||
|
||||
return $token?->team_id;
|
||||
if (! $user || is_null($teamId) || ! $user->teams()->where('teams.id', $teamId)->exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $teamId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\User\RevokeUserTeamTokens;
|
||||
use App\Events\ServerReachabilityChanged;
|
||||
use App\Notifications\Channels\SendsDiscord;
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
@@ -72,6 +73,8 @@ class Team extends Model implements SendsDiscord, SendsEmail, SendsPushover, Sen
|
||||
});
|
||||
|
||||
static::deleting(function (Team $team) {
|
||||
RevokeUserTeamTokens::forTeam($team->id);
|
||||
|
||||
foreach ($team->privateKeys as $key) {
|
||||
$key->delete();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Actions\User\RevokeUserTeamTokens;
|
||||
use App\Jobs\UpdateStripeCustomerEmailJob;
|
||||
use App\Notifications\Channels\SendsEmail;
|
||||
use App\Notifications\TransactionalEmails\EmailChangeVerification;
|
||||
@@ -121,6 +122,8 @@ class User extends Authenticatable implements SendsEmail
|
||||
|
||||
static::deleting(function (User $user) {
|
||||
\DB::transaction(function () use ($user) {
|
||||
RevokeUserTeamTokens::forUser($user);
|
||||
|
||||
$teams = $user->teams;
|
||||
foreach ($teams as $team) {
|
||||
$user_alone_in_team = $team->members->count() === 1;
|
||||
@@ -158,6 +161,7 @@ class User extends Authenticatable implements SendsEmail
|
||||
if ($found_other_member_who_is_not_owner) {
|
||||
$found_other_member_who_is_not_owner->pivot->role = 'owner';
|
||||
$found_other_member_who_is_not_owner->pivot->save();
|
||||
RevokeUserTeamTokens::forUserTeam($found_other_member_who_is_not_owner, $team->id);
|
||||
$team->members()->detach($user->id);
|
||||
} else {
|
||||
static::finalizeTeamDeletion($user, $team);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Actions\User\RevokeUserTeamTokens;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
@@ -17,6 +18,7 @@ trait DeletesUserSessions
|
||||
Session::invalidate();
|
||||
Session::regenerateToken();
|
||||
DB::table('sessions')->where('user_id', $this->id)->delete();
|
||||
RevokeUserTeamTokens::forUser($this->id);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user