mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-19 18:15:31 +00:00
feat: self-hosted GitLab Apps OAuth integration
Adds self-hosted GitLab OAuth sources so Coolify can connect to a self-managed GitLab instance, list private repositories, clone over an OAuth token, and deploy (the GitLab counterpart to GitHub Apps). Hardening: authenticated, one-time team-bound OAuth callback state; token redaction in deploy logs; custom host port/path kept in clone and ls-remote URLs; submodule OAuth auth; system-wide source selection. Covered by unit and feature tests. cosigned by OpenAI Codex at M1 Max
This commit is contained in:
1 parent
9d341d0bb9
commit
a26091de0a
32 files changed
+2216
-26
No files matched your search
@@ -6,10 +6,14 @@ use App\Actions\Application\CleanupPreviewDeployment;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Controllers\Webhook\Concerns\DetectsSkipDeployCommits;
|
||||
use App\Http\Controllers\Webhook\Concerns\MatchesManualWebhookApplications;
|
||||
use App\Livewire\Source\Gitlab\Change as GitlabSource;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\GitlabApp;
|
||||
use Exception;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class Gitlab extends Controller
|
||||
@@ -17,6 +21,302 @@ class Gitlab extends Controller
|
||||
use DetectsSkipDeployCommits;
|
||||
use MatchesManualWebhookApplications;
|
||||
|
||||
public function redirect(Request $request)
|
||||
{
|
||||
try {
|
||||
$code = $request->query('code');
|
||||
$state = $request->query('state');
|
||||
|
||||
if (! $code || ! $state) {
|
||||
return redirect()->route('source.all')->with('error', 'Invalid GitLab OAuth callback. Missing code or state.');
|
||||
}
|
||||
|
||||
// Validate the one-time, team-bound state (not a guessable source UUID) to stop forged callbacks from overwriting a source's tokens.
|
||||
$payload = Cache::pull(GitlabSource::oauthStateCacheKey($state));
|
||||
$team_id = $request->user()?->currentTeam()?->id;
|
||||
if (! is_array($payload) || is_null($team_id) || (int) data_get($payload, 'team_id') !== (int) $team_id) {
|
||||
return redirect()->route('source.all')->with('error', 'Invalid or expired GitLab OAuth state. Please start the authorization again.');
|
||||
}
|
||||
|
||||
$gitlabApp = GitlabApp::whereKey(data_get($payload, 'gitlab_app_id'))->firstOrFail();
|
||||
|
||||
$baseUrl = rtrim($gitlabApp->html_url, '/');
|
||||
|
||||
$response = Http::asForm()->post("{$baseUrl}/oauth/token", [
|
||||
'client_id' => $gitlabApp->client_id,
|
||||
'client_secret' => $gitlabApp->client_secret,
|
||||
'code' => $code,
|
||||
'grant_type' => 'authorization_code',
|
||||
'redirect_uri' => $gitlabApp->redirect_uri,
|
||||
]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
$error = data_get($response->json(), 'error_description', 'Token exchange failed');
|
||||
|
||||
return redirect()->route('source.gitlab.show', ['gitlab_app_uuid' => $gitlabApp->uuid])
|
||||
->with('error', "GitLab OAuth failed: {$error}");
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$gitlabApp->update([
|
||||
'access_token' => $data['access_token'],
|
||||
'refresh_token' => $data['refresh_token'],
|
||||
'expires_at' => time() + ($data['expires_in'] ?? 7200),
|
||||
]);
|
||||
|
||||
return redirect()->route('source.gitlab.show', ['gitlab_app_uuid' => $gitlabApp->uuid]);
|
||||
} catch (Exception $e) {
|
||||
return redirect()->route('source.all')->with('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function normal(Request $request)
|
||||
{
|
||||
try {
|
||||
$return_payloads = collect([]);
|
||||
$payload = $request->collect();
|
||||
$x_gitlab_token = $request->header('X-Gitlab-Token');
|
||||
$object_kind = data_get($payload, 'object_kind');
|
||||
$project_id = data_get($payload, 'project.id');
|
||||
|
||||
$allowed_events = ['push', 'merge_request'];
|
||||
if (! in_array($object_kind, $allowed_events)) {
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
'message' => 'Event not allowed. Only push and merge_request events are allowed.',
|
||||
]);
|
||||
}
|
||||
|
||||
if (empty($x_gitlab_token)) {
|
||||
auditLogWebhookFailure('gitlab', 'webhook_token_missing', [
|
||||
'event' => $object_kind,
|
||||
]);
|
||||
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
'message' => 'Missing X-Gitlab-Token header.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
$gitlab_app = GitlabApp::where('webhook_token', $x_gitlab_token)->first();
|
||||
if (! $gitlab_app) {
|
||||
auditLogWebhookFailure('gitlab', 'invalid_token', [
|
||||
'event' => $object_kind,
|
||||
]);
|
||||
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
'message' => 'Invalid webhook token.',
|
||||
], 401);
|
||||
}
|
||||
|
||||
$applications = Application::where('source_id', $gitlab_app->id)
|
||||
->where('source_type', GitlabApp::class)
|
||||
->where('repository_project_id', $project_id);
|
||||
|
||||
if ($object_kind === 'push') {
|
||||
$branch = data_get($payload, 'ref');
|
||||
if (Str::isMatch('/refs\/heads\/*/', $branch)) {
|
||||
$branch = Str::after($branch, 'refs/heads/');
|
||||
}
|
||||
if (! $branch) {
|
||||
return response([
|
||||
'status' => 'failed',
|
||||
'message' => 'No branch found in the request.',
|
||||
]);
|
||||
}
|
||||
|
||||
$applications = $applications->where('git_branch', $branch)->get();
|
||||
$added_files = data_get($payload, 'commits.*.added');
|
||||
$removed_files = data_get($payload, 'commits.*.removed');
|
||||
$modified_files = data_get($payload, 'commits.*.modified');
|
||||
$changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten();
|
||||
$skip_deploy_commits = self::shouldSkipDeploy(data_get($payload, 'commits.*.message', []));
|
||||
|
||||
foreach ($applications as $application) {
|
||||
if (! $application->destination->server->isFunctional()) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Server is not functional',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $application->isDeployable()) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Deployments disabled',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files);
|
||||
if (! $is_watch_path_triggered && ! blank($application->watch_paths)) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Changed files do not match watch paths.',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($skip_deploy_commits) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'All commits contain [skip cd] or [skip ci].',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
commit: data_get($payload, 'after', 'HEAD'),
|
||||
force_rebuild: false,
|
||||
is_webhook: true,
|
||||
);
|
||||
|
||||
if ($result['status'] === 'queue_full') {
|
||||
return response($result['message'], 429)->header('Retry-After', 60);
|
||||
}
|
||||
|
||||
auditLog('webhook.deployment.queued', [
|
||||
'provider' => 'gitlab',
|
||||
'mode' => 'app',
|
||||
'application_uuid' => $application->uuid,
|
||||
'application_name' => $application->name,
|
||||
'deployment_uuid' => $deployment_uuid->toString(),
|
||||
'commit' => data_get($payload, 'after'),
|
||||
]);
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => $result['status'] ?? 'success',
|
||||
'message' => $result['message'] ?? 'Deployment queued.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($object_kind === 'merge_request') {
|
||||
$action = data_get($payload, 'object_attributes.action');
|
||||
$branch = data_get($payload, 'object_attributes.source_branch');
|
||||
$base_branch = data_get($payload, 'object_attributes.target_branch');
|
||||
$pull_request_id = data_get($payload, 'object_attributes.iid');
|
||||
$pull_request_html_url = data_get($payload, 'object_attributes.url');
|
||||
$pull_request_title = data_get($payload, 'object_attributes.title');
|
||||
$latest_commit_message = data_get($payload, 'object_attributes.last_commit.message');
|
||||
$skip_deploy_pr = self::shouldSkipDeployAny([$pull_request_title, $latest_commit_message]);
|
||||
|
||||
$applications = $applications->where('git_branch', $base_branch)->get();
|
||||
|
||||
foreach ($applications as $application) {
|
||||
if (! $application->destination->server->isFunctional()) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Server is not functional',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (in_array($action, ['open', 'opened', 'synchronize', 'reopened', 'reopen', 'update'])) {
|
||||
if (! $application->isPRDeployable()) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'failed',
|
||||
'message' => 'Preview deployments disabled',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($skip_deploy_pr) {
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'skipped',
|
||||
'message' => 'PR title or latest commit contains [skip cd] or [skip ci].',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$deployment_uuid = new Cuid2;
|
||||
$found = ApplicationPreview::where('application_id', $application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first();
|
||||
|
||||
if (! $found) {
|
||||
if ($application->build_pack === 'dockercompose') {
|
||||
$pr_app = ApplicationPreview::create([
|
||||
'git_type' => 'gitlab',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
'docker_compose_domains' => $application->docker_compose_domains,
|
||||
]);
|
||||
$pr_app->generate_preview_fqdn_compose();
|
||||
} else {
|
||||
$pr_app = ApplicationPreview::create([
|
||||
'git_type' => 'gitlab',
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $pull_request_id,
|
||||
'pull_request_html_url' => $pull_request_html_url,
|
||||
]);
|
||||
$pr_app->generate_preview_fqdn();
|
||||
}
|
||||
}
|
||||
|
||||
$result = queue_application_deployment(
|
||||
application: $application,
|
||||
pull_request_id: $pull_request_id,
|
||||
deployment_uuid: $deployment_uuid,
|
||||
commit: data_get($payload, 'object_attributes.last_commit.id', 'HEAD'),
|
||||
force_rebuild: false,
|
||||
is_webhook: true,
|
||||
git_type: 'gitlab',
|
||||
);
|
||||
|
||||
if ($result['status'] === 'queue_full') {
|
||||
return response($result['message'], 429)->header('Retry-After', 60);
|
||||
}
|
||||
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => $result['status'] ?? 'success',
|
||||
'message' => $result['message'] ?? 'Preview Deployment queued',
|
||||
]);
|
||||
} elseif (in_array($action, ['closed', 'close', 'merge'])) {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)
|
||||
->where('pull_request_id', $pull_request_id)
|
||||
->first();
|
||||
|
||||
if ($found) {
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
$return_payloads->push([
|
||||
'application' => $application->name,
|
||||
'status' => 'success',
|
||||
'message' => 'Preview deployment closed.',
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response($return_payloads);
|
||||
} catch (Exception $e) {
|
||||
return handleError($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function manual(Request $request)
|
||||
{
|
||||
try {
|
||||
@@ -291,8 +591,6 @@ class Gitlab extends Controller
|
||||
} elseif ($action === 'closed' || $action === 'close' || $action === 'merge') {
|
||||
$found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first();
|
||||
if ($found) {
|
||||
// Use comprehensive cleanup that cancels active deployments,
|
||||
// kills helper containers, and removes all PR containers
|
||||
CleanupPreviewDeployment::run($application, $pull_request_id, $found);
|
||||
|
||||
$return_payloads->push([
|
||||
|
||||
@@ -96,9 +96,19 @@ class Source extends Component
|
||||
|
||||
private function getSources()
|
||||
{
|
||||
// filter the current source out
|
||||
$this->sources = currentTeam()->sources()->whereNotNull('app_id')->reject(function ($source) {
|
||||
return $source->id === $this->application->source_id;
|
||||
$this->sources = currentTeam()->sources()->filter(function ($source) {
|
||||
if ($source->id === $this->application->source_id
|
||||
&& $source->getMorphClass() === $this->application->source_type) {
|
||||
return false;
|
||||
}
|
||||
if ($source instanceof GithubApp) {
|
||||
return ! is_null($source->app_id);
|
||||
}
|
||||
if ($source instanceof GitlabApp) {
|
||||
return $source->isConnected();
|
||||
}
|
||||
|
||||
return true;
|
||||
})->sortBy('name');
|
||||
}
|
||||
|
||||
@@ -137,7 +147,6 @@ class Source extends Component
|
||||
|
||||
public function changeSource($sourceId, $sourceType)
|
||||
{
|
||||
|
||||
try {
|
||||
$this->authorize('update', $this->application);
|
||||
$allowedSourceTypes = [GithubApp::class, GitlabApp::class];
|
||||
@@ -150,16 +159,24 @@ class Source extends Component
|
||||
$this->dispatch('configurationChanged');
|
||||
|
||||
['repository' => $customRepository] = $this->application->customRepository();
|
||||
$repository = githubApi($this->application->source, "repos/{$customRepository}");
|
||||
$data = data_get($repository, 'data');
|
||||
$repository_project_id = data_get($data, 'id');
|
||||
if (isset($repository_project_id)) {
|
||||
if ($this->application->repository_project_id !== $repository_project_id) {
|
||||
$this->application->repository_project_id = $repository_project_id;
|
||||
$this->application->save();
|
||||
$repository_project_id = null;
|
||||
|
||||
if ($sourceType === GithubApp::class) {
|
||||
$repository = githubApi($source, "repos/{$customRepository}");
|
||||
$repository_project_id = data_get($repository, 'data.id');
|
||||
} elseif ($sourceType === GitlabApp::class) {
|
||||
if ($source->isConnected()) {
|
||||
$encoded = urlencode($customRepository);
|
||||
$project = gitlabApi($source, "/projects/{$encoded}");
|
||||
$repository_project_id = data_get($project, 'data.id');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($repository_project_id) && $this->application->repository_project_id !== $repository_project_id) {
|
||||
$this->application->repository_project_id = $repository_project_id;
|
||||
$this->application->save();
|
||||
}
|
||||
|
||||
$this->application->refresh();
|
||||
$this->getSources();
|
||||
$this->dispatch('success', 'Source updated!');
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Project\New;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\Project;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Component;
|
||||
|
||||
class GitlabPrivateRepository extends Component
|
||||
{
|
||||
public $current_step = 'gitlab_apps';
|
||||
|
||||
public $gitlab_apps;
|
||||
|
||||
public ?int $gitlab_app_id = null;
|
||||
|
||||
public $parameters;
|
||||
|
||||
public $currentRoute;
|
||||
|
||||
public $query;
|
||||
|
||||
public $type;
|
||||
|
||||
public int $selected_project_id;
|
||||
|
||||
public int $selected_gitlab_app_id;
|
||||
|
||||
public string $selected_repository_path = '';
|
||||
|
||||
public string $selected_branch_name = 'main';
|
||||
|
||||
public $repositories;
|
||||
|
||||
public int $total_repositories_count = 0;
|
||||
|
||||
public $branches;
|
||||
|
||||
public int $total_branches_count = 0;
|
||||
|
||||
public int $port = 3000;
|
||||
|
||||
public bool $is_static = false;
|
||||
|
||||
public ?string $publish_directory = null;
|
||||
|
||||
public ?string $base_directory = '/';
|
||||
|
||||
public ?string $docker_compose_location = '/docker-compose.yaml';
|
||||
|
||||
protected int $page = 1;
|
||||
|
||||
public $build_pack = 'nixpacks';
|
||||
|
||||
public bool $show_is_static = true;
|
||||
|
||||
private function getGitlabApp(): GitlabApp
|
||||
{
|
||||
return GitlabApp::private()->where('id', $this->gitlab_app_id)->firstOrFail();
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->currentRoute = Route::currentRouteName();
|
||||
$this->parameters = get_route_parameters();
|
||||
$this->query = request()->query();
|
||||
$this->repositories = $this->branches = collect();
|
||||
$this->gitlab_apps = GitlabApp::private()->select(['id', 'name', 'html_url', 'team_id', 'is_system_wide', 'is_public'])->get();
|
||||
}
|
||||
|
||||
public function updatedSelectedProjectId(): void
|
||||
{
|
||||
$this->loadBranches();
|
||||
}
|
||||
|
||||
public function updatedBuildPack()
|
||||
{
|
||||
if ($this->build_pack === 'nixpacks' || $this->build_pack === 'railpack') {
|
||||
$this->show_is_static = true;
|
||||
if (! $this->is_static) {
|
||||
$this->port = 3000;
|
||||
}
|
||||
} elseif ($this->build_pack === 'static') {
|
||||
$this->show_is_static = false;
|
||||
$this->is_static = false;
|
||||
$this->port = 80;
|
||||
} else {
|
||||
$this->show_is_static = false;
|
||||
$this->is_static = false;
|
||||
}
|
||||
}
|
||||
|
||||
public function loadRepositories($gitlab_app_id)
|
||||
{
|
||||
$this->repositories = collect();
|
||||
$this->branches = collect();
|
||||
$this->total_branches_count = 0;
|
||||
$this->page = 1;
|
||||
$this->selected_gitlab_app_id = $gitlab_app_id;
|
||||
$this->gitlab_app_id = $gitlab_app_id;
|
||||
$gitlab_app = $this->getGitlabApp();
|
||||
|
||||
try {
|
||||
$result = loadGitlabRepositories($gitlab_app, $this->page);
|
||||
$this->repositories = $this->repositories->concat(collect($result['repositories']));
|
||||
|
||||
while ($result['has_more'] && $this->page < 50) {
|
||||
$this->page++;
|
||||
$result = loadGitlabRepositories($gitlab_app, $this->page);
|
||||
$this->repositories = $this->repositories->concat(collect($result['repositories']));
|
||||
}
|
||||
$this->total_repositories_count = $this->repositories->count();
|
||||
|
||||
$this->repositories = $this->repositories->sortBy('name');
|
||||
if ($this->repositories->count() > 0) {
|
||||
$first = $this->repositories->first();
|
||||
$this->selected_project_id = data_get($first, 'id');
|
||||
$this->selected_repository_path = data_get($first, 'path_with_namespace');
|
||||
}
|
||||
$this->current_step = 'repository';
|
||||
} catch (\Throwable $e) {
|
||||
return $this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function loadBranches()
|
||||
{
|
||||
$repo = $this->repositories->where('id', $this->selected_project_id)->first();
|
||||
$this->selected_repository_path = data_get($repo, 'path_with_namespace', $this->selected_repository_path);
|
||||
$this->branches = collect();
|
||||
$this->page = 1;
|
||||
$gitlab_app = $this->getGitlabApp();
|
||||
|
||||
try {
|
||||
$branches = loadGitlabBranches($gitlab_app, $this->selected_project_id, $this->page);
|
||||
$this->total_branches_count = count($branches);
|
||||
$this->branches = $this->branches->concat(collect($branches));
|
||||
|
||||
while ($this->total_branches_count === 100) {
|
||||
$this->page++;
|
||||
$branches = loadGitlabBranches($gitlab_app, $this->selected_project_id, $this->page);
|
||||
$this->total_branches_count = count($branches);
|
||||
$this->branches = $this->branches->concat(collect($branches));
|
||||
}
|
||||
|
||||
$this->branches = sortBranchesByPriority($this->branches);
|
||||
$defaultBranch = data_get($repo, 'default_branch', 'main');
|
||||
$this->selected_branch_name = $this->branches->contains('name', $defaultBranch) ? $defaultBranch : data_get($this->branches, '0.name', 'main');
|
||||
} catch (\Throwable $e) {
|
||||
return $this->dispatch('error', $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$validator = validator([
|
||||
'selected_repository_path' => $this->selected_repository_path,
|
||||
'selected_branch_name' => $this->selected_branch_name,
|
||||
'docker_compose_location' => $this->docker_compose_location,
|
||||
], [
|
||||
'selected_repository_path' => 'required|string',
|
||||
'selected_branch_name' => ['required', 'string', new ValidGitBranch],
|
||||
'docker_compose_location' => ValidationPatterns::filePathRules(),
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new \RuntimeException('Invalid repository data: '.$validator->errors()->first());
|
||||
}
|
||||
|
||||
$destination_uuid = $this->query['destination'] ?? null;
|
||||
$destination = find_destination_for_current_team($destination_uuid);
|
||||
if (! $destination) {
|
||||
throw new \Exception('Destination not found.');
|
||||
}
|
||||
$destination_class = $destination->getMorphClass();
|
||||
|
||||
$project = Project::ownedByCurrentTeam()->where('uuid', $this->parameters['project_uuid'])->firstOrFail();
|
||||
$environment = $project->environments()->where('uuid', $this->parameters['environment_uuid'])->firstOrFail();
|
||||
|
||||
$gitlab_app = $this->getGitlabApp();
|
||||
|
||||
$application = Application::create([
|
||||
'name' => generate_application_name($this->selected_repository_path, $this->selected_branch_name),
|
||||
'repository_project_id' => $this->selected_project_id,
|
||||
'git_repository' => $this->selected_repository_path,
|
||||
'git_branch' => str($this->selected_branch_name)->trim()->toString(),
|
||||
'build_pack' => $this->build_pack,
|
||||
'ports_exposes' => $this->port,
|
||||
'publish_directory' => $this->publish_directory,
|
||||
'base_directory' => $this->base_directory,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination_class,
|
||||
'source_id' => $gitlab_app->id,
|
||||
'source_type' => $gitlab_app->getMorphClass(),
|
||||
]);
|
||||
$application->settings->is_static = $this->is_static;
|
||||
$application->settings->save();
|
||||
|
||||
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
$application->health_check_enabled = false;
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$application['docker_compose_location'] = $this->docker_compose_location;
|
||||
}
|
||||
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
|
||||
$application->fqdn = $fqdn;
|
||||
$application->name = generate_application_name($this->selected_repository_path, $this->selected_branch_name, $application->uuid);
|
||||
$application->save();
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
'project_uuid' => $project->uuid,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
if ($this->is_static) {
|
||||
$this->port = 80;
|
||||
$this->publish_directory = '/dist';
|
||||
} else {
|
||||
$this->port = 3000;
|
||||
$this->publish_directory = null;
|
||||
}
|
||||
$this->dispatch('success', 'Application settings updated!');
|
||||
}
|
||||
}
|
||||
@@ -170,6 +170,12 @@ class Select extends Component
|
||||
'description' => 'You can deploy public & private repositories through your GitHub Apps.',
|
||||
'logo' => asset('svgs/github.svg'),
|
||||
],
|
||||
[
|
||||
'id' => 'private-gitlab-app',
|
||||
'name' => 'Private Repository (with GitLab App)',
|
||||
'description' => 'You can deploy public & private repositories through your GitLab Apps.',
|
||||
'logo' => asset('svgs/gitlab.svg'),
|
||||
],
|
||||
[
|
||||
'id' => 'private-deploy-key',
|
||||
'name' => 'Private Repository (with Deploy Key)',
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Source\Gitlab;
|
||||
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Rules\SafeExternalUrl;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
|
||||
class Change extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public string $webhook_endpoint = '';
|
||||
|
||||
public ?string $ipv4 = null;
|
||||
|
||||
public ?string $ipv6 = null;
|
||||
|
||||
public ?string $fqdn = null;
|
||||
|
||||
public $parameters;
|
||||
|
||||
public ?GitlabApp $gitlab_app = null;
|
||||
|
||||
public string $name;
|
||||
|
||||
public string $apiUrl;
|
||||
|
||||
public string $htmlUrl;
|
||||
|
||||
public string $customUser;
|
||||
|
||||
public int $customPort;
|
||||
|
||||
public ?string $clientId = null;
|
||||
|
||||
public ?string $clientSecretInput = null;
|
||||
|
||||
public ?string $webhookToken = null;
|
||||
|
||||
public ?string $groupName = null;
|
||||
|
||||
public bool $isSystemWide;
|
||||
|
||||
public ?int $privateKeyId = null;
|
||||
|
||||
public $applications;
|
||||
|
||||
public $privateKeys;
|
||||
|
||||
public bool $isConnected = false;
|
||||
|
||||
public ?string $redirectUri = null;
|
||||
|
||||
public ?string $oauthState = null;
|
||||
|
||||
protected function rules(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'required|string',
|
||||
'apiUrl' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||
'htmlUrl' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||
'customUser' => 'required|string',
|
||||
'customPort' => 'required|int',
|
||||
'clientId' => 'nullable|string',
|
||||
'clientSecretInput' => 'nullable|string',
|
||||
'webhookToken' => 'nullable|string',
|
||||
'groupName' => 'nullable|string',
|
||||
'isSystemWide' => 'required|bool',
|
||||
'privateKeyId' => 'nullable|int',
|
||||
];
|
||||
}
|
||||
|
||||
public function mount()
|
||||
{
|
||||
try {
|
||||
$gitlab_app_uuid = request()->gitlab_app_uuid;
|
||||
$this->gitlab_app = GitlabApp::where(function ($query) {
|
||||
$query->where('team_id', currentTeam()->id)->orWhere('is_system_wide', true);
|
||||
})->whereUuid($gitlab_app_uuid)->firstOrFail();
|
||||
|
||||
$this->privateKeys = PrivateKey::ownedByCurrentTeamCached();
|
||||
$this->applications = $this->gitlab_app->applications;
|
||||
|
||||
$settings = instanceSettings();
|
||||
|
||||
$this->syncData(false);
|
||||
|
||||
$this->isConnected = $this->gitlab_app->isConnected();
|
||||
$this->fqdn = $settings->fqdn;
|
||||
|
||||
if ($settings->public_ipv4) {
|
||||
$this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port');
|
||||
}
|
||||
if ($settings->public_ipv6) {
|
||||
$this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port');
|
||||
}
|
||||
|
||||
$this->parameters = get_route_parameters();
|
||||
|
||||
if (isCloud() && ! isDev()) {
|
||||
$this->webhook_endpoint = config('app.url');
|
||||
} else {
|
||||
$this->webhook_endpoint = $this->fqdn ?? $this->ipv4 ?? $this->ipv6 ?? config('app.url') ?? '';
|
||||
}
|
||||
|
||||
$this->redirectUri = $this->webhook_endpoint.'/webhooks/source/gitlab/redirect';
|
||||
|
||||
$this->oauthState = $this->createOAuthState();
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public static function oauthStateCacheKey(string $state): string
|
||||
{
|
||||
return 'gitlab-app-oauth-state:'.hash('sha256', $state);
|
||||
}
|
||||
|
||||
private function createOAuthState(): string
|
||||
{
|
||||
$state = Str::random(64);
|
||||
|
||||
Cache::put(self::oauthStateCacheKey($state), [
|
||||
'gitlab_app_id' => $this->gitlab_app->id,
|
||||
'team_id' => currentTeam()->id,
|
||||
], now()->addMinutes(60));
|
||||
|
||||
return $state;
|
||||
}
|
||||
|
||||
private function syncData(bool $toModel = false): void
|
||||
{
|
||||
if ($toModel) {
|
||||
$this->gitlab_app->name = $this->name;
|
||||
$this->gitlab_app->api_url = $this->apiUrl;
|
||||
$this->gitlab_app->html_url = rtrim($this->htmlUrl, '/');
|
||||
$this->gitlab_app->custom_user = $this->customUser;
|
||||
$this->gitlab_app->custom_port = $this->customPort;
|
||||
$this->gitlab_app->client_id = $this->clientId;
|
||||
if (! empty($this->clientSecretInput)) {
|
||||
$this->gitlab_app->client_secret = $this->clientSecretInput;
|
||||
}
|
||||
$this->gitlab_app->webhook_token = $this->webhookToken;
|
||||
$this->gitlab_app->group_name = $this->groupName;
|
||||
$this->gitlab_app->is_system_wide = $this->isSystemWide;
|
||||
$this->gitlab_app->private_key_id = $this->privateKeyId;
|
||||
$this->gitlab_app->redirect_uri = $this->redirectUri;
|
||||
} else {
|
||||
$this->name = $this->gitlab_app->name;
|
||||
$this->apiUrl = $this->gitlab_app->api_url;
|
||||
$this->htmlUrl = $this->gitlab_app->html_url;
|
||||
$this->customUser = $this->gitlab_app->custom_user;
|
||||
$this->customPort = $this->gitlab_app->custom_port;
|
||||
$this->clientId = $this->gitlab_app->client_id;
|
||||
$this->clientSecretInput = null;
|
||||
$this->webhookToken = $this->gitlab_app->webhook_token;
|
||||
$this->groupName = $this->gitlab_app->group_name;
|
||||
$this->isSystemWide = $this->gitlab_app->is_system_wide;
|
||||
$this->privateKeyId = $this->gitlab_app->private_key_id;
|
||||
}
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->gitlab_app);
|
||||
|
||||
$this->validate();
|
||||
|
||||
$this->syncData(true);
|
||||
$this->gitlab_app->save();
|
||||
$this->dispatch('success', 'GitLab App updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function instantSave()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->gitlab_app);
|
||||
|
||||
$this->gitlab_app->makeVisible(['client_secret', 'webhook_token', 'access_token', 'refresh_token']);
|
||||
$this->syncData(true);
|
||||
$this->gitlab_app->save();
|
||||
$this->dispatch('success', 'GitLab App updated.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function testConnection()
|
||||
{
|
||||
try {
|
||||
$this->authorize('view', $this->gitlab_app);
|
||||
|
||||
if (! $this->gitlab_app->isConnected()) {
|
||||
$this->dispatch('error', 'GitLab App is not connected. Please complete the OAuth flow first.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
refreshGitlabToken($this->gitlab_app);
|
||||
|
||||
$apiUrl = $this->gitlab_app->apiUrlBase();
|
||||
$response = Http::GitLab($apiUrl, $this->gitlab_app->access_token)
|
||||
->timeout(10)
|
||||
->get('/user');
|
||||
|
||||
if ($response->successful()) {
|
||||
$username = data_get($response->json(), 'username', 'unknown');
|
||||
$this->dispatch('success', "Connection successful! Authenticated as: {$username}");
|
||||
} else {
|
||||
$error = data_get($response->json(), 'message', 'Unknown error');
|
||||
$this->dispatch('error', "Connection failed: {$error}");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function disconnect()
|
||||
{
|
||||
try {
|
||||
$this->authorize('update', $this->gitlab_app);
|
||||
|
||||
$this->gitlab_app->update([
|
||||
'access_token' => null,
|
||||
'refresh_token' => null,
|
||||
'expires_at' => null,
|
||||
]);
|
||||
|
||||
$this->isConnected = false;
|
||||
$this->dispatch('success', 'GitLab App disconnected.');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete()
|
||||
{
|
||||
try {
|
||||
$this->authorize('delete', $this->gitlab_app);
|
||||
|
||||
if ($this->gitlab_app->applications->isNotEmpty()) {
|
||||
$this->dispatch('error', 'This source is being used by an application. Please delete all applications first.');
|
||||
|
||||
return;
|
||||
}
|
||||
$this->gitlab_app->delete();
|
||||
|
||||
return redirect()->route('source.all');
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
|
||||
public function getOAuthUrl(): string
|
||||
{
|
||||
$baseUrl = rtrim($this->htmlUrl, '/');
|
||||
|
||||
$query = http_build_query([
|
||||
'client_id' => $this->clientId,
|
||||
'redirect_uri' => $this->redirectUri,
|
||||
'response_type' => 'code',
|
||||
'scope' => 'api read_user read_repository',
|
||||
'state' => $this->oauthState ??= $this->createOAuthState(),
|
||||
]);
|
||||
|
||||
return "{$baseUrl}/oauth/authorize?{$query}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Source\Gitlab;
|
||||
|
||||
use App\Models\GitlabApp;
|
||||
use App\Rules\SafeExternalUrl;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
|
||||
class Create extends Component
|
||||
{
|
||||
use AuthorizesRequests;
|
||||
|
||||
public string $name;
|
||||
|
||||
public string $html_url = 'https://gitlab.com';
|
||||
|
||||
public bool $is_system_wide = false;
|
||||
|
||||
public ?string $group_name = null;
|
||||
|
||||
public function mount()
|
||||
{
|
||||
$this->name = substr(generate_random_name(), 0, 30);
|
||||
}
|
||||
|
||||
public function createGitLabApp()
|
||||
{
|
||||
try {
|
||||
$this->authorize('createAnyResource');
|
||||
|
||||
$this->validate([
|
||||
'name' => 'required|string',
|
||||
'html_url' => ['required', 'string', 'url', new SafeExternalUrl],
|
||||
'is_system_wide' => 'required|bool',
|
||||
'group_name' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$htmlUrl = rtrim($this->html_url, '/');
|
||||
$apiUrl = $htmlUrl.'/api/v4';
|
||||
|
||||
$gitlab_app = GitlabApp::create([
|
||||
'name' => $this->name,
|
||||
'api_url' => $apiUrl,
|
||||
'html_url' => $htmlUrl,
|
||||
'is_system_wide' => $this->is_system_wide,
|
||||
'group_name' => $this->group_name,
|
||||
'webhook_token' => Str::random(32),
|
||||
'team_id' => currentTeam()->id,
|
||||
]);
|
||||
|
||||
if (session('from')) {
|
||||
session(['from' => session('from') + ['source_id' => $gitlab_app->id]]);
|
||||
}
|
||||
|
||||
return redirectRoute($this, 'source.gitlab.show', ['gitlab_app_uuid' => $gitlab_app->uuid]);
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1378,11 +1378,11 @@ class Application extends BaseModel
|
||||
|
||||
if ($this->deploymentType() === 'source') {
|
||||
$source_html_url = data_get($this, 'source.html_url');
|
||||
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL));
|
||||
$source_html_url_host = $url['host'];
|
||||
$source_html_url_scheme = $url['scheme'];
|
||||
|
||||
if ($this->source->getMorphClass() == 'App\Models\GithubApp') {
|
||||
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL)) ?: [];
|
||||
$source_html_url_host = $url['host'] ?? '';
|
||||
$source_html_url_scheme = $url['scheme'] ?? '';
|
||||
$escapedCustomRepository = escapeshellarg($customRepository);
|
||||
if ($this->source->is_public) {
|
||||
$escapedRepoUrl = escapeshellarg("{$this->source->html_url}/{$customRepository}");
|
||||
@@ -1420,6 +1420,32 @@ class Application extends BaseModel
|
||||
|
||||
if ($this->source->getMorphClass() === GitlabApp::class) {
|
||||
$gitlabSource = $this->source;
|
||||
|
||||
if ($gitlabSource->isConnected()) {
|
||||
$url = parse_url(filter_var($source_html_url, FILTER_SANITIZE_URL)) ?: [];
|
||||
$source_html_url_host = $this->urlHostWithPort($url);
|
||||
$source_html_url_scheme = $url['scheme'] ?? '';
|
||||
$token = generateGitlabCloneToken($gitlabSource);
|
||||
$encodedToken = rawurlencode($token);
|
||||
$pathPrefix = rtrim($url['path'] ?? '', '/');
|
||||
$repoUrl = "{$source_html_url_scheme}://oauth2:{$encodedToken}@{$source_html_url_host}{$pathPrefix}/{$customRepository}.git";
|
||||
$escapedRepoUrl = escapeshellarg($repoUrl);
|
||||
$fullRepoUrl = $repoUrl;
|
||||
$base_command = "{$base_command} {$escapedRepoUrl}";
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $base_command));
|
||||
} else {
|
||||
$commands->push($base_command);
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
}
|
||||
|
||||
$private_key = data_get($gitlabSource, 'privateKey.private_key');
|
||||
|
||||
if ($private_key) {
|
||||
@@ -1444,7 +1470,6 @@ class Application extends BaseModel
|
||||
];
|
||||
}
|
||||
|
||||
// GitLab source without private key — use URL as-is (supports user-embedded basic auth)
|
||||
$fullRepoUrl = $customRepository;
|
||||
$escapedCustomRepository = escapeshellarg($customRepository);
|
||||
$base_command = "{$base_command} {$escapedCustomRepository}";
|
||||
@@ -1677,6 +1702,52 @@ class Application extends BaseModel
|
||||
|
||||
if ($this->source->getMorphClass() === GitlabApp::class) {
|
||||
$gitlabSource = $this->source;
|
||||
|
||||
if ($gitlabSource->isConnected()) {
|
||||
$token = generateGitlabCloneToken($gitlabSource);
|
||||
$encodedToken = rawurlencode($token);
|
||||
$pathPrefix = rtrim($url['path'] ?? '', '/');
|
||||
$source_html_url_host = $this->urlHostWithPort($url ?: []);
|
||||
|
||||
// Rewrite same-host HTTPS submodule URLs to auth with the OAuth token (mirrors the GitHub path) without persisting credentials.
|
||||
$gitConfigOption = '-c '.escapeshellarg("url.{$source_html_url_scheme}://oauth2:{$encodedToken}@{$source_html_url_host}{$pathPrefix}/.insteadOf={$source_html_url_scheme}://{$source_html_url_host}{$pathPrefix}/");
|
||||
$gitConfigOptions = $this->withGitHttpTransportConfig($gitConfigOption);
|
||||
|
||||
$repoUrl = "{$source_html_url_scheme}://oauth2:{$encodedToken}@{$source_html_url_host}{$pathPrefix}/{$customRepository}.git";
|
||||
$escapedRepoUrl = escapeshellarg($repoUrl);
|
||||
$fullRepoUrl = $repoUrl;
|
||||
$git_clone_command_base = $this->applyGitConfigOptionsToCloneCommand("{$git_clone_command} {$escapedRepoUrl} {$escapedBaseDir}", $gitConfigOptions);
|
||||
if ($only_checkout) {
|
||||
$git_clone_command = $git_clone_command_base;
|
||||
} else {
|
||||
$git_clone_command = $this->setGitImportSettings($deployment_uuid, $git_clone_command_base, commit: $commit, gitConfigOptions: $gitConfigOptions);
|
||||
}
|
||||
|
||||
if ($pull_request_id !== 0) {
|
||||
$branch = "merge-requests/{$pull_request_id}/head:{$pr_branch_name}";
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, "echo 'Checking out {$branch}'"));
|
||||
} else {
|
||||
$commands->push("echo 'Checking out {$branch}'");
|
||||
}
|
||||
$git_checkout_command = $this->buildGitCheckoutCommand($pr_branch_name, gitConfigOptions: $gitConfigOptions);
|
||||
$escapedPrBranch = escapeshellarg($branch);
|
||||
$git_clone_command = "{$git_clone_command} && cd {$escapedBaseDir} && git {$gitConfigOptions} fetch origin {$escapedPrBranch} && {$git_checkout_command}";
|
||||
}
|
||||
|
||||
if ($exec_in_docker) {
|
||||
$commands->push(executeInDocker($deployment_uuid, $git_clone_command));
|
||||
} else {
|
||||
$commands->push($git_clone_command);
|
||||
}
|
||||
|
||||
return [
|
||||
'commands' => $commands->implode(' && '),
|
||||
'branch' => $branch,
|
||||
'fullRepoUrl' => $fullRepoUrl,
|
||||
];
|
||||
}
|
||||
|
||||
$private_key = data_get($gitlabSource, 'privateKey.private_key');
|
||||
|
||||
if ($private_key) {
|
||||
@@ -1717,7 +1788,6 @@ class Application extends BaseModel
|
||||
];
|
||||
}
|
||||
|
||||
// GitLab source without private key — use URL as-is (supports user-embedded basic auth)
|
||||
$fullRepoUrl = $customRepository;
|
||||
$escapedCustomRepository = escapeshellarg($customRepository);
|
||||
$git_clone_command = "{$git_clone_command} {$escapedCustomRepository} {$escapedBaseDir}";
|
||||
@@ -2382,6 +2452,14 @@ class Application extends BaseModel
|
||||
];
|
||||
}
|
||||
|
||||
private function urlHostWithPort(array $url): string
|
||||
{
|
||||
$host = $url['host'] ?? '';
|
||||
$port = isset($url['port']) ? ":{$url['port']}" : '';
|
||||
|
||||
return "{$host}{$port}";
|
||||
}
|
||||
|
||||
public function generateConfig($is_json = false)
|
||||
{
|
||||
$generator = new ConfigurationGenerator($this);
|
||||
|
||||
@@ -16,20 +16,68 @@ class GitlabApp extends BaseModel
|
||||
'app_id',
|
||||
'app_secret',
|
||||
'oauth_id',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
'expires_at',
|
||||
'redirect_uri',
|
||||
'group_name',
|
||||
'public_key',
|
||||
'webhook_token',
|
||||
'deploy_key_id',
|
||||
'private_key_id',
|
||||
'team_id',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'webhook_token',
|
||||
'app_secret',
|
||||
'client_secret',
|
||||
'access_token',
|
||||
'refresh_token',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'access_token' => 'encrypted',
|
||||
'refresh_token' => 'encrypted',
|
||||
'client_secret' => 'encrypted',
|
||||
'is_system_wide' => 'boolean',
|
||||
'is_public' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function booted(): void
|
||||
{
|
||||
static::deleting(function (GitlabApp $gitlabApp) {
|
||||
if ($gitlabApp->applications()->count() > 0) {
|
||||
throw new \RuntimeException('This source is being used by an application. Please delete all applications first.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static function ownedByCurrentTeam()
|
||||
{
|
||||
return GitlabApp::whereTeamId(currentTeam()->id);
|
||||
return GitlabApp::where(function ($query) {
|
||||
$query->where('team_id', currentTeam()->id)
|
||||
->orWhere('is_system_wide', true);
|
||||
});
|
||||
}
|
||||
|
||||
public static function public()
|
||||
{
|
||||
return GitlabApp::where(function ($query) {
|
||||
$query->where('team_id', currentTeam()->id)->orWhere('is_system_wide', true);
|
||||
})->where('is_public', true);
|
||||
}
|
||||
|
||||
public static function private()
|
||||
{
|
||||
return GitlabApp::where(function ($query) {
|
||||
$query->where('team_id', currentTeam()->id)->orWhere('is_system_wide', true);
|
||||
})->where('is_public', false)->whereNotNull('access_token');
|
||||
}
|
||||
|
||||
public function applications()
|
||||
@@ -41,4 +89,25 @@ class GitlabApp extends BaseModel
|
||||
{
|
||||
return $this->belongsTo(PrivateKey::class);
|
||||
}
|
||||
|
||||
public function team()
|
||||
{
|
||||
return $this->belongsTo(Team::class);
|
||||
}
|
||||
|
||||
public function isConnected(): bool
|
||||
{
|
||||
return ! empty($this->access_token) && ! empty($this->refresh_token);
|
||||
}
|
||||
|
||||
public function apiUrlBase(): string
|
||||
{
|
||||
$apiUrl = rtrim($this->api_url, '/');
|
||||
|
||||
if (! str_contains($apiUrl, '/api/v4')) {
|
||||
$apiUrl .= '/api/v4';
|
||||
}
|
||||
|
||||
return $apiUrl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\User;
|
||||
|
||||
class GitlabAppPolicy
|
||||
{
|
||||
public function viewAny(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function view(User $user, GitlabApp $gitlabApp): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function create(User $user): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function update(User $user, GitlabApp $gitlabApp): bool
|
||||
{
|
||||
if ($gitlabApp->is_system_wide) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function delete(User $user, GitlabApp $gitlabApp): bool
|
||||
{
|
||||
if ($gitlabApp->is_system_wide) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function restore(User $user, GitlabApp $gitlabApp): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
public function forceDelete(User $user, GitlabApp $gitlabApp): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -102,5 +102,16 @@ class AppServiceProvider extends ServiceProvider
|
||||
])->baseUrl($api_url);
|
||||
}
|
||||
});
|
||||
|
||||
Http::macro('GitLab', function (string $api_url, ?string $access_token = null) {
|
||||
$client = Http::withHeaders([
|
||||
'Accept' => 'application/json',
|
||||
])->baseUrl($api_url);
|
||||
if ($access_token) {
|
||||
$client = $client->withToken($access_token);
|
||||
}
|
||||
|
||||
return $client;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use App\Models\EmailNotificationSettings;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
@@ -51,6 +52,7 @@ use App\Policies\DatabasePolicy;
|
||||
use App\Policies\EnvironmentPolicy;
|
||||
use App\Policies\EnvironmentVariablePolicy;
|
||||
use App\Policies\GithubAppPolicy;
|
||||
use App\Policies\GitlabAppPolicy;
|
||||
use App\Policies\InstanceSettingsPolicy;
|
||||
use App\Policies\NotificationPolicy;
|
||||
use App\Policies\PrivateKeyPolicy;
|
||||
@@ -127,6 +129,9 @@ class AuthServiceProvider extends ServiceProvider
|
||||
|
||||
// Git source policies
|
||||
GithubApp::class => GithubAppPolicy::class,
|
||||
GitlabApp::class => GitlabAppPolicy::class,
|
||||
|
||||
// Cloud provider policies
|
||||
CloudProviderToken::class => CloudProviderTokenPolicy::class,
|
||||
CloudInitScript::class => CloudInitScriptPolicy::class,
|
||||
|
||||
|
||||
Reference in new issue
Block a user