fix(security): enforce team access on mutable actions
Authorize cloud provider token access, audit sensitive operations, and standardize public IDs across deployment and resource flows.
This commit is contained in:
parent
9dca7ca351
commit
062ad57740
83 changed files with 824 additions and 278 deletions
|
|
@ -18,6 +18,7 @@
|
|||
use Illuminate\Console\Command;
|
||||
use Illuminate\Mail\Message;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Support\Str;
|
||||
use Mail;
|
||||
|
||||
use function Laravel\Prompts\confirm;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@
|
|||
use OpenApi\Attributes as OA;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class ApplicationsController extends Controller
|
||||
{
|
||||
|
|
@ -1197,7 +1196,7 @@ private function create_application(Request $request, $type)
|
|||
$application->isConfigurationChanged(true);
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
|
@ -1436,7 +1435,7 @@ private function create_application(Request $request, $type)
|
|||
$application->isConfigurationChanged(true);
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
|
@ -1645,7 +1644,7 @@ private function create_application(Request $request, $type)
|
|||
$application->isConfigurationChanged(true);
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
|
@ -1691,7 +1690,7 @@ private function create_application(Request $request, $type)
|
|||
], 422);
|
||||
}
|
||||
if (! $request->has('name')) {
|
||||
$request->offsetSet('name', 'dockerfile-'.new Cuid2);
|
||||
$request->offsetSet('name', 'dockerfile-'.new_public_id());
|
||||
}
|
||||
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
|
|
@ -1765,7 +1764,7 @@ private function create_application(Request $request, $type)
|
|||
$application->isConfigurationChanged(true);
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
|
@ -1809,7 +1808,7 @@ private function create_application(Request $request, $type)
|
|||
], 422);
|
||||
}
|
||||
if (! $request->has('name')) {
|
||||
$request->offsetSet('name', 'docker-image-'.new Cuid2);
|
||||
$request->offsetSet('name', 'docker-image-'.new_public_id());
|
||||
}
|
||||
$return = $this->validateDataApplications($request, $server);
|
||||
if ($return instanceof JsonResponse) {
|
||||
|
|
@ -1884,7 +1883,7 @@ private function create_application(Request $request, $type)
|
|||
$application->isConfigurationChanged(true);
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
|
@ -2682,7 +2681,7 @@ public function update_by_uuid(Request $request)
|
|||
]);
|
||||
|
||||
if ($instantDeploy) {
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
|
@ -3589,7 +3588,7 @@ public function action_deploy(Request $request)
|
|||
|
||||
$this->authorize('deploy', $application);
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
|
@ -3787,7 +3786,7 @@ public function action_restart(Request $request)
|
|||
|
||||
$this->authorize('deploy', $application);
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
|
|
|
|||
|
|
@ -549,9 +549,18 @@ public function validateToken(Request $request)
|
|||
if (! $cloudToken) {
|
||||
return response()->json(['message' => 'Cloud provider token not found.'], 404);
|
||||
}
|
||||
$this->authorize('view', $cloudToken);
|
||||
|
||||
$validation = $this->validateProviderToken($cloudToken->provider, $cloudToken->token);
|
||||
|
||||
auditLog('api.cloud_token.validated', [
|
||||
'team_id' => $teamId,
|
||||
'cloud_token_uuid' => $cloudToken->uuid,
|
||||
'cloud_token_name' => $cloudToken->name,
|
||||
'provider' => $cloudToken->provider,
|
||||
'valid' => $validation['valid'],
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'valid' => $validation['valid'],
|
||||
'message' => $validation['valid'] ? 'Token is valid.' : $validation['error'],
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Http\Request;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class DeployController extends Controller
|
||||
{
|
||||
|
|
@ -511,7 +510,7 @@ public function deploy_resource($resource, bool $force = false, int $pr = 0, ?st
|
|||
if ($dockerTag !== null && $resource->build_pack !== 'dockerimage') {
|
||||
return ['message' => 'docker_tag can only be used with Docker Image applications.', 'deployment_uuid' => null];
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$result = queue_application_deployment(
|
||||
application: $resource,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ public function locations(Request $request)
|
|||
if (! $token) {
|
||||
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
|
||||
}
|
||||
$this->authorize('view', $token);
|
||||
|
||||
try {
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
|
|
@ -237,6 +238,7 @@ public function serverTypes(Request $request)
|
|||
if (! $token) {
|
||||
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
|
||||
}
|
||||
$this->authorize('view', $token);
|
||||
|
||||
try {
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
|
|
@ -336,6 +338,7 @@ public function images(Request $request)
|
|||
if (! $token) {
|
||||
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
|
||||
}
|
||||
$this->authorize('view', $token);
|
||||
|
||||
try {
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
|
|
@ -445,6 +448,7 @@ public function sshKeys(Request $request)
|
|||
if (! $token) {
|
||||
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
|
||||
}
|
||||
$this->authorize('view', $token);
|
||||
|
||||
try {
|
||||
$hetznerService = new HetznerService($token->token);
|
||||
|
|
@ -621,6 +625,7 @@ public function createServer(Request $request)
|
|||
if (! $token) {
|
||||
return response()->json(['message' => 'Hetzner cloud provider token not found.'], 404);
|
||||
}
|
||||
$this->authorize('view', $token);
|
||||
|
||||
// Validate private key
|
||||
$privateKey = PrivateKey::whereTeamId($teamId)->whereUuid($request->private_key_uuid)->first();
|
||||
|
|
|
|||
|
|
@ -97,12 +97,12 @@ public function push(Request $request)
|
|||
|
||||
if ($this->shouldDispatchUpdate($server, $data)) {
|
||||
PushServerUpdateJob::dispatch($server, $data);
|
||||
}
|
||||
|
||||
auditLog('sentinel.metrics_pushed', [
|
||||
'server_uuid' => $server->uuid,
|
||||
'team_id' => $server->team_id,
|
||||
]);
|
||||
auditLog('sentinel.metrics_pushed', [
|
||||
'server_uuid' => $server->uuid,
|
||||
'team_id' => $server->team_id,
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json(['message' => 'ok'], 200);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
use App\Models\ApplicationPreview;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Bitbucket extends Controller
|
||||
{
|
||||
|
|
@ -141,7 +140,7 @@ public function manual(Request $request)
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
|
|
@ -192,7 +191,7 @@ public function manual(Request $request)
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Gitea extends Controller
|
||||
{
|
||||
|
|
@ -127,7 +126,7 @@ public function manual(Request $request)
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
|
|
@ -194,7 +193,7 @@ public function manual(Request $request)
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Github extends Controller
|
||||
{
|
||||
|
|
@ -144,7 +143,7 @@ public function manual(Request $request)
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
|
|
@ -362,7 +361,7 @@ public function normal(Request $request)
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@
|
|||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Gitlab extends Controller
|
||||
{
|
||||
|
|
@ -168,7 +167,7 @@ public function manual(Request $request)
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
|
|
@ -236,7 +235,7 @@ public function manual(Request $request)
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if (! $found) {
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
|
|
@ -2207,7 +2206,7 @@ private function deploy_to_additional_destinations()
|
|||
|
||||
continue;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
queue_application_deployment(
|
||||
deployment_uuid: $deployment_uuid,
|
||||
application: $this->application,
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@
|
|||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class DatabaseBackupJob implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
|
|
@ -309,7 +308,7 @@ public function handle(): void
|
|||
// Generate unique UUID for each database backup execution
|
||||
$attempts = 0;
|
||||
do {
|
||||
$this->backup_log_uuid = (string) new Cuid2;
|
||||
$this->backup_log_uuid = new_public_id();
|
||||
$exists = ScheduledDatabaseBackupExecution::where('uuid', $this->backup_log_uuid)->exists();
|
||||
$attempts++;
|
||||
if ($attempts >= 3 && $exists) {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class ProcessGithubPullRequestWebhook implements ShouldBeEncrypted, ShouldQueue
|
||||
{
|
||||
|
|
@ -156,7 +155,7 @@ private function handleOpenAction(Application $application, ?GithubApp $githubAp
|
|||
}
|
||||
|
||||
// Queue the deployment
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $this->pullRequestId,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Index extends Component
|
||||
{
|
||||
|
|
@ -470,7 +469,7 @@ public function createNewProject()
|
|||
$this->createdProject = Project::create([
|
||||
'name' => 'My first project',
|
||||
'team_id' => currentTeam()->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
$this->currentState = 'create-resource';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Docker extends Component
|
||||
{
|
||||
|
|
@ -35,7 +34,7 @@ class Docker extends Component
|
|||
|
||||
public function mount(?string $server_id = null): void
|
||||
{
|
||||
$this->network = (string) new Cuid2;
|
||||
$this->network = new_public_id();
|
||||
$this->servers = Server::isUsable()->get();
|
||||
|
||||
if (filled($server_id)) {
|
||||
|
|
@ -68,7 +67,7 @@ public function updatedServerId(): void
|
|||
|
||||
public function generateName(): void
|
||||
{
|
||||
$name = data_get($this->selectedServer, 'name', new Cuid2);
|
||||
$name = data_get($this->selectedServer, 'name', new_public_id());
|
||||
$this->name = str("{$name}-{$this->network}")->kebab();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
// use Livewire\Component;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class MonacoEditor extends Component
|
||||
{
|
||||
|
|
@ -40,7 +39,7 @@ public function __construct(
|
|||
public function render()
|
||||
{
|
||||
if (is_null($this->id)) {
|
||||
$this->id = new Cuid2;
|
||||
$this->id = new_public_id();
|
||||
}
|
||||
|
||||
if (is_null($this->name)) {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class AddEmpty extends Component
|
||||
{
|
||||
|
|
@ -38,7 +37,7 @@ public function submit()
|
|||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'team_id' => currentTeam()->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
|
||||
$productionEnvironment = $project->environments()->where('name', 'production')->first();
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
use Livewire\Component;
|
||||
use Livewire\Features\SupportEvents\Event;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class General extends Component
|
||||
{
|
||||
|
|
@ -549,7 +548,7 @@ public function generateDomain(string $serviceName)
|
|||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
|
||||
$uuid = new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$domain = generateUrl(server: $this->application->destination->server, random: $uuid);
|
||||
$sanitizedKey = str($serviceName)->replace('-', '_')->replace('.', '_')->toString();
|
||||
$this->parsedServiceDomains[$sanitizedKey]['domain'] = $domain;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
use App\Models\Application;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Heading extends Component
|
||||
{
|
||||
|
|
@ -129,7 +128,7 @@ public function deploy(bool $force_rebuild = false)
|
|||
|
||||
protected function setDeploymentUuid()
|
||||
{
|
||||
$this->deploymentUuid = new Cuid2;
|
||||
$this->deploymentUuid = new_public_id();
|
||||
$this->parameters['deployment_uuid'] = $this->deploymentUuid;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Previews extends Component
|
||||
{
|
||||
|
|
@ -312,7 +311,7 @@ public function deploy(int $pull_request_id, ?string $pull_request_html_url = nu
|
|||
|
||||
protected function setDeploymentUuid()
|
||||
{
|
||||
$this->deployment_uuid = new Cuid2;
|
||||
$this->deployment_uuid = new_public_id();
|
||||
$this->parameters['deployment_uuid'] = $this->deployment_uuid;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class PreviewsCompose extends Component
|
||||
{
|
||||
|
|
@ -64,7 +63,7 @@ public function generate()
|
|||
if (empty($domain_string)) {
|
||||
$server = $this->preview->application->destination->server;
|
||||
$template = $this->preview->application->preview_url_template;
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
|
||||
// Generate a unique domain like main app services do
|
||||
$generated_fqdn = generateUrl(server: $server, random: $random);
|
||||
|
|
@ -79,7 +78,7 @@ public function generate()
|
|||
$domain_list = explode(',', $domain_string);
|
||||
$preview_fqdns = [];
|
||||
$template = $this->preview->application->preview_url_template;
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
|
||||
foreach ($domain_list as $single_domain) {
|
||||
$single_domain = trim($single_domain);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Rollback extends Component
|
||||
{
|
||||
|
|
@ -52,7 +51,7 @@ public function rollbackImage($commit)
|
|||
|
||||
$commit = validateGitRef($commit, 'rollback commit');
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $this->application,
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class CloneMe extends Component
|
||||
{
|
||||
|
|
@ -64,7 +63,7 @@ public function mount($project_uuid)
|
|||
->servers()
|
||||
->get()
|
||||
->reject(fn ($server) => $server->isBuildServer());
|
||||
$this->newName = str($this->project->name.'-clone-'.(string) new Cuid2)->slug();
|
||||
$this->newName = str($this->project->name.'-clone-'.new_public_id())->slug();
|
||||
}
|
||||
|
||||
public function toggleVolumeCloning(bool $value)
|
||||
|
|
@ -112,7 +111,7 @@ public function clone(string $type)
|
|||
if ($this->environment->name !== 'production') {
|
||||
$project->environments()->create([
|
||||
'name' => $this->environment->name,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
}
|
||||
$environment = $project->environments->where('name', $this->environment->name)->first();
|
||||
|
|
@ -124,7 +123,7 @@ public function clone(string $type)
|
|||
$project = $this->project;
|
||||
$environment = $this->project->environments()->create([
|
||||
'name' => $this->newName,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
}
|
||||
$applications = $this->environment->applications;
|
||||
|
|
@ -138,7 +137,7 @@ public function clone(string $type)
|
|||
}
|
||||
|
||||
foreach ($databases as $database) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newDatabase = $database->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
|
|
@ -229,7 +228,7 @@ public function clone(string $type)
|
|||
|
||||
$scheduledBackups = $database->scheduledBackups()->get();
|
||||
foreach ($scheduledBackups as $backup) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newBackup = $backup->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
|
|
@ -258,7 +257,7 @@ public function clone(string $type)
|
|||
}
|
||||
|
||||
foreach ($services as $service) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newService = $service->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
|
|
@ -282,7 +281,7 @@ public function clone(string $type)
|
|||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
'service_id' => $newService->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
|
@ -413,7 +412,7 @@ public function clone(string $type)
|
|||
|
||||
$scheduledBackups = $database->scheduledBackups()->get();
|
||||
foreach ($scheduledBackups as $backup) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newBackup = $backup->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
|
|
|
|||
|
|
@ -2,13 +2,20 @@
|
|||
|
||||
namespace App\Livewire\Project\Database;
|
||||
|
||||
use App\Models\StandalonePostgresql;
|
||||
use Exception;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
|
||||
class InitScript extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
#[Locked]
|
||||
public StandalonePostgresql $database;
|
||||
|
||||
#[Locked]
|
||||
public array $script;
|
||||
|
||||
|
|
@ -35,6 +42,7 @@ public function mount()
|
|||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->validate();
|
||||
$this->script['index'] = $this->index;
|
||||
$this->script['content'] = $this->content;
|
||||
|
|
@ -48,6 +56,7 @@ public function submit()
|
|||
public function delete()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->database);
|
||||
$this->dispatch('delete_init_script', $this->script);
|
||||
} catch (Exception $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
use App\Services\DockerImageParser;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class DockerImage extends Component
|
||||
{
|
||||
|
|
@ -130,7 +129,7 @@ public function submit()
|
|||
$imageTag = $parser->isImageHash() ? 'sha256-'.$parser->getTag() : $parser->getTag();
|
||||
|
||||
$application = Application::create([
|
||||
'name' => 'docker-image-'.new Cuid2,
|
||||
'name' => 'docker-image-'.new_public_id(),
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
use App\Models\Project;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class EmptyProject extends Component
|
||||
{
|
||||
|
|
@ -13,7 +12,7 @@ public function createEmptyProject()
|
|||
$project = Project::create([
|
||||
'name' => generate_random_name(),
|
||||
'team_id' => currentTeam()->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
|
||||
return redirectRoute($this, 'project.show', ['project_uuid' => $project->uuid, 'environment_uuid' => $project->environments->first()->uuid]);
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use App\Models\GithubApp;
|
||||
use App\Models\Project;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class SimpleDockerfile extends Component
|
||||
{
|
||||
|
|
@ -48,7 +47,7 @@ public function submit()
|
|||
$port = 80;
|
||||
}
|
||||
$application = Application::create([
|
||||
'name' => 'dockerfile-'.new Cuid2,
|
||||
'name' => 'dockerfile-'.new_public_id(),
|
||||
'repository_project_id' => 0,
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
use App\Models\ServiceDatabase;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Danger extends Component
|
||||
{
|
||||
|
|
@ -39,7 +38,7 @@ class Danger extends Component
|
|||
public function mount()
|
||||
{
|
||||
$parameters = get_route_parameters();
|
||||
$this->modalId = new Cuid2;
|
||||
$this->modalId = new_public_id();
|
||||
$this->projectUuid = data_get($parameters, 'project_uuid');
|
||||
$this->environmentUuid = data_get($parameters, 'environment_uuid');
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Destination extends Component
|
||||
{
|
||||
|
|
@ -80,7 +79,7 @@ public function redeploy(int $network_id, int $server_id)
|
|||
|
||||
return;
|
||||
}
|
||||
$deployment_uuid = new Cuid2;
|
||||
$deployment_uuid = new_public_id();
|
||||
$server = Server::ownedByCurrentTeam()->findOrFail($server_id);
|
||||
$destination = $server->standaloneDockers->where('id', $network_id)->firstOrFail();
|
||||
$result = queue_application_deployment(
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@
|
|||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class ExecuteContainerCommand extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public $selected_container = 'default';
|
||||
|
||||
public Collection $containers;
|
||||
|
|
@ -40,6 +43,7 @@ public function mount()
|
|||
if (data_get($this->parameters, 'application_uuid')) {
|
||||
$this->type = 'application';
|
||||
$this->resource = Application::ownedByCurrentTeam()->where('uuid', $this->parameters['application_uuid'])->firstOrFail();
|
||||
$this->authorize('view', $this->resource);
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
|
|
@ -56,6 +60,7 @@ public function mount()
|
|||
abort(404);
|
||||
}
|
||||
$this->resource = $resource;
|
||||
$this->authorize('view', $this->resource);
|
||||
if ($this->resource->destination->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->destination->server);
|
||||
}
|
||||
|
|
@ -63,6 +68,7 @@ public function mount()
|
|||
} elseif (data_get($this->parameters, 'service_uuid')) {
|
||||
$this->type = 'service';
|
||||
$this->resource = Service::ownedByCurrentTeam()->where('uuid', $this->parameters['service_uuid'])->firstOrFail();
|
||||
$this->authorize('view', $this->resource);
|
||||
if ($this->resource->server->isFunctional()) {
|
||||
$this->servers = $this->servers->push($this->resource->server);
|
||||
}
|
||||
|
|
@ -70,6 +76,7 @@ public function mount()
|
|||
} elseif (data_get($this->parameters, 'server_uuid')) {
|
||||
$this->type = 'server';
|
||||
$this->resource = Server::ownedByCurrentTeam()->where('uuid', $this->parameters['server_uuid'])->firstOrFail();
|
||||
$this->authorize('view', $this->resource);
|
||||
$this->servers = $this->servers->push($this->resource);
|
||||
}
|
||||
$this->servers = $this->servers->sortByDesc(fn ($server) => $server->isTerminalEnabled());
|
||||
|
|
@ -152,7 +159,9 @@ public function updatedSelectedContainer()
|
|||
public function connectToServer()
|
||||
{
|
||||
try {
|
||||
$this->authorize('canAccessTerminal');
|
||||
$server = $this->servers->first();
|
||||
$this->authorize('view', $server);
|
||||
if ($server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
|
|
@ -181,6 +190,7 @@ public function connectToContainer()
|
|||
return;
|
||||
}
|
||||
try {
|
||||
$this->authorize('canAccessTerminal');
|
||||
// Validate container name format
|
||||
if (! ValidationPatterns::isValidContainerName($this->selected_container)) {
|
||||
throw new \InvalidArgumentException('Invalid container name format');
|
||||
|
|
@ -198,6 +208,8 @@ public function connectToContainer()
|
|||
throw new \RuntimeException('Invalid server configuration.');
|
||||
}
|
||||
|
||||
$this->authorize('view', $server);
|
||||
|
||||
if ($server->isForceDisabled()) {
|
||||
throw new \RuntimeException('Server is disabled.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@
|
|||
use App\Models\SwarmDocker;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class ResourceOperations extends Component
|
||||
{
|
||||
|
|
@ -66,7 +65,7 @@ public function cloneTo($destination_id)
|
|||
if (! $new_destination) {
|
||||
return $this->addError('destination_id', 'Destination not found.');
|
||||
}
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$server = $new_destination->server;
|
||||
|
||||
if ($this->resource->getMorphClass() === Application::class) {
|
||||
|
|
@ -89,7 +88,7 @@ public function cloneTo($destination_id)
|
|||
$this->resource->getMorphClass() === StandaloneDragonfly::class ||
|
||||
$this->resource->getMorphClass() === StandaloneClickhouse::class
|
||||
) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$new_resource = $this->resource->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
|
|
@ -180,7 +179,7 @@ public function cloneTo($destination_id)
|
|||
|
||||
$scheduledBackups = $this->resource->scheduledBackups()->get();
|
||||
foreach ($scheduledBackups as $backup) {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$newBackup = $backup->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
|
|
@ -216,7 +215,7 @@ public function cloneTo($destination_id)
|
|||
|
||||
return redirect()->to($route);
|
||||
} elseif ($this->resource->type() === 'service') {
|
||||
$uuid = (string) new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
$new_resource = $this->resource->replicate([
|
||||
'id',
|
||||
'created_at',
|
||||
|
|
@ -243,7 +242,7 @@ public function cloneTo($destination_id)
|
|||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
'service_id' => $new_resource->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -5,11 +5,14 @@
|
|||
use App\Helpers\SshMultiplexingHelper;
|
||||
use App\Models\Server;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Component;
|
||||
|
||||
class Terminal extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public bool $hasShell = true;
|
||||
|
||||
public bool $isTerminalConnected = false;
|
||||
|
|
@ -32,7 +35,11 @@ private function checkShellAvailability(Server $server, string $container): bool
|
|||
#[On('send-terminal-command')]
|
||||
public function sendTerminalCommand($isContainer, $identifier, $serverUuid)
|
||||
{
|
||||
$this->authorize('canAccessTerminal');
|
||||
|
||||
$server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail();
|
||||
$this->authorize('view', $server);
|
||||
|
||||
if (! $server->isTerminalEnabled() || $server->isForceDisabled()) {
|
||||
abort(403, 'Terminal access is disabled on this server.');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Show extends Component
|
||||
{
|
||||
|
|
@ -49,7 +48,7 @@ public function submit()
|
|||
$environment = Environment::create([
|
||||
'name' => $this->name,
|
||||
'project_id' => $this->project->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
|
||||
return redirectRoute($this, 'project.resource.index', [
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Livewire\Security;
|
||||
|
||||
use App\Models\CloudInitScript;
|
||||
use App\Rules\ValidCloudInitYaml;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
|
|
@ -40,7 +41,7 @@ protected function rules(): array
|
|||
{
|
||||
return [
|
||||
'name' => 'required|string|max:255',
|
||||
'script' => ['required', 'string', new \App\Rules\ValidCloudInitYaml],
|
||||
'script' => ['required', 'string', new ValidCloudInitYaml],
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -68,17 +69,29 @@ public function save()
|
|||
'script' => $this->script,
|
||||
]);
|
||||
|
||||
auditLog('ui.cloud_init_script.updated', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_init_script_id' => $cloudInitScript->id,
|
||||
'cloud_init_script_name' => $cloudInitScript->name,
|
||||
]);
|
||||
|
||||
$message = 'Cloud-init script updated successfully.';
|
||||
} else {
|
||||
// Create new script
|
||||
$this->authorize('create', CloudInitScript::class);
|
||||
|
||||
CloudInitScript::create([
|
||||
$cloudInitScript = CloudInitScript::create([
|
||||
'team_id' => currentTeam()->id,
|
||||
'name' => $this->name,
|
||||
'script' => $this->script,
|
||||
]);
|
||||
|
||||
auditLog('ui.cloud_init_script.created', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_init_script_id' => $cloudInitScript->id,
|
||||
'cloud_init_script_name' => $cloudInitScript->name,
|
||||
]);
|
||||
|
||||
$message = 'Cloud-init script created successfully.';
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,9 +36,16 @@ public function deleteScript(int $scriptId)
|
|||
$script = CloudInitScript::ownedByCurrentTeam()->findOrFail($scriptId);
|
||||
$this->authorize('delete', $script);
|
||||
|
||||
$scriptName = $script->name;
|
||||
$script->delete();
|
||||
$this->loadScripts();
|
||||
|
||||
auditLog('ui.cloud_init_script.deleted', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_init_script_id' => $scriptId,
|
||||
'cloud_init_script_name' => $scriptName,
|
||||
]);
|
||||
|
||||
$this->dispatch('success', 'Cloud-init script deleted successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -54,7 +54,6 @@ private function validateToken(string $provider, string $token): bool
|
|||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
|
||||
ray($response);
|
||||
|
||||
return $response->successful();
|
||||
}
|
||||
|
|
@ -85,6 +84,13 @@ public function addToken()
|
|||
'name' => $this->name,
|
||||
]);
|
||||
|
||||
auditLog('ui.cloud_token.created', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_token_uuid' => $savedToken->uuid,
|
||||
'cloud_token_name' => $savedToken->name,
|
||||
'provider' => $savedToken->provider,
|
||||
]);
|
||||
|
||||
$this->reset(['token', 'name']);
|
||||
|
||||
// Dispatch event with token ID so parent components can react
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Models\CloudProviderToken;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class CloudProviderTokens extends Component
|
||||
|
|
@ -57,6 +58,14 @@ public function validateToken(int $tokenId)
|
|||
} else {
|
||||
$this->dispatch('error', 'Unknown provider.');
|
||||
}
|
||||
|
||||
auditLog('ui.cloud_token.validated', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_token_uuid' => $token->uuid,
|
||||
'cloud_token_name' => $token->name,
|
||||
'provider' => $token->provider,
|
||||
'valid' => $isValid ?? false,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
@ -65,7 +74,7 @@ public function validateToken(int $tokenId)
|
|||
private function validateHetznerToken(string $token): bool
|
||||
{
|
||||
try {
|
||||
$response = \Illuminate\Support\Facades\Http::withToken($token)
|
||||
$response = Http::withToken($token)
|
||||
->timeout(10)
|
||||
->get('https://api.hetzner.cloud/v1/servers?per_page=1');
|
||||
|
||||
|
|
@ -78,7 +87,7 @@ private function validateHetznerToken(string $token): bool
|
|||
private function validateDigitalOceanToken(string $token): bool
|
||||
{
|
||||
try {
|
||||
$response = \Illuminate\Support\Facades\Http::withToken($token)
|
||||
$response = Http::withToken($token)
|
||||
->timeout(10)
|
||||
->get('https://api.digitalocean.com/v2/account');
|
||||
|
||||
|
|
@ -102,9 +111,19 @@ public function deleteToken(int $tokenId)
|
|||
return;
|
||||
}
|
||||
|
||||
$tokenUuid = $token->uuid;
|
||||
$tokenName = $token->name;
|
||||
$tokenProvider = $token->provider;
|
||||
$token->delete();
|
||||
$this->loadTokens();
|
||||
|
||||
auditLog('ui.cloud_token.deleted', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'cloud_token_uuid' => $tokenUuid,
|
||||
'cloud_token_name' => $tokenName,
|
||||
'provider' => $tokenProvider,
|
||||
]);
|
||||
|
||||
$this->dispatch('success', 'Cloud provider token deleted successfully.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Livewire\Component;
|
||||
|
||||
class Show extends Component
|
||||
|
|
@ -67,6 +68,16 @@ public function setCloudProviderToken($tokenId)
|
|||
|
||||
$this->server->cloudProviderToken()->associate($ownedToken);
|
||||
$this->server->save();
|
||||
|
||||
auditLog('ui.server.cloud_token_assigned', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'server_name' => $this->server->name,
|
||||
'cloud_token_uuid' => $ownedToken->uuid,
|
||||
'cloud_token_name' => $ownedToken->name,
|
||||
'provider' => $ownedToken->provider,
|
||||
]);
|
||||
|
||||
$this->dispatch('success', 'Hetzner token updated successfully.');
|
||||
$this->dispatch('refreshServerShow');
|
||||
} catch (\Exception $e) {
|
||||
|
|
@ -79,7 +90,7 @@ private function validateTokenForServer(CloudProviderToken $token): array
|
|||
{
|
||||
try {
|
||||
// First, validate the token itself
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->token,
|
||||
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
|
||||
|
||||
|
|
@ -92,7 +103,7 @@ private function validateTokenForServer(CloudProviderToken $token): array
|
|||
|
||||
// Check if this token can access the specific Hetzner server
|
||||
if ($this->server->hetzner_server_id) {
|
||||
$serverResponse = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
$serverResponse = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->token,
|
||||
])->timeout(10)->get("https://api.hetzner.cloud/v1/servers/{$this->server->hetzner_server_id}");
|
||||
|
||||
|
|
@ -123,7 +134,7 @@ public function validateToken()
|
|||
return;
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->token,
|
||||
])->timeout(10)->get('https://api.hetzner.cloud/v1/servers');
|
||||
|
||||
|
|
@ -132,6 +143,16 @@ public function validateToken()
|
|||
} else {
|
||||
$this->dispatch('error', 'Hetzner token is invalid or has insufficient permissions.');
|
||||
}
|
||||
|
||||
auditLog('ui.server.cloud_token_validated', [
|
||||
'team_id' => currentTeam()->id,
|
||||
'server_uuid' => $this->server->uuid,
|
||||
'server_name' => $this->server->name,
|
||||
'cloud_token_uuid' => $token->uuid,
|
||||
'cloud_token_name' => $token->name,
|
||||
'provider' => $token->provider,
|
||||
'valid' => $response->successful(),
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,21 @@
|
|||
|
||||
use App\Models\S3Storage;
|
||||
use App\Models\ScheduledDatabaseBackup;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Livewire\Component;
|
||||
|
||||
class Resources extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public S3Storage $storage;
|
||||
|
||||
public array $selectedStorages = [];
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->authorize('view', $this->storage);
|
||||
|
||||
$backups = ScheduledDatabaseBackup::where('s3_storage_id', $this->storage->id)
|
||||
->where('save_s3', true)
|
||||
->get();
|
||||
|
|
@ -25,6 +30,8 @@ public function mount(): void
|
|||
|
||||
public function disableS3(int $backupId): void
|
||||
{
|
||||
$this->authorize('update', $this->storage);
|
||||
|
||||
$backup = ScheduledDatabaseBackup::where('id', $backupId)
|
||||
->where('s3_storage_id', $this->storage->id)
|
||||
->firstOrFail();
|
||||
|
|
@ -41,6 +48,8 @@ public function disableS3(int $backupId): void
|
|||
|
||||
public function moveBackup(int $backupId): void
|
||||
{
|
||||
$this->authorize('update', $this->storage);
|
||||
|
||||
$backup = ScheduledDatabaseBackup::where('id', $backupId)
|
||||
->where('s3_storage_id', $this->storage->id)
|
||||
->firstOrFail();
|
||||
|
|
@ -62,6 +71,8 @@ public function moveBackup(int $backupId): void
|
|||
return;
|
||||
}
|
||||
|
||||
$this->authorize('update', $newStorage);
|
||||
|
||||
$backup->update(['s3_storage_id' => $newStorage->id]);
|
||||
|
||||
unset($this->selectedStorages[$backupId]);
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class InviteLink extends Component
|
||||
{
|
||||
|
|
@ -61,7 +60,7 @@ private function generateInviteLink(bool $sendEmail = false)
|
|||
if ($member_emails->contains($this->email)) {
|
||||
return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.');
|
||||
}
|
||||
$uuid = (string) new Cuid2(32);
|
||||
$uuid = new_public_id(32);
|
||||
$link = url('/').config('constants.invitation.link.base_url').$uuid;
|
||||
$user = User::whereEmail($this->email)->first();
|
||||
|
||||
|
|
|
|||
|
|
@ -7,15 +7,19 @@
|
|||
|
||||
trait ResolvesTeam
|
||||
{
|
||||
protected function ensureAbility(Request $request, string $ability = 'read'): ?Response
|
||||
protected function ensureAbility(Request $request, string $ability = 'read', ?string $tool = null): ?Response
|
||||
{
|
||||
$user = $request->user();
|
||||
if (! $user) {
|
||||
$this->auditMcpTool($request, $tool, 'denied', ['reason' => 'unauthenticated']);
|
||||
|
||||
return Response::error('Unauthenticated.');
|
||||
}
|
||||
|
||||
$token = $user->currentAccessToken();
|
||||
if (! $token) {
|
||||
$this->auditMcpTool($request, $tool, 'denied', ['reason' => 'invalid_token']);
|
||||
|
||||
return Response::error('Invalid token.');
|
||||
}
|
||||
|
||||
|
|
@ -23,6 +27,11 @@ protected function ensureAbility(Request $request, string $ability = 'read'): ?R
|
|||
return null;
|
||||
}
|
||||
|
||||
$this->auditMcpTool($request, $tool, 'denied', [
|
||||
'reason' => 'missing_ability',
|
||||
'required_ability' => $ability,
|
||||
]);
|
||||
|
||||
return Response::error("Missing required permissions: {$ability}");
|
||||
}
|
||||
|
||||
|
|
@ -38,4 +47,28 @@ protected function resolveTeamId(Request $request): ?int
|
|||
|
||||
return (int) $teamId;
|
||||
}
|
||||
|
||||
protected function mcpSuccess(Request $request, Response $response, array $context = []): Response
|
||||
{
|
||||
$this->auditMcpTool($request, $this->name ?? null, 'success', $context);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
protected function mcpError(Request $request, string $message, array $context = []): Response
|
||||
{
|
||||
$this->auditMcpTool($request, $this->name ?? null, 'error', $context + ['reason' => $message]);
|
||||
|
||||
return Response::error($message);
|
||||
}
|
||||
|
||||
protected function auditMcpTool(Request $request, ?string $tool, string $outcome, array $context = []): void
|
||||
{
|
||||
auditLog('mcp.tool.called', [
|
||||
'tool' => $tool ?: 'unknown',
|
||||
'team_id' => $this->resolveTeamId($request),
|
||||
'outcome' => $outcome,
|
||||
...$context,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,13 +13,14 @@
|
|||
use App\Mcp\Tools\ListServers;
|
||||
use App\Mcp\Tools\ListServices;
|
||||
use Laravel\Mcp\Server;
|
||||
use Laravel\Mcp\Server\Attributes\Instructions;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Attributes\Version;
|
||||
|
||||
#[Name('Coolify')]
|
||||
#[Version('0.1.0')]
|
||||
#[Instructions(<<<'MD'
|
||||
class CoolifyServer extends Server
|
||||
{
|
||||
protected string $name = 'Coolify';
|
||||
|
||||
protected string $version = '0.1.0';
|
||||
|
||||
protected string $instructions = <<<'MD'
|
||||
Read-only MCP server for Coolify, scoped to the authenticated team token.
|
||||
|
||||
Recommended workflow:
|
||||
|
|
@ -28,9 +29,8 @@
|
|||
3. get_server / get_application / get_database / get_service — full details for a single UUID.
|
||||
|
||||
Every response is `{ data, _actions?, _pagination? }`. `_actions` suggests the next tool + args; `_pagination.next` is the args to call again for the next page.
|
||||
MD)]
|
||||
class CoolifyServer extends Server
|
||||
{
|
||||
MD;
|
||||
|
||||
protected array $tools = [
|
||||
GetInfrastructureOverview::class,
|
||||
ListServers::class,
|
||||
|
|
|
|||
|
|
@ -8,36 +8,36 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_application')]
|
||||
#[Description('Get full details for a single application by UUID.')]
|
||||
class GetApplication extends Tool
|
||||
{
|
||||
protected string $name = 'get_application';
|
||||
|
||||
protected string $description = 'Get full details for a single application by UUID.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$uuid = $request->get('uuid');
|
||||
if (! is_string($uuid) || $uuid === '') {
|
||||
return Response::error('uuid argument is required.');
|
||||
return $this->mcpError($request, 'uuid argument is required.');
|
||||
}
|
||||
|
||||
$application = Application::ownedByCurrentTeamAPI($teamId)->where('uuid', $uuid)->first();
|
||||
if (! $application) {
|
||||
return Response::error("Application [{$uuid}] not found.");
|
||||
return $this->mcpError($request, "Application [{$uuid}] not found.", ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
// Drop relations that the server_status accessor lazy-loads — they
|
||||
|
|
@ -45,10 +45,10 @@ public function handle(Request $request): Response
|
|||
$application->setRelations([]);
|
||||
$application->makeHidden(['destination', 'source', 'additional_servers', 'environment', 'tags', 'environmentVariables']);
|
||||
|
||||
return $this->respond(
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$this->scrubSensitive($application->toArray()),
|
||||
$this->actionsForApplication($uuid, $application->status),
|
||||
);
|
||||
), ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -7,46 +7,46 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_database')]
|
||||
#[Description('Get full details for a standalone database by UUID. Detects type across postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse.')]
|
||||
class GetDatabase extends Tool
|
||||
{
|
||||
protected string $name = 'get_database';
|
||||
|
||||
protected string $description = 'Get full details for a standalone database by UUID. Detects type across postgresql, mysql, mariadb, mongodb, redis, keydb, dragonfly, clickhouse.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$uuid = $request->get('uuid');
|
||||
if (! is_string($uuid) || $uuid === '') {
|
||||
return Response::error('uuid argument is required.');
|
||||
return $this->mcpError($request, 'uuid argument is required.');
|
||||
}
|
||||
|
||||
$database = queryDatabaseByUuidWithinTeam($uuid, (string) $teamId);
|
||||
if (! $database) {
|
||||
return Response::error("Database [{$uuid}] not found.");
|
||||
return $this->mcpError($request, "Database [{$uuid}] not found.", ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
// Drop relations so deep server/destination data doesn't leak.
|
||||
$database->setRelations([]);
|
||||
$database->makeHidden(['destination', 'source', 'environment', 'environment_variables', 'environment_variables_preview']);
|
||||
|
||||
return $this->respond(
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$this->scrubSensitive($database->toArray()),
|
||||
$this->actionsForDatabase($uuid, $database->status ?? null),
|
||||
);
|
||||
), ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -9,26 +9,26 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_infrastructure_overview')]
|
||||
#[Description('High-level overview of the authenticated team: Coolify version, all servers, projects with resource counts, and aggregate counts. Start here to understand the setup.')]
|
||||
class GetInfrastructureOverview extends Tool
|
||||
{
|
||||
protected string $name = 'get_infrastructure_overview';
|
||||
|
||||
protected string $description = 'High-level overview of the authenticated team: Coolify version, all servers, projects with resource counts, and aggregate counts. Start here to understand the setup.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$servers = Server::whereTeamId($teamId)
|
||||
|
|
@ -72,7 +72,7 @@ public function handle(Request $request): Response
|
|||
];
|
||||
}
|
||||
|
||||
return $this->respond([
|
||||
return $this->mcpSuccess($request, $this->respond([
|
||||
'coolify_version' => config('constants.coolify.version'),
|
||||
'servers' => $servers,
|
||||
'projects' => $projectSummaries,
|
||||
|
|
@ -83,7 +83,7 @@ public function handle(Request $request): Response
|
|||
'services' => $serviceCount,
|
||||
'databases' => $databaseCount,
|
||||
],
|
||||
]);
|
||||
]));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -8,36 +8,36 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_server')]
|
||||
#[Description('Get full details for a single server by UUID.')]
|
||||
class GetServer extends Tool
|
||||
{
|
||||
protected string $name = 'get_server';
|
||||
|
||||
protected string $description = 'Get full details for a single server by UUID.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$uuid = $request->get('uuid');
|
||||
if (! is_string($uuid) || $uuid === '') {
|
||||
return Response::error('uuid argument is required.');
|
||||
return $this->mcpError($request, 'uuid argument is required.');
|
||||
}
|
||||
|
||||
$server = Server::whereTeamId($teamId)->where('uuid', $uuid)->with('settings')->first();
|
||||
if (! $server) {
|
||||
return Response::error("Server [{$uuid}] not found.");
|
||||
return $this->mcpError($request, "Server [{$uuid}] not found.", ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
$data = $this->scrubSensitive($server->toArray());
|
||||
|
|
@ -45,7 +45,7 @@ public function handle(Request $request): Response
|
|||
$data['is_usable'] = $server->settings?->is_usable;
|
||||
$data['connection_timeout'] = $server->settings?->connection_timeout;
|
||||
|
||||
return $this->respond($data, $this->actionsForServer($uuid));
|
||||
return $this->mcpSuccess($request, $this->respond($data, $this->actionsForServer($uuid)), ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -8,31 +8,31 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('get_service')]
|
||||
#[Description('Get full details for a single service (multi-container stack) by UUID.')]
|
||||
class GetService extends Tool
|
||||
{
|
||||
protected string $name = 'get_service';
|
||||
|
||||
protected string $description = 'Get full details for a single service (multi-container stack) by UUID.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$uuid = $request->get('uuid');
|
||||
if (! is_string($uuid) || $uuid === '') {
|
||||
return Response::error('uuid argument is required.');
|
||||
return $this->mcpError($request, 'uuid argument is required.');
|
||||
}
|
||||
|
||||
$service = Service::whereRelation('environment.project.team', 'id', $teamId)
|
||||
|
|
@ -40,16 +40,16 @@ public function handle(Request $request): Response
|
|||
->first();
|
||||
|
||||
if (! $service) {
|
||||
return Response::error("Service [{$uuid}] not found.");
|
||||
return $this->mcpError($request, "Service [{$uuid}] not found.", ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
$service->setRelations([]);
|
||||
$service->makeHidden(['destination', 'source', 'environment', 'applications', 'databases', 'serviceApplications', 'serviceDatabases']);
|
||||
|
||||
return $this->respond(
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$this->scrubSensitive($service->toArray()),
|
||||
$this->actionsForService($uuid, $service->status ?? null),
|
||||
);
|
||||
), ['resource_uuid' => $uuid]);
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -8,31 +8,31 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_applications')]
|
||||
#[Description('List applications owned by the authenticated team. Returns summary (uuid, name, status, fqdn, git_repository). Optional "tag" argument filters by tag name. Use get_application for full details.')]
|
||||
class ListApplications extends Tool
|
||||
{
|
||||
protected string $name = 'list_applications';
|
||||
|
||||
protected string $description = 'List applications owned by the authenticated team. Returns summary (uuid, name, status, fqdn, git_repository). Optional "tag" argument filters by tag name. Use get_application for full details.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$tagName = $request->get('tag');
|
||||
if ($tagName !== null && (! is_string($tagName) || trim($tagName) === '')) {
|
||||
return Response::error('tag argument must be a non-empty string.');
|
||||
return $this->mcpError($request, 'tag argument must be a non-empty string.');
|
||||
}
|
||||
$args = $this->paginationArgs($request);
|
||||
|
||||
|
|
@ -59,11 +59,11 @@ public function handle(Request $request): Response
|
|||
|
||||
$extra = $tagName ? ['tag' => $tagName] : [];
|
||||
|
||||
return $this->respond(
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$summaries,
|
||||
[],
|
||||
$this->paginationMeta('list_applications', $args, $total, $extra),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -8,26 +8,26 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_databases')]
|
||||
#[Description('List standalone databases owned by the authenticated team. Returns summary (uuid, name, status, type). Use get_database for full details.')]
|
||||
class ListDatabases extends Tool
|
||||
{
|
||||
protected string $name = 'list_databases';
|
||||
|
||||
protected string $description = 'List standalone databases owned by the authenticated team. Returns summary (uuid, name, status, type). Use get_database for full details.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$args = $this->paginationArgs($request);
|
||||
|
|
@ -52,11 +52,11 @@ public function handle(Request $request): Response
|
|||
->values()
|
||||
->all();
|
||||
|
||||
return $this->respond(
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$summaries,
|
||||
[],
|
||||
$this->paginationMeta('list_databases', $args, $total),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -8,26 +8,26 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_projects')]
|
||||
#[Description('List projects owned by the authenticated team. Returns summary (uuid, name, description).')]
|
||||
class ListProjects extends Tool
|
||||
{
|
||||
protected string $name = 'list_projects';
|
||||
|
||||
protected string $description = 'List projects owned by the authenticated team. Returns summary (uuid, name, description).';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$args = $this->paginationArgs($request);
|
||||
|
|
@ -49,11 +49,11 @@ public function handle(Request $request): Response
|
|||
->values()
|
||||
->all();
|
||||
|
||||
return $this->respond(
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$summaries,
|
||||
[],
|
||||
$this->paginationMeta('list_projects', $args, $total),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -8,26 +8,26 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_servers')]
|
||||
#[Description('List servers visible to the authenticated team token. Returns summary (uuid, name, ip, reachability). Use get_server for full details.')]
|
||||
class ListServers extends Tool
|
||||
{
|
||||
protected string $name = 'list_servers';
|
||||
|
||||
protected string $description = 'List servers visible to the authenticated team token. Returns summary (uuid, name, ip, reachability). Use get_server for full details.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$args = $this->paginationArgs($request);
|
||||
|
|
@ -50,11 +50,11 @@ public function handle(Request $request): Response
|
|||
->values()
|
||||
->all();
|
||||
|
||||
return $this->respond(
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$summaries,
|
||||
[],
|
||||
$this->paginationMeta('list_servers', $args, $total),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -8,26 +8,26 @@
|
|||
use Illuminate\Contracts\JsonSchema\JsonSchema;
|
||||
use Laravel\Mcp\Request;
|
||||
use Laravel\Mcp\Response;
|
||||
use Laravel\Mcp\Server\Attributes\Description;
|
||||
use Laravel\Mcp\Server\Attributes\Name;
|
||||
use Laravel\Mcp\Server\Tool;
|
||||
|
||||
#[Name('list_services')]
|
||||
#[Description('List services (multi-container stacks) owned by the authenticated team. Returns summary (uuid, name, status). Use get_service for full details.')]
|
||||
class ListServices extends Tool
|
||||
{
|
||||
protected string $name = 'list_services';
|
||||
|
||||
protected string $description = 'List services (multi-container stacks) owned by the authenticated team. Returns summary (uuid, name, status). Use get_service for full details.';
|
||||
|
||||
use BuildsResponse;
|
||||
use ResolvesTeam;
|
||||
|
||||
public function handle(Request $request): Response
|
||||
{
|
||||
if ($error = $this->ensureAbility($request, 'read')) {
|
||||
if ($error = $this->ensureAbility($request, 'read', $this->name)) {
|
||||
return $error;
|
||||
}
|
||||
|
||||
$teamId = $this->resolveTeamId($request);
|
||||
if (is_null($teamId)) {
|
||||
return Response::error('Invalid token.');
|
||||
return $this->mcpError($request, 'Invalid token.');
|
||||
}
|
||||
|
||||
$args = $this->paginationArgs($request);
|
||||
|
|
@ -49,11 +49,11 @@ public function handle(Request $request): Response
|
|||
->values()
|
||||
->all();
|
||||
|
||||
return $this->respond(
|
||||
return $this->mcpSuccess($request, $this->respond(
|
||||
$summaries,
|
||||
[],
|
||||
$this->paginationMeta('list_services', $args, $total),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
public function schema(JsonSchema $schema): array
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Application model',
|
||||
|
|
@ -1925,7 +1924,7 @@ public function loadComposeFile($isInit = false, ?string $restoreBaseDirectory =
|
|||
if ($isInit && $this->docker_compose_raw) {
|
||||
return;
|
||||
}
|
||||
$uuid = new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
['commands' => $cloneCommand] = $this->generateGitImportCommands(deployment_uuid: $uuid, only_checkout: true, exec_in_docker: false, custom_base_dir: 'checkout');
|
||||
$cloneCommand = str_replace(' clone ', ' clone --quiet ', $cloneCommand);
|
||||
$workdir = rtrim($this->base_directory, '/');
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class ApplicationPreview extends BaseModel
|
||||
{
|
||||
|
|
@ -111,7 +110,7 @@ public function generate_preview_fqdn()
|
|||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$urlPath = $url->getPath();
|
||||
$path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : '';
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
|
|
@ -173,7 +172,7 @@ public function generate_preview_fqdn_compose()
|
|||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$urlPath = $url->getPath();
|
||||
$path = ($urlPath !== '' && $urlPath !== '/') ? $urlPath : '';
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $this->pull_request_id, $preview_fqdn);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
abstract class BaseModel extends Model
|
||||
{
|
||||
|
|
@ -15,7 +14,7 @@ protected static function boot()
|
|||
static::creating(function (Model $model) {
|
||||
// Generate a UUID if one isn't set
|
||||
if (! $model->uuid) {
|
||||
$model->uuid = (string) new Cuid2;
|
||||
$model->uuid = new_public_id();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,12 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
|
||||
class CloudProviderToken extends BaseModel
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'team_id',
|
||||
'provider',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use DanHarrin\LivewireRateLimiting\WithRateLimiting;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
|
@ -30,7 +31,7 @@
|
|||
)]
|
||||
class PrivateKey extends BaseModel
|
||||
{
|
||||
use HasSafeStringAttribute, WithRateLimiting;
|
||||
use HasFactory, HasSafeStringAttribute, WithRateLimiting;
|
||||
|
||||
protected $fillable = [
|
||||
'name',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Project model',
|
||||
|
|
@ -59,7 +58,7 @@ protected static function booted()
|
|||
Environment::create([
|
||||
'name' => 'production',
|
||||
'project_id' => $project->id,
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
]);
|
||||
});
|
||||
static::deleting(function ($project) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,6 @@
|
|||
use Spatie\Url\Url;
|
||||
use Stevebauman\Purify\Facades\Purify;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
/**
|
||||
* @property array{
|
||||
|
|
@ -1041,7 +1040,7 @@ public function defaultStandaloneDockerAttributes(?int $id = null): array
|
|||
{
|
||||
$attributes = [
|
||||
'name' => 'coolify',
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
'network' => 'coolify',
|
||||
'server_id' => $this->id,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@
|
|||
use Spatie\Activitylog\Models\Activity;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
#[OA\Schema(
|
||||
description: 'Service model',
|
||||
|
|
@ -71,7 +70,7 @@ protected static function booted()
|
|||
{
|
||||
static::creating(function ($service) {
|
||||
if (blank($service->name)) {
|
||||
$service->name = 'service-'.(new Cuid2);
|
||||
$service->name = 'service-'.new_public_id();
|
||||
}
|
||||
});
|
||||
static::created(function ($service) {
|
||||
|
|
@ -1555,7 +1554,7 @@ public function saveComposeConfigs()
|
|||
"cd $workdir",
|
||||
], $this->server);
|
||||
|
||||
$filename = new Cuid2.'-docker-compose.yml';
|
||||
$filename = new_public_id().'-docker-compose.yml';
|
||||
Storage::disk('local')->put("tmp/{$filename}", $this->docker_compose);
|
||||
$path = Storage::path("tmp/{$filename}");
|
||||
instant_scp($path, "{$workdir}/docker-compose.yml", $this->server);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,65 @@
|
|||
namespace App\Providers;
|
||||
|
||||
// use Illuminate\Support\Facades\Gate;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\CloudInitScript;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\DiscordNotificationSettings;
|
||||
use App\Models\EmailNotificationSettings;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\PushoverNotificationSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceApplication;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\SharedEnvironmentVariable;
|
||||
use App\Models\SlackNotificationSettings;
|
||||
use App\Models\StandaloneClickhouse;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandaloneDragonfly;
|
||||
use App\Models\StandaloneKeydb;
|
||||
use App\Models\StandaloneMariadb;
|
||||
use App\Models\StandaloneMongodb;
|
||||
use App\Models\StandaloneMysql;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\StandaloneRedis;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\TelegramNotificationSettings;
|
||||
use App\Models\WebhookNotificationSettings;
|
||||
use App\Policies\ApiTokenPolicy;
|
||||
use App\Policies\ApplicationPolicy;
|
||||
use App\Policies\ApplicationPreviewPolicy;
|
||||
use App\Policies\ApplicationSettingPolicy;
|
||||
use App\Policies\CloudInitScriptPolicy;
|
||||
use App\Policies\CloudProviderTokenPolicy;
|
||||
use App\Policies\DatabasePolicy;
|
||||
use App\Policies\EnvironmentPolicy;
|
||||
use App\Policies\EnvironmentVariablePolicy;
|
||||
use App\Policies\GithubAppPolicy;
|
||||
use App\Policies\InstanceSettingsPolicy;
|
||||
use App\Policies\NotificationPolicy;
|
||||
use App\Policies\PrivateKeyPolicy;
|
||||
use App\Policies\ProjectPolicy;
|
||||
use App\Policies\ResourceCreatePolicy;
|
||||
use App\Policies\ServerPolicy;
|
||||
use App\Policies\ServiceApplicationPolicy;
|
||||
use App\Policies\ServiceDatabasePolicy;
|
||||
use App\Policies\ServicePolicy;
|
||||
use App\Policies\SharedEnvironmentVariablePolicy;
|
||||
use App\Policies\StandaloneDockerPolicy;
|
||||
use App\Policies\SwarmDockerPolicy;
|
||||
use App\Policies\TeamPolicy;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Sanctum\PersonalAccessToken;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
|
@ -15,49 +71,51 @@ class AuthServiceProvider extends ServiceProvider
|
|||
* @var array<class-string, class-string>
|
||||
*/
|
||||
protected $policies = [
|
||||
\App\Models\Server::class => \App\Policies\ServerPolicy::class,
|
||||
\App\Models\PrivateKey::class => \App\Policies\PrivateKeyPolicy::class,
|
||||
\App\Models\StandaloneDocker::class => \App\Policies\StandaloneDockerPolicy::class,
|
||||
\App\Models\SwarmDocker::class => \App\Policies\SwarmDockerPolicy::class,
|
||||
\App\Models\Application::class => \App\Policies\ApplicationPolicy::class,
|
||||
\App\Models\ApplicationPreview::class => \App\Policies\ApplicationPreviewPolicy::class,
|
||||
\App\Models\ApplicationSetting::class => \App\Policies\ApplicationSettingPolicy::class,
|
||||
\App\Models\Service::class => \App\Policies\ServicePolicy::class,
|
||||
\App\Models\ServiceApplication::class => \App\Policies\ServiceApplicationPolicy::class,
|
||||
\App\Models\ServiceDatabase::class => \App\Policies\ServiceDatabasePolicy::class,
|
||||
\App\Models\Project::class => \App\Policies\ProjectPolicy::class,
|
||||
\App\Models\Environment::class => \App\Policies\EnvironmentPolicy::class,
|
||||
\App\Models\EnvironmentVariable::class => \App\Policies\EnvironmentVariablePolicy::class,
|
||||
\App\Models\SharedEnvironmentVariable::class => \App\Policies\SharedEnvironmentVariablePolicy::class,
|
||||
Server::class => ServerPolicy::class,
|
||||
PrivateKey::class => PrivateKeyPolicy::class,
|
||||
StandaloneDocker::class => StandaloneDockerPolicy::class,
|
||||
SwarmDocker::class => SwarmDockerPolicy::class,
|
||||
Application::class => ApplicationPolicy::class,
|
||||
ApplicationPreview::class => ApplicationPreviewPolicy::class,
|
||||
ApplicationSetting::class => ApplicationSettingPolicy::class,
|
||||
Service::class => ServicePolicy::class,
|
||||
ServiceApplication::class => ServiceApplicationPolicy::class,
|
||||
ServiceDatabase::class => ServiceDatabasePolicy::class,
|
||||
Project::class => ProjectPolicy::class,
|
||||
Environment::class => EnvironmentPolicy::class,
|
||||
EnvironmentVariable::class => EnvironmentVariablePolicy::class,
|
||||
SharedEnvironmentVariable::class => SharedEnvironmentVariablePolicy::class,
|
||||
// Database policies - all use the shared DatabasePolicy
|
||||
\App\Models\StandalonePostgresql::class => \App\Policies\DatabasePolicy::class,
|
||||
\App\Models\StandaloneMysql::class => \App\Policies\DatabasePolicy::class,
|
||||
\App\Models\StandaloneMariadb::class => \App\Policies\DatabasePolicy::class,
|
||||
\App\Models\StandaloneMongodb::class => \App\Policies\DatabasePolicy::class,
|
||||
\App\Models\StandaloneRedis::class => \App\Policies\DatabasePolicy::class,
|
||||
\App\Models\StandaloneKeydb::class => \App\Policies\DatabasePolicy::class,
|
||||
\App\Models\StandaloneDragonfly::class => \App\Policies\DatabasePolicy::class,
|
||||
\App\Models\StandaloneClickhouse::class => \App\Policies\DatabasePolicy::class,
|
||||
StandalonePostgresql::class => DatabasePolicy::class,
|
||||
StandaloneMysql::class => DatabasePolicy::class,
|
||||
StandaloneMariadb::class => DatabasePolicy::class,
|
||||
StandaloneMongodb::class => DatabasePolicy::class,
|
||||
StandaloneRedis::class => DatabasePolicy::class,
|
||||
StandaloneKeydb::class => DatabasePolicy::class,
|
||||
StandaloneDragonfly::class => DatabasePolicy::class,
|
||||
StandaloneClickhouse::class => DatabasePolicy::class,
|
||||
|
||||
// Notification policies - all use the shared NotificationPolicy
|
||||
\App\Models\EmailNotificationSettings::class => \App\Policies\NotificationPolicy::class,
|
||||
\App\Models\DiscordNotificationSettings::class => \App\Policies\NotificationPolicy::class,
|
||||
\App\Models\TelegramNotificationSettings::class => \App\Policies\NotificationPolicy::class,
|
||||
\App\Models\SlackNotificationSettings::class => \App\Policies\NotificationPolicy::class,
|
||||
\App\Models\PushoverNotificationSettings::class => \App\Policies\NotificationPolicy::class,
|
||||
\App\Models\WebhookNotificationSettings::class => \App\Policies\NotificationPolicy::class,
|
||||
EmailNotificationSettings::class => NotificationPolicy::class,
|
||||
DiscordNotificationSettings::class => NotificationPolicy::class,
|
||||
TelegramNotificationSettings::class => NotificationPolicy::class,
|
||||
SlackNotificationSettings::class => NotificationPolicy::class,
|
||||
PushoverNotificationSettings::class => NotificationPolicy::class,
|
||||
WebhookNotificationSettings::class => NotificationPolicy::class,
|
||||
|
||||
// API Token policy
|
||||
\Laravel\Sanctum\PersonalAccessToken::class => \App\Policies\ApiTokenPolicy::class,
|
||||
PersonalAccessToken::class => ApiTokenPolicy::class,
|
||||
|
||||
// Instance settings policy
|
||||
\App\Models\InstanceSettings::class => \App\Policies\InstanceSettingsPolicy::class,
|
||||
InstanceSettings::class => InstanceSettingsPolicy::class,
|
||||
|
||||
// Team policy
|
||||
\App\Models\Team::class => \App\Policies\TeamPolicy::class,
|
||||
Team::class => TeamPolicy::class,
|
||||
|
||||
// Git source policies
|
||||
\App\Models\GithubApp::class => \App\Policies\GithubAppPolicy::class,
|
||||
GithubApp::class => GithubAppPolicy::class,
|
||||
CloudProviderToken::class => CloudProviderTokenPolicy::class,
|
||||
CloudInitScript::class => CloudInitScriptPolicy::class,
|
||||
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Checkbox extends Component
|
||||
{
|
||||
|
|
@ -58,7 +57,7 @@ public function render(): View|Closure|string
|
|||
// Generate unique HTML ID by adding random suffix
|
||||
// This prevents duplicate IDs when multiple forms are on the same page
|
||||
if ($this->id) {
|
||||
$uniqueSuffix = new Cuid2;
|
||||
$uniqueSuffix = new_public_id();
|
||||
$this->htmlId = $this->id.'-'.$uniqueSuffix;
|
||||
} else {
|
||||
$this->htmlId = $this->id;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Datalist extends Component
|
||||
{
|
||||
|
|
@ -55,7 +54,7 @@ public function render(): View|Closure|string
|
|||
$this->modelBinding = $this->id;
|
||||
|
||||
if (is_null($this->id)) {
|
||||
$this->id = new Cuid2;
|
||||
$this->id = new_public_id();
|
||||
// Don't create wire:model binding for auto-generated IDs
|
||||
$this->modelBinding = 'null';
|
||||
}
|
||||
|
|
@ -64,7 +63,7 @@ public function render(): View|Closure|string
|
|||
// This prevents duplicate IDs when multiple forms are on the same page
|
||||
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||
// Use original ID with random suffix for uniqueness
|
||||
$uniqueSuffix = new Cuid2;
|
||||
$uniqueSuffix = new_public_id();
|
||||
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||
} else {
|
||||
$this->htmlId = (string) $this->id;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class EnvVarInput extends Component
|
||||
{
|
||||
|
|
@ -56,7 +55,7 @@ public function render(): View|Closure|string
|
|||
$this->modelBinding = $this->id;
|
||||
|
||||
if (is_null($this->id)) {
|
||||
$this->id = new Cuid2;
|
||||
$this->id = new_public_id();
|
||||
// Don't create wire:model binding for auto-generated IDs
|
||||
$this->modelBinding = 'null';
|
||||
}
|
||||
|
|
@ -64,7 +63,7 @@ public function render(): View|Closure|string
|
|||
// This prevents duplicate IDs when multiple forms are on the same page
|
||||
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||
// Use original ID with random suffix for uniqueness
|
||||
$uniqueSuffix = new Cuid2;
|
||||
$uniqueSuffix = new_public_id();
|
||||
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||
} else {
|
||||
$this->htmlId = (string) $this->id;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Input extends Component
|
||||
{
|
||||
|
|
@ -51,7 +51,7 @@ public function render(): View|Closure|string
|
|||
$this->modelBinding = $this->id;
|
||||
|
||||
if (is_null($this->id)) {
|
||||
$this->id = new Cuid2;
|
||||
$this->id = new_public_id();
|
||||
// Don't create wire:model binding for auto-generated IDs
|
||||
$this->modelBinding = 'null';
|
||||
}
|
||||
|
|
@ -59,7 +59,7 @@ public function render(): View|Closure|string
|
|||
// This prevents duplicate IDs when multiple forms are on the same page
|
||||
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||
// Use original ID with random suffix for uniqueness
|
||||
$uniqueSuffix = new Cuid2;
|
||||
$uniqueSuffix = new_public_id();
|
||||
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||
} else {
|
||||
$this->htmlId = (string) $this->id;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@
|
|||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Select extends Component
|
||||
{
|
||||
|
|
@ -48,7 +47,7 @@ public function render(): View|Closure|string
|
|||
$this->modelBinding = $this->id;
|
||||
|
||||
if (is_null($this->id)) {
|
||||
$this->id = new Cuid2;
|
||||
$this->id = new_public_id();
|
||||
// Don't create wire:model binding for auto-generated IDs
|
||||
$this->modelBinding = 'null';
|
||||
}
|
||||
|
|
@ -57,7 +56,7 @@ public function render(): View|Closure|string
|
|||
// This prevents duplicate IDs when multiple forms are on the same page
|
||||
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||
// Use original ID with random suffix for uniqueness
|
||||
$uniqueSuffix = new Cuid2;
|
||||
$uniqueSuffix = new_public_id();
|
||||
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||
} else {
|
||||
$this->htmlId = (string) $this->id;
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
use Closure;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\View\Component;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class Textarea extends Component
|
||||
{
|
||||
|
|
@ -63,7 +63,7 @@ public function render(): View|Closure|string
|
|||
$this->modelBinding = $this->id;
|
||||
|
||||
if (is_null($this->id)) {
|
||||
$this->id = new Cuid2;
|
||||
$this->id = new_public_id();
|
||||
// Don't create wire:model binding for auto-generated IDs
|
||||
$this->modelBinding = 'null';
|
||||
}
|
||||
|
|
@ -72,7 +72,7 @@ public function render(): View|Closure|string
|
|||
// This prevents duplicate IDs when multiple forms are on the same page
|
||||
if ($this->modelBinding && $this->modelBinding !== 'null') {
|
||||
// Use original ID with random suffix for uniqueness
|
||||
$uniqueSuffix = new Cuid2;
|
||||
$uniqueSuffix = new_public_id();
|
||||
$this->htmlId = $this->modelBinding.'-'.$uniqueSuffix;
|
||||
} else {
|
||||
$this->htmlId = (string) $this->id;
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use Spatie\Url\Url;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
function queue_application_deployment(Application $application, string $deployment_uuid, ?int $pull_request_id = 0, ?string $commit = null, bool $force_rebuild = false, bool $is_webhook = false, bool $is_api = false, bool $restart_only = false, ?string $git_type = null, bool $no_questions_asked = false, ?Server $server = null, ?StandaloneDocker $destination = null, bool $only_this_server = false, bool $rollback = false, ?string $docker_registry_image_tag = null)
|
||||
{
|
||||
|
|
@ -192,7 +191,7 @@ function next_after_cancel(?Server $server = null)
|
|||
|
||||
function clone_application(Application $source, $destination, array $overrides = [], bool $cloneVolumeData = false): Application
|
||||
{
|
||||
$uuid = $overrides['uuid'] ?? (string) new Cuid2;
|
||||
$uuid = $overrides['uuid'] ?? new_public_id();
|
||||
$server = $destination->server;
|
||||
|
||||
if ($server->team_id !== currentTeam()->id) {
|
||||
|
|
@ -259,7 +258,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
'application_id' => $newApplication->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
|
@ -274,7 +273,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
'created_at',
|
||||
'updated_at',
|
||||
])->fill([
|
||||
'uuid' => (string) new Cuid2,
|
||||
'uuid' => new_public_id(),
|
||||
'application_id' => $newApplication->id,
|
||||
'status' => 'exited',
|
||||
'fqdn' => null,
|
||||
|
|
@ -322,7 +321,7 @@ function clone_application(Application $source, $destination, array $overrides =
|
|||
VolumeCloneJob::dispatch($sourceVolume, $targetVolume, $sourceServer, $targetServer, $newPersistentVolume);
|
||||
|
||||
queue_application_deployment(
|
||||
deployment_uuid: (string) new Cuid2,
|
||||
deployment_uuid: new_public_id(),
|
||||
application: $source,
|
||||
server: $sourceServer,
|
||||
destination: $source->destination,
|
||||
|
|
|
|||
|
|
@ -17,12 +17,11 @@
|
|||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
function create_standalone_postgresql($environmentId, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null, string $databaseImage = 'postgres:16-alpine'): StandalonePostgresql
|
||||
{
|
||||
$database = new StandalonePostgresql;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->uuid = new_public_id();
|
||||
$database->name = 'postgresql-database-'.$database->uuid;
|
||||
$database->image = $databaseImage;
|
||||
$database->postgres_password = Str::password(length: 64, symbols: false);
|
||||
|
|
@ -40,7 +39,7 @@ function create_standalone_postgresql($environmentId, StandaloneDocker|SwarmDock
|
|||
function create_standalone_redis($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneRedis
|
||||
{
|
||||
$database = new StandaloneRedis;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->uuid = new_public_id();
|
||||
$database->name = 'redis-database-'.$database->uuid;
|
||||
|
||||
$redis_password = Str::password(length: 64, symbols: false);
|
||||
|
|
@ -79,7 +78,7 @@ function create_standalone_redis($environment_id, StandaloneDocker|SwarmDocker $
|
|||
function create_standalone_mongodb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMongodb
|
||||
{
|
||||
$database = new StandaloneMongodb;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->uuid = new_public_id();
|
||||
$database->name = 'mongodb-database-'.$database->uuid;
|
||||
$database->mongo_initdb_root_password = Str::password(length: 64, symbols: false);
|
||||
$database->environment_id = $environment_id;
|
||||
|
|
@ -96,7 +95,7 @@ function create_standalone_mongodb($environment_id, StandaloneDocker|SwarmDocker
|
|||
function create_standalone_mysql($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMysql
|
||||
{
|
||||
$database = new StandaloneMysql;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->uuid = new_public_id();
|
||||
$database->name = 'mysql-database-'.$database->uuid;
|
||||
$database->mysql_root_password = Str::password(length: 64, symbols: false);
|
||||
$database->mysql_password = Str::password(length: 64, symbols: false);
|
||||
|
|
@ -114,7 +113,7 @@ function create_standalone_mysql($environment_id, StandaloneDocker|SwarmDocker $
|
|||
function create_standalone_mariadb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneMariadb
|
||||
{
|
||||
$database = new StandaloneMariadb;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->uuid = new_public_id();
|
||||
$database->name = 'mariadb-database-'.$database->uuid;
|
||||
$database->mariadb_root_password = Str::password(length: 64, symbols: false);
|
||||
$database->mariadb_password = Str::password(length: 64, symbols: false);
|
||||
|
|
@ -132,7 +131,7 @@ function create_standalone_mariadb($environment_id, StandaloneDocker|SwarmDocker
|
|||
function create_standalone_keydb($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneKeydb
|
||||
{
|
||||
$database = new StandaloneKeydb;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->uuid = new_public_id();
|
||||
$database->name = 'keydb-database-'.$database->uuid;
|
||||
$database->keydb_password = Str::password(length: 64, symbols: false);
|
||||
$database->environment_id = $environment_id;
|
||||
|
|
@ -149,7 +148,7 @@ function create_standalone_keydb($environment_id, StandaloneDocker|SwarmDocker $
|
|||
function create_standalone_dragonfly($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneDragonfly
|
||||
{
|
||||
$database = new StandaloneDragonfly;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->uuid = new_public_id();
|
||||
$database->name = 'dragonfly-database-'.$database->uuid;
|
||||
$database->dragonfly_password = Str::password(length: 64, symbols: false);
|
||||
$database->environment_id = $environment_id;
|
||||
|
|
@ -166,7 +165,7 @@ function create_standalone_dragonfly($environment_id, StandaloneDocker|SwarmDock
|
|||
function create_standalone_clickhouse($environment_id, StandaloneDocker|SwarmDocker $destination, ?array $otherData = null): StandaloneClickhouse
|
||||
{
|
||||
$database = new StandaloneClickhouse;
|
||||
$database->uuid = (new Cuid2);
|
||||
$database->uuid = new_public_id();
|
||||
$database->name = 'clickhouse-database-'.$database->uuid;
|
||||
$database->clickhouse_admin_password = Str::password(length: 64, symbols: false);
|
||||
$database->environment_id = $environment_id;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@
|
|||
use Illuminate\Support\Str;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pullRequestId = null, ?bool $includePullrequests = false): Collection
|
||||
{
|
||||
|
|
@ -459,7 +458,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_
|
|||
foreach ($domains as $loop => $domain) {
|
||||
try {
|
||||
if ($generate_unique_uuid) {
|
||||
$uuid = new Cuid2;
|
||||
$uuid = new_public_id();
|
||||
}
|
||||
|
||||
$url = Url::fromString($domain);
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
use Illuminate\Support\Str;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
/**
|
||||
* Validates a Docker Compose YAML string for command injection vulnerabilities.
|
||||
|
|
@ -1240,7 +1239,7 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||
$schema = $url->getScheme();
|
||||
$portInt = $url->getPort();
|
||||
$port = $portInt !== null ? ':'.$portInt : '';
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $pullRequestId, $preview_fqdn);
|
||||
|
|
|
|||
|
|
@ -64,7 +64,6 @@
|
|||
use PurplePixie\PhpDns\DNSTypes;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
function base_configuration_dir(): string
|
||||
{
|
||||
|
|
@ -115,6 +114,13 @@ function sanitize_string(?string $input = null): ?string
|
|||
return $sanitized;
|
||||
}
|
||||
|
||||
function new_public_id(int $length = 24): string
|
||||
{
|
||||
$length = max(1, $length);
|
||||
|
||||
return Str::lower(Str::random($length));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate that a path or identifier is safe for use in shell commands.
|
||||
*
|
||||
|
|
@ -455,7 +461,7 @@ function generate_random_name(?string $cuid = null): string
|
|||
]
|
||||
);
|
||||
if (is_null($cuid)) {
|
||||
$cuid = new Cuid2;
|
||||
$cuid = new_public_id();
|
||||
}
|
||||
|
||||
return Str::kebab("{$generator->getName()}-$cuid");
|
||||
|
|
@ -491,7 +497,7 @@ function formatPrivateKey(string $privateKey)
|
|||
function generate_application_name(string $git_repository, string $git_branch, ?string $cuid = null): string
|
||||
{
|
||||
if (is_null($cuid)) {
|
||||
$cuid = new Cuid2;
|
||||
$cuid = new_public_id();
|
||||
}
|
||||
|
||||
$repo_name = str_contains($git_repository, '/') ? last(explode('/', $git_repository)) : $git_repository;
|
||||
|
|
@ -3259,7 +3265,7 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal
|
|||
$template = $resource->preview_url_template;
|
||||
$host = $url->getHost();
|
||||
$schema = $url->getScheme();
|
||||
$random = new Cuid2;
|
||||
$random = new_public_id();
|
||||
$preview_fqdn = str_replace('{{random}}', $random, $template);
|
||||
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
|
||||
$preview_fqdn = str_replace('{{pr_id}}', $pull_request_id, $preview_fqdn);
|
||||
|
|
|
|||
25
database/factories/CloudProviderTokenFactory.php
Normal file
25
database/factories/CloudProviderTokenFactory.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<CloudProviderToken>
|
||||
*/
|
||||
class CloudProviderTokenFactory extends Factory
|
||||
{
|
||||
protected $model = CloudProviderToken::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'team_id' => Team::factory(),
|
||||
'provider' => 'hetzner',
|
||||
'token' => 'test-cloud-provider-token',
|
||||
'name' => fake()->words(3, true),
|
||||
];
|
||||
}
|
||||
}
|
||||
37
database/factories/PrivateKeyFactory.php
Normal file
37
database/factories/PrivateKeyFactory.php
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends Factory<PrivateKey>
|
||||
*/
|
||||
class PrivateKeyFactory extends Factory
|
||||
{
|
||||
protected $model = PrivateKey::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->words(2, true),
|
||||
'description' => fake()->sentence(),
|
||||
'private_key' => $this->privateKey(),
|
||||
'team_id' => Team::factory(),
|
||||
'is_git_related' => false,
|
||||
];
|
||||
}
|
||||
|
||||
private function privateKey(): string
|
||||
{
|
||||
return '-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
|
||||
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
|
||||
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
|
||||
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
-----END OPENSSH PRIVATE KEY-----';
|
||||
}
|
||||
}
|
||||
|
|
@ -142,7 +142,8 @@
|
|||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
@forelse($initScripts ?? [] as $script)
|
||||
<livewire:project.database.init-script :script="$script" :wire:key="$script['index']" />
|
||||
<livewire:project.database.init-script :database="$database" :script="$script"
|
||||
:wire:key="$script['index']" />
|
||||
@empty
|
||||
<div>No initialization scripts found.</div>
|
||||
@endforelse
|
||||
|
|
|
|||
|
|
@ -1,14 +1,23 @@
|
|||
<?php
|
||||
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Once;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::query()->whereKey(0)->delete();
|
||||
$settings = new InstanceSettings(['is_api_enabled' => true]);
|
||||
$settings->id = 0;
|
||||
$settings->save();
|
||||
Once::flush();
|
||||
|
||||
// Create a team with owner
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
|
|
@ -410,4 +419,33 @@
|
|||
$response->assertStatus(200);
|
||||
$response->assertJson(['valid' => true, 'message' => 'Token is valid.']);
|
||||
});
|
||||
|
||||
test('writes an audit log entry when validating a stored token', function () {
|
||||
$token = CloudProviderToken::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'provider' => 'hetzner',
|
||||
'name' => 'Audit Token',
|
||||
]);
|
||||
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/servers' => Http::response([], 200),
|
||||
]);
|
||||
|
||||
$auditChannel = Mockery::mock();
|
||||
$auditChannel->shouldReceive('info')
|
||||
->once()
|
||||
->with('api.cloud_token.validated', Mockery::on(function (array $context) use ($token) {
|
||||
return $context['cloud_token_uuid'] === $token->uuid
|
||||
&& $context['provider'] === 'hetzner'
|
||||
&& $context['valid'] === true;
|
||||
}));
|
||||
|
||||
Log::shouldReceive('channel')->with('audit')->andReturn($auditChannel);
|
||||
|
||||
$this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$this->bearerToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson("/api/v1/cloud-tokens/{$token->uuid}/validate")
|
||||
->assertOk();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,15 +1,23 @@
|
|||
<?php
|
||||
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Once;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::query()->whereKey(0)->delete();
|
||||
$settings = new InstanceSettings(['is_api_enabled' => true]);
|
||||
$settings->id = 0;
|
||||
$settings->save();
|
||||
Once::flush();
|
||||
|
||||
// Create a team with owner
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
|
|
@ -73,6 +81,27 @@
|
|||
|
||||
$response->assertStatus(404);
|
||||
});
|
||||
|
||||
test('member read token cannot use a stored cloud provider token', function () {
|
||||
$member = User::factory()->create();
|
||||
$this->team->members()->attach($member->id, ['role' => 'member']);
|
||||
session(['currentTeam' => $this->team]);
|
||||
$memberToken = $member->createToken('member-read', ['read'])->plainTextToken;
|
||||
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/locations*' => Http::response([
|
||||
'locations' => [['id' => 1, 'name' => 'nbg1']],
|
||||
], 200),
|
||||
]);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$memberToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->getJson('/api/v1/hetzner/locations?cloud_provider_token_id='.$this->hetznerToken->uuid);
|
||||
|
||||
$response->assertForbidden();
|
||||
Http::assertNothingSent();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/hetzner/server-types', function () {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,11 @@
|
|||
use App\Models\PrivateKey;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Policies\CloudInitScriptPolicy;
|
||||
use App\Policies\CloudProviderTokenPolicy;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
|
@ -131,6 +134,11 @@
|
|||
expect(auth()->user()->can('viewAny', CloudInitScript::class))->toBeTrue();
|
||||
});
|
||||
|
||||
test('cloud provider and cloud init policies are explicitly registered', function () {
|
||||
expect(Gate::getPolicyFor(CloudProviderToken::class))->toBeInstanceOf(CloudProviderTokenPolicy::class)
|
||||
->and(Gate::getPolicyFor(CloudInitScript::class))->toBeInstanceOf(CloudInitScriptPolicy::class);
|
||||
});
|
||||
|
||||
// --- Personal Access Token (API Token) Policy ---
|
||||
|
||||
test('any user can create personal access token', function () {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Once;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
|
@ -64,6 +65,23 @@ function mcpToolJson($response): array
|
|||
return json_decode($response->json('result.content.0.text'), true);
|
||||
}
|
||||
|
||||
function expectMcpAuditLog(array $expected): void
|
||||
{
|
||||
$auditChannel = Mockery::mock();
|
||||
|
||||
Log::shouldReceive('channel')
|
||||
->with('audit')
|
||||
->once()
|
||||
->andReturn($auditChannel);
|
||||
|
||||
$auditChannel
|
||||
->shouldReceive('info')
|
||||
->once()
|
||||
->with('mcp.tool.called', Mockery::on(fn (array $context) => collect($expected)->every(
|
||||
fn ($value, $key) => data_get($context, $key) === $value,
|
||||
)));
|
||||
}
|
||||
|
||||
test('MCP endpoint returns 404 when the instance setting is disabled', function () {
|
||||
InstanceSettings::query()->where('id', 0)->update(['is_mcp_server_enabled' => false]);
|
||||
Once::flush();
|
||||
|
|
@ -193,6 +211,48 @@ function mcpToolJson($response): array
|
|||
expect($response->json('result.content.0.text'))->toContain('Missing required permissions');
|
||||
});
|
||||
|
||||
test('MCP tools audit successful execution with the actual tool name', function () {
|
||||
Project::create(['name' => 'Mine', 'team_id' => $this->team->id]);
|
||||
$token = $this->user->createToken('mcp-read', ['read'])->plainTextToken;
|
||||
|
||||
expectMcpAuditLog([
|
||||
'tool' => 'list_projects',
|
||||
'team_id' => $this->team->id,
|
||||
'outcome' => 'success',
|
||||
]);
|
||||
|
||||
mcpCallTool($token, 'list_projects')->assertOk();
|
||||
});
|
||||
|
||||
test('MCP tools audit denied execution after ability checks', function () {
|
||||
$token = $this->user->createToken('mcp-no-abilities', [])->plainTextToken;
|
||||
|
||||
expectMcpAuditLog([
|
||||
'tool' => 'list_projects',
|
||||
'team_id' => $this->team->id,
|
||||
'outcome' => 'denied',
|
||||
]);
|
||||
|
||||
$response = mcpCallTool($token, 'list_projects');
|
||||
$response->assertOk();
|
||||
expect($response->json('result.isError'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('MCP tools audit execution errors after tool handling', function () {
|
||||
$token = $this->user->createToken('mcp-read', ['read'])->plainTextToken;
|
||||
|
||||
expectMcpAuditLog([
|
||||
'tool' => 'get_server',
|
||||
'team_id' => $this->team->id,
|
||||
'outcome' => 'error',
|
||||
'resource_uuid' => 'missing-server',
|
||||
]);
|
||||
|
||||
$response = mcpCallTool($token, 'get_server', ['uuid' => 'missing-server']);
|
||||
$response->assertOk();
|
||||
expect($response->json('result.isError'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('MCP rejects token when user no longer belongs to token team', function () {
|
||||
Project::create(['name' => 'Hidden', 'team_id' => $this->team->id]);
|
||||
$token = $this->user->createToken('mcp-read', ['read'])->plainTextToken;
|
||||
|
|
|
|||
30
tests/Feature/MutableLivewireComponentsAuthorizationTest.php
Normal file
30
tests/Feature/MutableLivewireComponentsAuthorizationTest.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('keeps mutable Livewire components behind authorization checks', function (string $path, array $requiredNeedles) {
|
||||
$source = file_get_contents(base_path($path));
|
||||
|
||||
foreach ($requiredNeedles as $needle) {
|
||||
expect($source)->toContain($needle);
|
||||
}
|
||||
})->with([
|
||||
'storage resources' => [
|
||||
'app/Livewire/Storage/Resources.php',
|
||||
['AuthorizesRequests', "authorize('update'", "authorize('view'"],
|
||||
],
|
||||
'postgres init script editor' => [
|
||||
'app/Livewire/Project/Database/InitScript.php',
|
||||
['AuthorizesRequests', "authorize('update'"],
|
||||
],
|
||||
'execute container command' => [
|
||||
'app/Livewire/Project/Shared/ExecuteContainerCommand.php',
|
||||
['AuthorizesRequests', "authorize('view'", "authorize('canAccessTerminal'"],
|
||||
],
|
||||
'terminal' => [
|
||||
'app/Livewire/Project/Shared/Terminal.php',
|
||||
['AuthorizesRequests', "authorize('view'", "authorize('canAccessTerminal'"],
|
||||
],
|
||||
]);
|
||||
|
|
@ -1,19 +1,36 @@
|
|||
<?php
|
||||
|
||||
use App\Livewire\Security\CloudInitScriptForm;
|
||||
use App\Livewire\Security\CloudInitScripts;
|
||||
use App\Livewire\Security\CloudProviderTokenForm;
|
||||
use App\Livewire\Security\CloudProviderTokens;
|
||||
use App\Models\Application;
|
||||
use App\Models\CloudInitScript;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Once;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function makeAuditTeamUser(): array
|
||||
{
|
||||
if (! InstanceSettings::query()->whereKey(0)->exists()) {
|
||||
$settings = new InstanceSettings;
|
||||
$settings->id = 0;
|
||||
$settings->save();
|
||||
}
|
||||
Once::flush();
|
||||
|
||||
$team = Team::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$team->members()->attach($user->id, ['role' => 'owner']);
|
||||
|
|
@ -122,6 +139,89 @@ function makeAuditApplication(string $repo = 'test-org/test-repo'): Application
|
|||
});
|
||||
});
|
||||
|
||||
describe('security UI audit logging', function () {
|
||||
test('creating a cloud provider token from Livewire writes an audit entry', function () {
|
||||
[$team] = makeAuditTeamUser();
|
||||
|
||||
Http::fake([
|
||||
'https://api.hetzner.cloud/v1/servers' => Http::response([], 200),
|
||||
]);
|
||||
|
||||
$auditChannel = Mockery::mock();
|
||||
$auditChannel->shouldReceive('info')
|
||||
->once()
|
||||
->with('ui.cloud_token.created', Mockery::on(function (array $context) use ($team) {
|
||||
return $context['team_id'] === $team->id
|
||||
&& $context['provider'] === 'hetzner'
|
||||
&& $context['cloud_token_name'] === 'UI Token';
|
||||
}));
|
||||
|
||||
Log::shouldReceive('channel')->with('audit')->andReturn($auditChannel);
|
||||
|
||||
Livewire::test(CloudProviderTokenForm::class)
|
||||
->set('provider', 'hetzner')
|
||||
->set('token', 'secret-token')
|
||||
->set('name', 'UI Token')
|
||||
->call('addToken');
|
||||
});
|
||||
|
||||
test('deleting a cloud provider token from Livewire writes an audit entry', function () {
|
||||
[$team] = makeAuditTeamUser();
|
||||
$token = CloudProviderToken::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'provider' => 'hetzner',
|
||||
'name' => 'Delete Me',
|
||||
]);
|
||||
|
||||
$auditChannel = Mockery::mock();
|
||||
$auditChannel->shouldReceive('info')
|
||||
->once()
|
||||
->with('ui.cloud_token.deleted', Mockery::on(function (array $context) use ($token) {
|
||||
return $context['cloud_token_uuid'] === $token->uuid
|
||||
&& $context['cloud_token_name'] === 'Delete Me';
|
||||
}));
|
||||
|
||||
Log::shouldReceive('channel')->with('audit')->andReturn($auditChannel);
|
||||
|
||||
Livewire::test(CloudProviderTokens::class)
|
||||
->call('deleteToken', $token->id);
|
||||
});
|
||||
|
||||
test('creating and deleting cloud-init scripts from Livewire write audit entries', function () {
|
||||
[$team] = makeAuditTeamUser();
|
||||
|
||||
$auditChannel = Mockery::mock();
|
||||
$auditChannel->shouldReceive('info')
|
||||
->once()
|
||||
->with('ui.cloud_init_script.created', Mockery::on(function (array $context) use ($team) {
|
||||
return $context['team_id'] === $team->id
|
||||
&& $context['cloud_init_script_name'] === 'Bootstrap';
|
||||
}));
|
||||
$auditChannel->shouldReceive('info')
|
||||
->once()
|
||||
->with('ui.cloud_init_script.deleted', Mockery::on(function (array $context) {
|
||||
return $context['cloud_init_script_name'] === 'Bootstrap';
|
||||
}));
|
||||
|
||||
Log::shouldReceive('channel')->with('audit')->andReturn($auditChannel);
|
||||
|
||||
Livewire::test(CloudInitScriptForm::class)
|
||||
->set('name', 'Bootstrap')
|
||||
->set('script', "#cloud-config\npackages: []")
|
||||
->call('save');
|
||||
|
||||
$script = CloudInitScript::where('team_id', $team->id)->firstOrFail();
|
||||
|
||||
Livewire::test(CloudInitScripts::class)
|
||||
->call('deleteScript', $script->id);
|
||||
});
|
||||
|
||||
test('cloud provider token form does not contain debug ray calls', function () {
|
||||
expect(file_get_contents(app_path('Livewire/Security/CloudProviderTokenForm.php')))
|
||||
->not->toContain('ray(');
|
||||
});
|
||||
});
|
||||
|
||||
describe('webhook signature failure logging', function () {
|
||||
test('GitHub manual webhook with bad signature logs to audit channel', function () {
|
||||
$app = makeAuditApplication();
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
|
@ -81,6 +82,23 @@ function sentinelPayload(array $containers, ?float $diskPercentage = 42.0): arra
|
|||
Queue::assertPushed(PushServerUpdateJob::class, 1);
|
||||
});
|
||||
|
||||
it('only audits sentinel pushes that dispatch a state update', function () use ($running) {
|
||||
$auditChannel = Mockery::mock();
|
||||
$auditChannel->shouldReceive('info')
|
||||
->once()
|
||||
->with('sentinel.metrics_pushed', Mockery::on(function (array $context) {
|
||||
return $context['server_uuid'] === $this->server->uuid
|
||||
&& $context['team_id'] === $this->team->id;
|
||||
}));
|
||||
|
||||
Log::shouldReceive('channel')->with('audit')->andReturn($auditChannel);
|
||||
|
||||
pushSentinel($this->token, sentinelPayload($running()))->assertOk();
|
||||
pushSentinel($this->token, sentinelPayload($running()))->assertOk();
|
||||
|
||||
Queue::assertPushed(PushServerUpdateJob::class, 1);
|
||||
});
|
||||
|
||||
it('updates the heartbeat even when the job is skipped', function () use ($running) {
|
||||
pushSentinel($this->token, sentinelPayload($running()))->assertOk();
|
||||
|
||||
|
|
|
|||
19
tests/Unit/PublicIdTest.php
Normal file
19
tests/Unit/PublicIdTest.php
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
it('generates lowercase URL-safe public ids without Cuid2', function () {
|
||||
$ids = collect(range(1, 100))->map(fn () => new_public_id());
|
||||
|
||||
expect($ids)->each
|
||||
->toBeString()
|
||||
->toHaveLength(24)
|
||||
->toMatch('/^[a-z0-9]+$/');
|
||||
|
||||
expect($ids->unique())->toHaveCount(100);
|
||||
});
|
||||
|
||||
it('honors custom public id lengths', function () {
|
||||
expect(new_public_id(32))
|
||||
->toBeString()
|
||||
->toHaveLength(32)
|
||||
->toMatch('/^[a-z0-9]+$/');
|
||||
});
|
||||
Loading…
Reference in a new issue