Skip to content

Commit

Permalink
Merge pull request #4754 from coollabsio/improve-git-and-service-prov…
Browse files Browse the repository at this point in the history
…ider

Improves: GitHub handling, AppServiceProvider and 500 error message rendering
  • Loading branch information
andrasbacsai authored Jan 16, 2025
2 parents f997872 + dd897a1 commit 3c83c7f
Show file tree
Hide file tree
Showing 13 changed files with 392 additions and 77 deletions.
11 changes: 10 additions & 1 deletion app/Jobs/GithubAppPermissionJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,19 +27,28 @@ public function __construct(public GithubApp $github_app) {}
public function handle()
{
try {
$github_access_token = generate_github_jwt_token($this->github_app);
$github_access_token = generateGithubJwt($this->github_app);

$response = Http::withHeaders([
'Authorization' => "Bearer $github_access_token",
'Accept' => 'application/vnd.github+json',
])->get("{$this->github_app->api_url}/app");

if (! $response->successful()) {
throw new \RuntimeException('Failed to fetch GitHub app permissions: '.$response->body());
}

$response = $response->json();
$permissions = data_get($response, 'permissions');

$this->github_app->contents = data_get($permissions, 'contents');
$this->github_app->metadata = data_get($permissions, 'metadata');
$this->github_app->pull_requests = data_get($permissions, 'pull_requests');
$this->github_app->administration = data_get($permissions, 'administration');

$this->github_app->save();
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');

} catch (\Throwable $e) {
send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage());
throw $e;
Expand Down
2 changes: 1 addition & 1 deletion app/Livewire/Project/New/GithubPrivateRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public function loadRepositories($github_app_id)
$this->page = 1;
$this->selected_github_app_id = $github_app_id;
$this->github_app = GithubApp::where('id', $github_app_id)->first();
$this->token = generate_github_installation_token($this->github_app);
$this->token = generateGithubInstallationToken($this->github_app);
$this->loadRepositoryByPage();
if ($this->repositories->count() < $this->total_repositories_count) {
while ($this->repositories->count() < $this->total_repositories_count) {
Expand Down
2 changes: 1 addition & 1 deletion app/Livewire/Source/Github/Change.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public function checkPermissions()
// Need administration:read:write permission
// https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#list-self-hosted-runners-for-a-repository

// $github_access_token = generate_github_installation_token($this->github_app);
// $github_access_token = generateGithubInstallationToken($this->github_app);
// $repositories = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100");
// $runners_by_repository = collect([]);
// $repositories = $repositories->json()['repositories'];
Expand Down
4 changes: 2 additions & 2 deletions app/Models/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -999,7 +999,7 @@ public function generateGitLsRemoteCommands(string $deployment_uuid, bool $exec_
$fullRepoUrl = "{$this->source->html_url}/{$customRepository}";
$base_command = "{$base_command} {$this->source->html_url}/{$customRepository}";
} else {
$github_access_token = generate_github_installation_token($this->source);
$github_access_token = generateGithubInstallationToken($this->source);

if ($exec_in_docker) {
$base_command = "{$base_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
Expand Down Expand Up @@ -1111,7 +1111,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req
$commands->push($git_clone_command);
}
} else {
$github_access_token = generate_github_installation_token($this->source);
$github_access_token = generateGithubInstallationToken($this->source);
if ($exec_in_docker) {
$git_clone_command = "{$git_clone_command} $source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git {$baseDir}";
$fullRepoUrl = "$source_html_url_scheme://x-access-token:$github_access_token@$source_html_url_host/{$customRepository}.git";
Expand Down
57 changes: 44 additions & 13 deletions app/Providers/AppServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,68 @@
namespace App\Providers;

use App\Models\PersonalAccessToken;
use Illuminate\Support\Facades\Event;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\ServiceProvider;
use Illuminate\Validation\Rules\Password;
use Laravel\Sanctum\Sanctum;
use Laravel\Telescope\TelescopeServiceProvider;

class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
if ($this->app->environment('local')) {
$this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
if (App::isLocal()) {
$this->app->register(TelescopeServiceProvider::class);
}
}

public function boot(): void
{
Event::listen(function (\SocialiteProviders\Manager\SocialiteWasCalled $event) {
$event->extendSocialite('authentik', \SocialiteProviders\Authentik\Provider::class);
});
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
$this->configureCommands();
$this->configureModels();
$this->configurePasswords();
$this->configureSanctumModel();
$this->configureGitHubHttp();
}

Password::defaults(function () {
$rule = Password::min(8);
private function configureCommands(): void
{
if (App::isProduction()) {
DB::prohibitDestructiveCommands();
}
}

private function configureModels(): void
{
// Disabled because it's causing issues with the application
// Model::shouldBeStrict();
}

return $this->app->isProduction()
? $rule->mixedCase()->letters()->numbers()->symbols()
: $rule;
private function configurePasswords(): void
{
Password::defaults(function () {
return App::isProduction()
? Password::min(8)
->mixedCase()
->letters()
->numbers()
->symbols()
->uncompromised()
: Password::min(8)->letters();
});
}

Http::macro('github', function (string $api_url, ?string $github_access_token = null) {
private function configureSanctumModel(): void
{
Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class);
}

private function configureGitHubHttp(): void
{
Http::macro('GitHub', function (string $api_url, ?string $github_access_token = null) {
if ($github_access_token) {
return Http::withHeaders([
'X-GitHub-Api-Version' => '2022-11-28',
Expand Down
118 changes: 75 additions & 43 deletions bootstrap/helpers/github.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,85 +12,117 @@
use Lcobucci\JWT\Signer\Rsa\Sha256;
use Lcobucci\JWT\Token\Builder;

function generate_github_installation_token(GithubApp $source)
function generateGithubToken(GithubApp $source, string $type)
{
$response = Http::get("{$source->api_url}/zen");
$serverTime = CarbonImmutable::now()->setTimezone('UTC');
$githubTime = Carbon::parse($response->header('date'));
$timeDiff = abs($serverTime->diffInSeconds($githubTime));

if ($timeDiff > 50) {
throw new \Exception(
'System time is out of sync with GitHub API time:<br>'.
'- System time: '.$serverTime->format('Y-m-d H:i:s').' UTC<br>'.
'- GitHub time: '.$githubTime->format('Y-m-d H:i:s').' UTC<br>'.
'- Difference: '.$timeDiff.' seconds<br>'.
'Please synchronize your system clock.'
);
}

$signingKey = InMemory::plainText($source->privateKey->private_key);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
$now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
$issuedToken = $tokenBuilder
$now = CarbonImmutable::now()->setTimezone('UTC');
$now = $now->setTime($now->format('H'), $now->format('i'), $now->format('s'));

$jwt = $tokenBuilder
->issuedBy($source->app_id)
->issuedAt($now)
->expiresAt($now->modify('+10 minutes'))
->issuedAt($now->modify('-1 minute'))
->expiresAt($now->modify('+8 minutes'))
->getToken($algorithm, $signingKey)
->toString();
$token = Http::withHeaders([
'Authorization' => "Bearer $issuedToken",
'Accept' => 'application/vnd.github.machine-man-preview+json',
])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");
if ($token->failed()) {
throw new RuntimeException('Failed to get access token for '.$source->name.' with error: '.data_get($token->json(), 'message', 'no error message found'));
}

return $token->json()['token'];
return match ($type) {
'jwt' => $jwt,
'installation' => (function () use ($source, $jwt) {
$response = Http::withHeaders([
'Authorization' => "Bearer $jwt",
'Accept' => 'application/vnd.github.machine-man-preview+json',
])->post("{$source->api_url}/app/installations/{$source->installation_id}/access_tokens");

if (! $response->successful()) {
$error = data_get($response->json(), 'message', 'no error message found');
throw new RuntimeException("Failed to get installation token for {$source->name} with error: ".$error);
}

return $response->json()['token'];
})(),
default => throw new \InvalidArgumentException("Unsupported token type: {$type}")
};
}

function generate_github_jwt_token(GithubApp $source)
function generateGithubInstallationToken(GithubApp $source)
{
$signingKey = InMemory::plainText($source->privateKey->private_key);
$algorithm = new Sha256;
$tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default()));
$now = CarbonImmutable::now();
$now = $now->setTime($now->format('H'), $now->format('i'));
return generateGithubToken($source, 'installation');
}

return $tokenBuilder
->issuedBy($source->app_id)
->issuedAt($now->modify('-1 minute'))
->expiresAt($now->modify('+10 minutes'))
->getToken($algorithm, $signingKey)
->toString();
function generateGithubJwt(GithubApp $source)
{
return generateGithubToken($source, 'jwt');
}

function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true)
{
if (is_null($source)) {
throw new \Exception('Not implemented yet.');
throw new \Exception('Source is required for API calls');
}

if ($source->getMorphClass() !== GithubApp::class) {
throw new \InvalidArgumentException("Unsupported source type: {$source->getMorphClass()}");
}
if ($source->getMorphClass() === \App\Models\GithubApp::class) {
if ($source->is_public) {
$response = Http::github($source->api_url)->$method($endpoint);

if ($source->is_public) {
$response = Http::GitHub($source->api_url)->$method($endpoint);
} else {
$token = generateGithubInstallationToken($source);
if ($data && in_array(strtolower($method), ['post', 'patch', 'put'])) {
$response = Http::GitHub($source->api_url, $token)->$method($endpoint, $data);
} else {
$github_access_token = generate_github_installation_token($source);
if ($data && ($method === 'post' || $method === 'patch' || $method === 'put')) {
$response = Http::github($source->api_url, $github_access_token)->$method($endpoint, $data);
} else {
$response = Http::github($source->api_url, $github_access_token)->$method($endpoint);
}
$response = Http::GitHub($source->api_url, $token)->$method($endpoint);
}
}
$json = $response->json();
if ($response->failed() && $throwError) {
ray($json);
throw new \Exception("Failed to get data from {$source->name} with error:<br><br>".$json['message'].'<br><br>Rate Limit resets at: '.Carbon::parse((int) $response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s').'UTC');

if (! $response->successful() && $throwError) {
$resetTime = Carbon::parse((int) $response->header('X-RateLimit-Reset'))->format('Y-m-d H:i:s');
$errorMessage = data_get($response->json(), 'message', 'no error message found');
$remainingCalls = $response->header('X-RateLimit-Remaining', '0');

throw new \Exception(
'GitHub API call failed:<br>'.
"Error: {$errorMessage}<br>".
'Rate Limit Status:<br>'.
"- Remaining Calls: {$remainingCalls}<br>".
"- Reset Time: {$resetTime} UTC"
);
}

return [
'rate_limit_remaining' => $response->header('X-RateLimit-Remaining'),
'rate_limit_reset' => $response->header('X-RateLimit-Reset'),
'data' => collect($json),
'data' => collect($response->json()),
];
}

function get_installation_path(GithubApp $source)
function getInstallationPath(GithubApp $source)
{
$github = GithubApp::where('uuid', $source->uuid)->first();
$name = str(Str::kebab($github->name));
$installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps';

return "$github->html_url/$installation_path/$name/installations/new";
}
function get_permissions_path(GithubApp $source)

function getPermissionsPath(GithubApp $source)
{
$github = GithubApp::where('uuid', $source->uuid)->first();
$name = str(Str::kebab($github->name));
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"spatie/laravel-ray": "^1.37",
"spatie/laravel-schemaless-attributes": "^2.4",
"spatie/url": "^2.2",
"stevebauman/purify": "^6.2",
"stripe/stripe-php": "^16.2.0",
"symfony/yaml": "^7.1.6",
"visus/cuid2": "^4.1.0",
Expand Down
Loading

0 comments on commit 3c83c7f

Please sign in to comment.