From 17fab5f363bbdd54445f693a575a45883b5674a3 Mon Sep 17 00:00:00 2001
From: Uladzimir Tsykun
Date: Sun, 4 Jun 2023 03:35:16 +0200
Subject: [PATCH] GitLab's integration webhooks support
---
config/packages/security.yaml | 2 +-
config/services.yaml | 3 +-
...mpose-prod.yml => docker-compose-split.yml | 0
docker-compose.yml | 13 +-
public/packeton/img/logo/bitbucket.png | Bin 0 -> 1490 bytes
public/packeton/img/logo/gitea.png | Bin 0 -> 4351 bytes
public/packeton/img/logo/github.png | Bin 0 -> 6393 bytes
public/packeton/img/logo/gitlab.png | Bin 0 -> 4323 bytes
public/packeton/js/layout.js | 5 +
src/Controller/Api/ApiController.php | 48 +++-
.../OAuth/IntegrationController.php | 25 +-
src/Controller/PackageController.php | 2 +-
src/DependencyInjection/Configuration.php | 15 +-
src/Entity/OAuthIntegration.php | 12 +
src/Entity/PackageSerializedTrait.php | 10 +
src/Event/PackageEvent.php | 22 ++
src/EventListener/IntegrationListener.php | 57 ++++
src/Form/Type/IntegrationSettingsType.php | 3 +-
src/Integrations/AppInterface.php | 10 +-
src/Integrations/Base/AppIntegrationTrait.php | 28 +-
.../Base/BaseIntegrationTrait.php | 38 ++-
src/Integrations/Github/GitHubIntegration.php | 2 +-
src/Integrations/Github/GithubResultPager.php | 8 +-
src/Integrations/Gitlab/GitLabIntegration.php | 260 +++++++++++++++++-
.../Gitlab/GitLabOAuth2Factory.php | 15 +
src/Integrations/IntegrationRegistry.php | 3 +
src/Integrations/Model/AppConfig.php | 10 +
src/Integrations/Model/IntegrationUtils.php | 27 ++
src/Menu/MenuBuilder.php | 7 +-
src/Model/AutoHookUser.php | 13 +-
src/Model/PackageManager.php | 9 +
src/Model/PatUserScores.php | 5 +
src/Security/Api/ApiTokenAuthenticator.php | 6 +-
.../Token/IntegrationTokenChecker.php | 5 +-
src/Security/Token/PatTokenChecker.php | 3 +-
src/Twig/PackagistExtension.php | 4 +-
templates/integration/connect.html.twig | 45 +++
templates/integration/index.html.twig | 33 ++-
templates/integration/list.html.twig | 38 +++
translations/messages.en.yml | 2 +
40 files changed, 718 insertions(+), 70 deletions(-)
rename docker-compose-prod.yml => docker-compose-split.yml (100%)
create mode 100644 public/packeton/img/logo/bitbucket.png
create mode 100644 public/packeton/img/logo/gitea.png
create mode 100644 public/packeton/img/logo/github.png
create mode 100644 public/packeton/img/logo/gitlab.png
create mode 100644 src/Event/PackageEvent.php
create mode 100644 src/EventListener/IntegrationListener.php
create mode 100644 templates/integration/connect.html.twig
create mode 100644 templates/integration/list.html.twig
diff --git a/config/packages/security.yaml b/config/packages/security.yaml
index aa433b82..25ac82f2 100644
--- a/config/packages/security.yaml
+++ b/config/packages/security.yaml
@@ -72,7 +72,7 @@ security:
# Maintainers
- { path: (^(/users/(.+)/packages))+, roles: ROLE_MAINTAINER }
- { path: (^(/users/(.+)/favorites))+, roles: ROLE_MAINTAINER }
- - { path: (^(/metadata/changes.json$|/explore|/jobs/|/archive/))+, roles: ROLE_MAINTAINER }
+ - { path: (^(/metadata/changes.json$|/explore|/jobs/|/archive/|/api/hooks/))+, roles: ROLE_MAINTAINER }
# Secured part of the site
# This config requires being logged for the whole site and having the admin role for the admin part.
diff --git a/config/services.yaml b/config/services.yaml
index 837110c5..64dac3ca 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -219,6 +219,7 @@ services:
- '@Packeton\Security\Token\DefaultTokenChecker'
calls:
- [addTokenChecker, ['@Packeton\Security\Token\PatTokenChecker']]
- - [addTokenChecker, ['@Packeton\Security\Token\PatTokenChecker']]
+ - [addTokenChecker, ['@Packeton\Security\Token\JwtTokenChecker']]
+ - [addTokenChecker, ['@Packeton\Security\Token\IntegrationTokenChecker']]
Symfony\Component\HttpClient\NoPrivateNetworkHttpClient: ~
diff --git a/docker-compose-prod.yml b/docker-compose-split.yml
similarity index 100%
rename from docker-compose-prod.yml
rename to docker-compose-split.yml
diff --git a/docker-compose.yml b/docker-compose.yml
index cad57e28..eb2983f2 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,16 +5,21 @@ services:
build:
context: .
image: packeton/packeton:latest
+ restart: unless-stopped
container_name: packagist
hostname: packagist
environment:
- ADMIN_USER: admin
- ADMIN_PASSWORD: 123456
- ADMIN_EMAIL: admin@example.com
+# ADMIN_USER: admin
+# ADMIN_PASSWORD: 123456
+# ADMIN_EMAIL: admin@example.com
TRUSTED_PROXIES: 172.16.0.0/12
# Default SQLite
- # DATABASE_URL: "mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4"
+ DATABASE_URL: "mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8&charset=utf8mb4"
+
+# MAILER_DSN: smtp://user:pass@smtp.mailgun.org:587
+# MAILER_FROM: Packeton
ports:
+# setup nginx reverse proxy for ssl
- '127.0.0.1:8088:80'
volumes:
- .docker:/data
diff --git a/public/packeton/img/logo/bitbucket.png b/public/packeton/img/logo/bitbucket.png
new file mode 100644
index 0000000000000000000000000000000000000000..06a68b9bf550b9cfab2c484288a5743bd9d3bd52
GIT binary patch
literal 1490
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
literal 0
HcmV?d00001
diff --git a/public/packeton/img/logo/github.png b/public/packeton/img/logo/github.png
new file mode 100644
index 0000000000000000000000000000000000000000..6cb3b705d018006a2bd4200ea94c9d5fb98b6f76
GIT binary patch
literal 6393
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
Hu0mjfZ$b4`
literal 0
HcmV?d00001
diff --git a/public/packeton/img/logo/gitlab.png b/public/packeton/img/logo/gitlab.png
new file mode 100644
index 0000000000000000000000000000000000000000..aeb653f1d7c8fd9e6d539ddcac9395a2d933fb50
GIT binary patch
literal 4323
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
PDFEg-Zy95+dp!9czY1r(
literal 0
HcmV?d00001
diff --git a/public/packeton/js/layout.js b/public/packeton/js/layout.js
index 5001c610..85248146 100644
--- a/public/packeton/js/layout.js
+++ b/public/packeton/js/layout.js
@@ -90,4 +90,9 @@
if (typeof select2.select2 === 'function') {
select2.select2();
}
+
+ let tooltip = $('[data-toggle="tooltip"]');
+ if (typeof tooltip.tooltip === 'function') {
+ tooltip.tooltip();
+ }
})(jQuery, humane);
diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php
index ec120c18..c7f3bb01 100644
--- a/src/Controller/Api/ApiController.php
+++ b/src/Controller/Api/ApiController.php
@@ -11,6 +11,8 @@
use Packeton\Entity\Package;
use Packeton\Entity\User;
use Packeton\Entity\Webhook;
+use Packeton\Integrations\IntegrationRegistry;
+use Packeton\Model\AutoHookUser;
use Packeton\Model\DownloadManager;
use Packeton\Model\PackageManager;
use Packeton\Service\Scheduler;
@@ -35,7 +37,9 @@ public function __construct(
protected DownloadManager $downloadManager,
protected LoggerInterface $logger,
protected ValidatorInterface $validator,
- ){}
+ protected IntegrationRegistry $integrations
+ ) {
+ }
#[Route('/api/create-package', name: 'generic_create', methods: ['POST'])]
public function createPackageAction(Request $request): Response
@@ -65,6 +69,8 @@ public function createPackageAction(Request $request): Response
$em = $this->registry->getManager();
$em->persist($package);
$em->flush();
+
+ $this->container->get(PackageManager::class)->insetPackage($package);
} catch (\Exception $e) {
$this->logger->critical($e->getMessage(), ['exception', $e]);
return new JsonResponse(['status' => 'error', 'message' => 'Error saving package'], 500);
@@ -78,15 +84,23 @@ public function createPackageAction(Request $request): Response
#[Route('/api/hooks/{alias}/{id}', name: 'api_integration_postreceive')]
public function integrationHook(Request $request, string $alias, #[Vars] OAuthIntegration $oauth): Response
{
- // parse the payload
- $payload = $this->getJsonPayload($request);
+ if ($alias !== $oauth->getAlias()) {
+ return new JsonResponse(['error' => "App $alias is not found"], 409);
+ }
+
+ $response = $this->receiveIntegrationHook($request, $oauth);
+ if (null !== $response) {
+ return new JsonResponse($response, $response['code'] ?? 200);
+ }
+
+ return $this->updatePackageAction($request, fallback: true);
}
#[Route('/api/github', name: 'github_postreceive')]
#[Route('/api/bitbucket', name: 'bitbucket_postreceive')]
#[Route('/api/update-package', name: 'generic_postreceive')]
#[Route('/api/update-package/{name}', name: 'generic_named_postreceive', requirements: ['name' => '%package_name_regex%'])]
- public function updatePackageAction(Request $request, #[Vars] Package $package = null): Response
+ public function updatePackageAction(Request $request, #[Vars] Package $package = null, bool $fallback = false): Response
{
// parse the payload
$payload = $this->getJsonPayload($request);
@@ -95,6 +109,12 @@ public function updatePackageAction(Request $request, #[Vars] Package $package =
return new JsonResponse(['status' => 'error', 'message' => 'Missing payload parameter'], 406);
}
+ // May helpfully for GitLab Packagist Integrations. Replacement for group webhooks that enabled only for PAID EE version
+ // See docs how to use GitLab Integrations
+ if (false === $fallback && $this->getUser() instanceof AutoHookUser && null !== ($response = $this->receiveIntegrationHook($request))) {
+ return new JsonResponse($response, $response['code'] ?? 200);
+ }
+
$packages = [$package];
// Get from query parameter.
if ($packageNames = $request->get('composer_package_name')) {
@@ -332,6 +352,26 @@ protected function receivePost(Request $request, $url, $urlRegex)
return $this->schedulePostJobs($packages);
}
+ protected function receiveIntegrationHook(Request $request, OAuthIntegration $oauth = null): ?array
+ {
+ $user = $this->getUser();
+ if (null === $oauth && $user instanceof AutoHookUser) {
+ $oauth = $this->registry->getRepository(OAuthIntegration::class)->find((int) $user->getHookIdentifier());
+ if (null === $oauth) {
+ return null;
+ }
+ }
+
+ try {
+ $app = $this->integrations->findApp($oauth->getAlias());
+ return $app->receiveHooks($request, $this->getJsonPayload($request), $oauth);
+ } catch (\Throwable $e) {
+ $this->logger->error($e->getMessage(), ['e' => $e]);
+ }
+
+ return null;
+ }
+
/**
* @param Package[] $packages
* @return Response
diff --git a/src/Controller/OAuth/IntegrationController.php b/src/Controller/OAuth/IntegrationController.php
index 5dc00ce0..4cdc4b54 100644
--- a/src/Controller/OAuth/IntegrationController.php
+++ b/src/Controller/OAuth/IntegrationController.php
@@ -12,6 +12,8 @@
use Packeton\Integrations\Exception\NotFoundAppException;
use Packeton\Integrations\IntegrationRegistry;
use Packeton\Integrations\AppInterface;
+use Packeton\Integrations\Model\IntegrationUtils;
+use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
@@ -23,13 +25,22 @@ class IntegrationController extends AbstractController
public function __construct(
protected IntegrationRegistry $integrations,
protected ManagerRegistry $registry,
+ protected LoggerInterface $logger
) {
}
#[Route('', name: 'integration_list')]
public function listAction(): Response
{
+ $integrations = $this->integrations->findAllApps();
+ return $this->render('integration/list.html.twig', ['integrations' => $integrations]);
+ }
+ #[Route('/connect', name: 'integration_connect')]
+ public function connect(): Response
+ {
+ $integrations = $this->integrations->findAllApps();
+ return $this->render('integration/connect.html.twig', ['integrations' => $integrations]);
}
#[Route('/{alias}/{id}', name: 'integration_index')]
@@ -100,16 +111,22 @@ public function connectOrg(Request $request, string $alias, #[Vars] OAuthIntegra
$client = $this->getClient($alias, $oauth);
$oauth->setConnected($org, $connected = !$oauth->isConnected($org));
- $this->registry->getManager()->flush();
+
$response = ['connected' => $connected];
try {
- $connected ? $client->addOrgHook($oauth, $org) : $client->removeOrgHook($oauth, $org);
+ $status = $connected ? $client->addOrgHook($oauth, $org) : $client->removeOrgHook($oauth, $org);
+ if (is_array($status)) {
+ $oauth->setWebhookInfo($org, $status);
+ $response += $status;
+ }
} catch (\Throwable $e) {
- return new JsonResponse(['error' => $e->getMessage()] + $response, 400);
+ $this->logger->error($e->getMessage(), ['e' => $e]);
+ $response += ['error' => IntegrationUtils::castError($e), 'code' => 409];
}
- return new JsonResponse($response, 200);
+ $this->registry->getManager()->flush();
+ return new JsonResponse($response, $response['code'] ?? 200);
}
protected function getClient($alias, OAuthIntegration $oauth = null): AppInterface
diff --git a/src/Controller/PackageController.php b/src/Controller/PackageController.php
index d383b245..ad8446ae 100644
--- a/src/Controller/PackageController.php
+++ b/src/Controller/PackageController.php
@@ -129,7 +129,7 @@ public function submitPackageAction(Request $req, string $type = null): Response
$em->persist($package);
$em->flush();
- $this->providerManager->insertPackage($package);
+ $this->container->get(PackageManager::class)->insetPackage($package);
$this->addFlash('success', $package->getName().' has been added to the package list, the repository will now be crawled.');
return new RedirectResponse($this->generateUrl('view_package', ['name' => $package->getName()]));
diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php
index 9ac13ab1..1572ecbc 100644
--- a/src/DependencyInjection/Configuration.php
+++ b/src/DependencyInjection/Configuration.php
@@ -217,6 +217,7 @@ private function addIntegrationSection(ArrayNodeDefinition $rootNode, array $fac
$nodeBuilder->children()
->booleanNode('enabled')->defaultTrue()->end()
->scalarNode('base_url')->end()
+ ->scalarNode('webhook_url')->info('Static current host')->end()
->scalarNode('svg_logo')->end()
->scalarNode('logo')->end()
->scalarNode('login_title')->end()
@@ -267,15 +268,25 @@ private function defaultIconsData(): array
{
return [
'github' => [
- 'logo' => null,
+ 'logo' => '/packeton/img/logo/github.png',
'svg_logo' => 'svg/github.html.twig',
'login_title' => 'Login with GitHub',
],
'gitlab' => [
- 'logo' => null,
+ 'logo' => '/packeton/img/logo/gitlab.png',
'svg_logo' => 'svg/gitlab.html.twig',
'login_title' => 'Login with GitLab',
],
+ 'gitea' => [
+ 'logo' => '/packeton/img/logo/gitea.png',
+ 'svg_logo' => 'svg/gitea.html.twig',
+ 'login_title' => 'Login with Gitea',
+ ],
+ 'bitbucket' => [
+ 'logo' => '/packeton/img/logo/bitbucket.png',
+ 'svg_logo' => 'svg/gitea.html.twig',
+ 'login_title' => 'Login with Bitbucket',
+ ]
];
}
}
diff --git a/src/Entity/OAuthIntegration.php b/src/Entity/OAuthIntegration.php
index bdec1036..5d262dd7 100644
--- a/src/Entity/OAuthIntegration.php
+++ b/src/Entity/OAuthIntegration.php
@@ -127,6 +127,18 @@ public function getEnabledOrganizations(): array
return $this->getSerialized('enabled_org', 'array', []);
}
+ public function setWebhookInfo(string|int $orgs, array $info): self
+ {
+ $info += ['status' => true, 'error' => null, 'id' => null];
+ return $this->setSerialized("web_hook_$orgs", $info);
+ }
+
+ public function getWebhookInfo(string|int $orgs): ?array
+ {
+ $info = $this->getSerialized("web_hook_$orgs", 'array');
+ return is_array($info) ? $info + ['status' => null, 'error' => null, 'id' => null] : null;
+ }
+
public function isConnected(string|int $name): bool
{
return in_array((string)$name, $this->getEnabledOrganizations());
diff --git a/src/Entity/PackageSerializedTrait.php b/src/Entity/PackageSerializedTrait.php
index d49b7710..b8d3fc0d 100644
--- a/src/Entity/PackageSerializedTrait.php
+++ b/src/Entity/PackageSerializedTrait.php
@@ -36,6 +36,16 @@ public function setExcludedGlob(?string $glob): void
$this->setSerializedField('excludedGlob', $glob);
}
+ public function getWebhookInfo(): ?array
+ {
+ return $this->serializedData['webhook_info'] ?? null;
+ }
+
+ public function setWebhookInfo(?array $info): void
+ {
+ $this->setSerializedField('webhook_info', $info);
+ }
+
public function isSkipNotModifyTag(): ?bool
{
return (bool)($this->serializedData['skip_empty_tag'] ?? null);
diff --git a/src/Event/PackageEvent.php b/src/Event/PackageEvent.php
new file mode 100644
index 00000000..dd576b6c
--- /dev/null
+++ b/src/Event/PackageEvent.php
@@ -0,0 +1,22 @@
+package;
+ }
+}
diff --git a/src/EventListener/IntegrationListener.php b/src/EventListener/IntegrationListener.php
new file mode 100644
index 00000000..e347b60a
--- /dev/null
+++ b/src/EventListener/IntegrationListener.php
@@ -0,0 +1,57 @@
+getPackage();
+ if (null === ($oauth = $package->getIntegration()) || empty($package->getExternalRef())) {
+ return;
+ }
+
+ try {
+ $app = $this->integrations->findApp($oauth->getAlias());
+ $info = $app->addHook($oauth, $package->getExternalRef());
+ if ($info['status'] ?? false) {
+ $package->setAutoUpdated(true);
+ }
+ } catch (\Throwable $e) {
+ $info = ['status' => false, 'error' => IntegrationUtils::castError($e, $app ?? null)];
+ }
+
+ $package->setWebhookInfo($info);
+ }
+
+ #[AsEventListener(event: 'packageRemove')]
+ public function onPackageDelete(UpdaterEvent $event): void
+ {
+ $package = $event->getPackage();
+ if (null === ($oauth = $package->getIntegration()) || empty($package->getExternalRef())) {
+ return;
+ }
+
+ try {
+ $app = $this->integrations->findApp($oauth->getAlias(), false);
+ $app->removeHook($oauth, $package->getExternalRef());
+ } catch (\Throwable $e) {
+ }
+ }
+}
diff --git a/src/Form/Type/IntegrationSettingsType.php b/src/Form/Type/IntegrationSettingsType.php
index b22d6add..9b1df71a 100644
--- a/src/Form/Type/IntegrationSettingsType.php
+++ b/src/Form/Type/IntegrationSettingsType.php
@@ -5,6 +5,7 @@
namespace Packeton\Form\Type;
use Packeton\Entity\OAuthIntegration;
+use Packeton\Util\PacketonUtils;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
@@ -16,7 +17,7 @@ class IntegrationSettingsType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$repos = $options['repos'];
- $repos = array_combine(array_column($repos, 'label'), array_column($repos, 'name'));
+ $repos = PacketonUtils::buildChoices($repos, 'label', 'name');
$builder
->add('globFilter', TextareaType::class, [
diff --git a/src/Integrations/AppInterface.php b/src/Integrations/AppInterface.php
index 57410937..7e15a0ad 100644
--- a/src/Integrations/AppInterface.php
+++ b/src/Integrations/AppInterface.php
@@ -38,15 +38,15 @@ public function organizations(App $accessToken): array;
public function cacheClear(string|int $appId): void;
- public function addHook(App $accessToken, int|string $repoId): void;
+ public function addHook(App $accessToken, int|string $repoId): ?array;
- public function removeHook(App $accessToken, int|string $repoId): void;
+ public function removeHook(App $accessToken, int|string $repoId): ?array;
- public function addOrgHook(App $accessToken, int|string $orgId): void;
+ public function addOrgHook(App $accessToken, int|string $orgId): ?array;
- public function removeOrgHook(App $accessToken, int|string $orgId): void;
+ public function removeOrgHook(App $accessToken, int|string $orgId): ?array;
- public function receiveHooks(Request $request, array $payload): bool;
+ public function receiveHooks(Request $request, ?array $payload, App $app): ?array;
public function findApps(): array;
diff --git a/src/Integrations/Base/AppIntegrationTrait.php b/src/Integrations/Base/AppIntegrationTrait.php
index b7b91ae8..39d99f38 100644
--- a/src/Integrations/Base/AppIntegrationTrait.php
+++ b/src/Integrations/Base/AppIntegrationTrait.php
@@ -13,6 +13,8 @@
trait AppIntegrationTrait
{
+ protected $ownOrg = ['name' => 'Own profile', 'identifier' => '@self'];
+
protected \Redis $redis;
protected LockFactory $lock;
protected ManagerRegistry $registry;
@@ -79,30 +81,34 @@ public function organizations(OAuthIntegration $accessToken): array
return [];
}
- public function addHook(array|OAuthIntegration $accessToken, int|string $repositoryId): void
+ public function receiveHooks(Request $request, ?array $payload, OAuthIntegration $accessToken): ?array
{
+ return null;
}
- public function receiveHooks(Request $request, array $payload): bool
+ public function findApps(): array
{
- return false;
+ return $this->registry->getRepository(OAuthIntegration::class)->findBy(['alias' => $this->name]);
}
- public function findApps(): array
+ public function addHook(array|OAuthIntegration $accessToken, int|string $repositoryId): ?array
{
- return $this->registry->getRepository(OAuthIntegration::class)->findBy(['alias' => $this->name]);
+ return null;
}
- public function removeHook(OAuthIntegration $accessToken, int|string $repositoryId): void
+ public function removeHook(OAuthIntegration $accessToken, int|string $repositoryId): ?array
{
+ return null;
}
- public function addOrgHook(OAuthIntegration $accessToken, int|string $orgId): void
+ public function addOrgHook(OAuthIntegration $accessToken, int|string $orgId): ?array
{
+ return null;
}
- public function removeOrgHook(OAuthIntegration $accessToken, int|string $orgId): void
+ public function removeOrgHook(OAuthIntegration $accessToken, int|string $orgId): ?array
{
+ return null;
}
public function zipballDownload(OAuthIntegration $accessToken, string $reference): string
@@ -114,8 +120,9 @@ public function authenticateIO(OAuthIntegration $accessToken, IOInterface $io, C
{
}
- protected function getCached(string|int $appId, string $key, bool $withCache = true, callable $callback = null): mixed
+ protected function getCached(string|int|OAuthIntegration $appId, string $key, bool $withCache = true, callable $callback = null): mixed
{
+ $appId = $appId instanceof OAuthIntegration ? $appId->getId() : $appId;
if (true === $withCache) {
try {
$result = $this->redis->hGet("oauthapp:{$this->name}:$appId", $key);
@@ -131,8 +138,9 @@ protected function getCached(string|int $appId, string $key, bool $withCache = t
return $result;
}
- protected function setCached(string|int $appId, string $key, mixed $value): void
+ protected function setCached(string|int|OAuthIntegration $appId, string $key, mixed $value): void
{
+ $appId = $appId instanceof OAuthIntegration ? $appId->getId() : $appId;
$this->redis->hSet("oauthapp:{$this->name}:$appId", $key, json_encode($value));
}
}
diff --git a/src/Integrations/Base/BaseIntegrationTrait.php b/src/Integrations/Base/BaseIntegrationTrait.php
index f2031cc2..65a33527 100644
--- a/src/Integrations/Base/BaseIntegrationTrait.php
+++ b/src/Integrations/Base/BaseIntegrationTrait.php
@@ -16,27 +16,43 @@ trait BaseIntegrationTrait
/**
* {@inheritdoc}
*/
- public function getConfig(OAuthIntegration $app = null): AppConfig
+ public function getConfig(OAuthIntegration $app = null, bool $details = false): AppConfig
{
- return new AppConfig($this->config + $this->getConfigApp($app));
+ return new AppConfig($this->config + $this->getConfigApp($app, $details));
}
/**
* {@inheritdoc}
*/
- protected function getConfigApp(OAuthIntegration $app = null): array
+ protected function getConfigApp(OAuthIntegration $app = null, bool $details = false): array
{
- if (null === $app) {
- return [];
+ $config = [];
+ if ($app instanceof OAuthIntegration) {
+ $params = ['alias' => $this->name, 'id' => $app->getId(), 'token' => $app->getHookToken()];
+ if (($baseUrl = $this->config['webhook_url'] ?? null)) {
+ $baseUrl = rtrim($baseUrl, '/');
+ $components = parse_url($baseUrl);
+ if (isset($components['path'])) {
+ $config['hook_url'] = $baseUrl . "?token={$app->getHookToken()}";
+ } else {
+ $config['hook_url'] = $baseUrl . $this->router->generate('api_integration_postreceive', $params);
+ }
+ } else {
+ $config['hook_url'] = $this->router->generate('api_integration_postreceive', $params, 0);
+ }
}
- $base = $this->router->generate('home', referenceType: UrlGeneratorInterface::ABSOLUTE_URL);
- $hookUrl = $this->router->generate('api_integration_postreceive', ['alias' => $this->name, 'id' => $app->getId(), 'token' => $app->getHookToken()], 0);
+ if ($app || $details) {
+ $config['redirect_urls'] = $this->getRedirectUrls();
+ }
- return [
- 'redirect_urls' => [$base],
- 'hook_url' => $hookUrl
- ];
+ return $config;
+ }
+
+ public function getRedirectUrls(): array
+ {
+ $base = $this->router->generate('home', referenceType: UrlGeneratorInterface::ABSOLUTE_URL);
+ return [$base];
}
protected function getAuthorizationResponse(string $baseUrl, array $options, string $route = 'oauth_check'): RedirectResponse
diff --git a/src/Integrations/Github/GitHubIntegration.php b/src/Integrations/Github/GitHubIntegration.php
index aa69da1a..cc546d02 100644
--- a/src/Integrations/Github/GitHubIntegration.php
+++ b/src/Integrations/Github/GitHubIntegration.php
@@ -109,7 +109,7 @@ public function repositories(OAuthIntegration $app): array
$callback = function() use ($app, &$accessToken, &$url) {
$accessToken ??= $this->refreshToken($app);
$param = $this->getApiHeaders($accessToken);
- $pager = new GithubResultPager($this->httpClient, 'GET', $this->getApiUrl($url), $param);
+ $pager = new GithubResultPager($this->httpClient, $this->getApiUrl($url), $param);
return $pager->all();
};
diff --git a/src/Integrations/Github/GithubResultPager.php b/src/Integrations/Github/GithubResultPager.php
index 70e2e149..0ec61e23 100644
--- a/src/Integrations/Github/GithubResultPager.php
+++ b/src/Integrations/Github/GithubResultPager.php
@@ -11,20 +11,20 @@
#[Exclude]
class GithubResultPager
{
- public static $perPage = 100;
+ public $perPage = 100;
public function __construct(
private readonly HttpClientInterface $httpClient,
- private readonly string $method,
private readonly string $url,
- private readonly array $params
+ private readonly array $params,
+ private readonly string $method = 'GET'
) {
}
public function all(): array
{
$params = $this->params;
- $params['query']['per_page'] = self::$perPage;
+ $params['query']['per_page'] ??= $this->perPage;
$processed = [];
$url = $this->url;
diff --git a/src/Integrations/Gitlab/GitLabIntegration.php b/src/Integrations/Gitlab/GitLabIntegration.php
index f7593845..8edb5f2c 100644
--- a/src/Integrations/Gitlab/GitLabIntegration.php
+++ b/src/Integrations/Gitlab/GitLabIntegration.php
@@ -4,13 +4,19 @@
namespace Packeton\Integrations\Gitlab;
+use Composer\Config;
+use Composer\IO\IOInterface;
use Doctrine\Persistence\ManagerRegistry;
+use Packeton\Entity\OAuthIntegration;
+use Packeton\Entity\OAuthIntegration as App;
use Packeton\Entity\User;
use Packeton\Integrations\AppInterface;
use Packeton\Integrations\Base\AppIntegrationTrait;
use Packeton\Integrations\Base\BaseIntegrationTrait;
+use Packeton\Integrations\Github\GithubResultPager;
use Packeton\Integrations\IntegrationInterface;
use Packeton\Integrations\LoginInterface;
+use Packeton\Integrations\Model\IntegrationUtils;
use Packeton\Integrations\Model\OAuth2State;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
@@ -19,6 +25,7 @@
use Symfony\Component\Lock\LockFactory;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface as UG;
use Symfony\Component\Routing\RouterInterface;
+use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class GitLabIntegration implements IntegrationInterface, LoginInterface, AppInterface
@@ -132,9 +139,7 @@ public function fetchUser(Request|array $request, array $options = [], array &$a
{
$accessToken ??= $request instanceof Request ? $this->getAccessToken($request) : $request;
- $params = $this->getApiHeaders($accessToken);
- $response = $this->httpClient->request('GET', $this->getApiUrl('/user'), $params);
- $response = $response->toArray();
+ $response = $this->makeApiRequest($accessToken, 'GET', '/user');
$response['user_identifier'] = $response['email'];
$response['external_id'] = isset($response['id']) ? "{$this->name}:{$response['id']}" : null;
@@ -158,9 +163,256 @@ public function createUser(array $data): User
return $user;
}
+ /**
+ * {@inheritdoc}
+ */
+ public function organizations(App $app): array
+ {
+ $organizations = $this->getCached($app, 'orgs', callback: function () use ($app) {
+ $accessToken = $this->refreshToken($app);
+ return $this->makeCGetRequest($accessToken, '/groups', ['query' => 30]);
+ });
+
+ $orgs = array_map(fn ($org) => $org + ['identifier' => $org['id'], 'logo' => $org['avatar_url'] ?? null, 'name' => $org['name'] ?? $org['id']], $organizations);
+ return array_merge([$this->ownOrg], $orgs);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function repositories(App $app): array
+ {
+ $organizations = $app->getEnabledOrganizations();
+ $organizations = array_diff($organizations, ['@self']);
+
+ $allRepos = [];
+ if ($app->isConnected('@self')) {
+ $ownRepos = $this->getCached($app, 'repos:self', callback: function () use ($app) {
+ $accessToken = $this->refreshToken($app);
+ return $this->makeCGetRequest($accessToken, '/projects', ['query' => ['membership' => true, 'owned' => true]]);
+ });
+
+ $allRepos = array_merge($allRepos, $ownRepos);
+ }
+
+ foreach ($organizations as $organization) {
+ $orgRepos = $this->getCached($app, "repos:$organization", callback: function () use ($app, $organization) {
+ $accessToken = $this->refreshToken($app);
+
+ try {
+ return $this->makeCGetRequest($accessToken, "/groups/{$organization}/projects", ['query' => ['membership' => true, 'owned' => true]]);
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), ['e' => $e]);
+ return [];
+ }
+ });
+
+ $allRepos = array_merge($allRepos, $orgRepos);
+ }
+
+ return $this->formatRepos($allRepos);
+ }
+
+ protected function formatRepos(array $repos): array
+ {
+ $repos = array_map(function ($repo) {
+ $required = [
+ 'name' => $repo['path_with_namespace'],
+ 'label' => $repo['path_with_namespace'],
+ 'ext_ref' => $this->name.':'.$repo['id'],
+ 'url' => $repo['http_url_to_repo'] ?? $repo['web_url'],
+ 'ssh_url' => $repo['ssh_url_to_repo'] ?? null,
+ ];
+
+ return $required + $repo;
+ }, $repos);
+
+ $unique = [];
+ foreach ($repos as $repo) {
+ $unique[$repo['name']] = $repo;
+ }
+
+ return array_values($unique);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getRedirectUrls(): array
+ {
+ return [
+ $this->router->generate('oauth_install', ['alias' => $this->name], UG::ABSOLUTE_URL),
+ $this->router->generate('oauth_check', ['alias' => $this->name], UG::ABSOLUTE_URL),
+ ];
+ }
+
+ public function findHooks(App|array $accessToken, int|string $orgId, ?bool $isRepo = null, ?string $url = null): ?array
+ {
+ $orgId = str_replace($this->name.':', '', (string)$orgId, $count);
+ $isRepo ??= $count > 0;
+
+ $accessToken = $this->refreshToken($accessToken);
+ try {
+ $list = $this->makeCGetRequest($accessToken, $isRepo ? "/projects/$orgId/hooks" : "/groups/$orgId/hooks");
+ } catch (\Exception $e) {
+ return $url ? null : [];
+ }
+
+ if (null !== $url) {
+ $list = array_filter($list, fn ($u) => $u['url'] === $url);
+ return reset($list) ?: null;
+ }
+
+ return $list;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addOrgHook(App $app, int|string $orgId): ?array
+ {
+ return $this->doAddHook($app, $orgId, false);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeOrgHook(App $app, int|string $orgId): ?array
+ {
+ return $this->doRemoveHook($app, $orgId, false);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addHook(App $app, int|string $orgId): ?array
+ {
+ return $this->doAddHook($app, $orgId, true);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function removeHook(App $app, int|string $orgId): ?array
+ {
+ return $this->doRemoveHook($app, $orgId, true);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function authenticateIO(OAuthIntegration $oauth2, IOInterface $io, Config $config, string $repoUrl = null): void
+ {
+ $token = $this->refreshToken($oauth2);
+ $urls = parse_url($this->baseUrl);
+
+ $params = [
+ '_driver' => 'gitlab',
+ '_no_api' => !($useApi = IntegrationUtils::useApiPref($this->getConfig(), $oauth2)),
+ 'gitlab-domains' => ['gitlab.com', $urls['host']],
+ 'gitlab-oauth' => [$urls['host'] => $token['access_token']],
+ ];
+
+ if (false === $useApi) {
+ $params['_driver'] = 'git';
+ }
+
+ $params += $this->config['composer_config'] ?? [];
+ $config->merge(['config' => $params]);
+ $io->loadConfiguration($config);
+ }
+
+ protected function doAddHook(App $app, int|string $orgId, bool $isRepo): ?array
+ {
+ if ('@self' === $orgId) {
+ return null;
+ }
+
+ $orgId = str_replace($this->name.':', '', (string)$orgId);
+ $accessToken = $this->refreshToken($app);
+ $url = $this->getConfig($app)->getHookUrl();
+ if ($hook = $this->findHooks($accessToken, $orgId, $isRepo, $url)) {
+ return ['status' => true, 'id' => $hook['id']];
+ }
+
+ try {
+ $body = ['url' => $url, 'push_events' => true, 'tag_push_events' => true, 'merge_requests_events' => true];
+ $response = $this->makeApiRequest($accessToken, 'POST', $isRepo ? "/projects/$orgId/hooks" : "/groups/$orgId/hooks", ['json' => $body]);
+ if (isset($response['id'])) {
+ return ['status' => true, 'id' => $response['id']];
+ }
+ } catch (\Exception $e) {
+ if ($e instanceof HttpExceptionInterface && $e->getResponse()->getStatusCode() === 404 && false === $isRepo) {
+ $statusMessage = 'Notice. GitLab allow Groups webhooks only for EE paid plan. But you may manually setup '
+ . "Packagist integration with target on packeton host (without path), username \"token\" and token \"{$app->getHookToken()}\". "
+ . "See documentation for details";
+ }
+
+ return ['status' => false, 'error' => IntegrationUtils::castError($e), 'status_message' => $statusMessage ?? null];
+ }
+
+ if ($hook = $this->findHooks($accessToken, $orgId, $isRepo, $url)) {
+ return ['status' => true, 'id' => $hook['id']];
+ }
+
+ return null;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function doRemoveHook(App $app, int|string $orgId, bool $isRepo): ?array
+ {
+ if ('@self' === $orgId) {
+ return null;
+ }
+
+ $accessToken = $this->refreshToken($app);
+ $url = $this->getConfig($app)->getHookUrl();
+
+ $id = false === $isRepo ? ($app->getWebhookInfo($orgId)['id'] ?? null) : null;
+ if ($id !== null) {
+ try {
+ $this->makeApiRequest($accessToken, 'DELETE', $isRepo ? "/projects/$orgId/hooks/$id" : "/groups/$orgId/hooks/$id");
+ return [];
+ } catch (\Exception $e) {
+ }
+ }
+
+ if ($hook = $this->findHooks($accessToken, $orgId, $isRepo, $url)) {
+ $id = $hook['id'];
+ try {
+ $this->makeApiRequest($accessToken, 'DELETE', $isRepo ? "/projects/$orgId/hooks/$id" : "/groups/$orgId/hooks/$id");
+ return [];
+ } catch (\Exception $e) {
+ return ['status' => false, 'error' => IntegrationUtils::castError($e), 'id' => $id];
+ }
+ }
+
+ return [];
+ }
+
protected function getApiUrl(string $path): string
{
- return $this->baseUrl . '/api/v4' . $path;
+ $apiVer = $this->config['api_version'] ?? 'v4';
+ return $this->baseUrl . '/api/' . $apiVer . $path;
+ }
+
+ protected function makeApiRequest(array $token, string $method, string $path, array $params = []): array
+ {
+ $params = array_merge_recursive($this->getApiHeaders($token), $params);
+ $response = $this->httpClient->request($method, $this->getApiUrl($path), $params);
+ $content = $response->getContent();
+ $content = $content ? json_decode($content, true) : [];
+
+ return is_array($content) ? $content : [];
+ }
+
+ protected function makeCGetRequest(array $token, string $path, array $params = []): array
+ {
+ $params = array_merge_recursive($this->getApiHeaders($token), $params);
+ $paginator = new GithubResultPager($this->httpClient, $this->getApiUrl($path), $params);
+ return $paginator->all();
}
protected function getApiHeaders(array $token, array $default = []): array
diff --git a/src/Integrations/Gitlab/GitLabOAuth2Factory.php b/src/Integrations/Gitlab/GitLabOAuth2Factory.php
index 8172a953..6ed7be7e 100644
--- a/src/Integrations/Gitlab/GitLabOAuth2Factory.php
+++ b/src/Integrations/Gitlab/GitLabOAuth2Factory.php
@@ -6,6 +6,7 @@
use Packeton\Integrations\Factory\OAuth2FactoryInterface;
use Packeton\Integrations\Factory\OAuth2FactoryTrait;
+use Symfony\Component\Config\Definition\Builder\NodeDefinition;
use Symfony\Component\DependencyInjection\Attribute\Exclude;
#[Exclude]
@@ -15,4 +16,18 @@ class GitLabOAuth2Factory implements OAuth2FactoryInterface
protected $key = 'gitlab';
use OAuth2FactoryTrait;
+
+ /**
+ * {@inheritdoc}
+ */
+ public function addConfiguration(NodeDefinition $node): void
+ {
+ $builder = $node->children();
+
+ $builder
+ ->scalarNode('client_id')->end()
+ ->scalarNode('client_secret')->end()
+ ->scalarNode('api_version')->example('v4')->end();
+
+ }
}
diff --git a/src/Integrations/IntegrationRegistry.php b/src/Integrations/IntegrationRegistry.php
index e78948c9..f90bd09e 100644
--- a/src/Integrations/IntegrationRegistry.php
+++ b/src/Integrations/IntegrationRegistry.php
@@ -74,6 +74,9 @@ public function findApp(string $name, bool $check = true): AppInterface
return $app;
}
+ /**
+ * @return AppInterface[]
+ */
public function findAllApps(): array
{
$apps = [];
diff --git a/src/Integrations/Model/AppConfig.php b/src/Integrations/Model/AppConfig.php
index 3c284233..101ca520 100644
--- a/src/Integrations/Model/AppConfig.php
+++ b/src/Integrations/Model/AppConfig.php
@@ -56,6 +56,16 @@ public function roles(): array
return $this->defaultRoles;
}
+ public function getLogo(): ?string
+ {
+ return $this->config['logo'] ?? null;
+ }
+
+ public function getTitle(): ?string
+ {
+ return $this->config['title'] ?? null;
+ }
+
public function getName(): string
{
return $this->config['name'];
diff --git a/src/Integrations/Model/IntegrationUtils.php b/src/Integrations/Model/IntegrationUtils.php
index 994add89..a625afca 100644
--- a/src/Integrations/Model/IntegrationUtils.php
+++ b/src/Integrations/Model/IntegrationUtils.php
@@ -5,7 +5,9 @@
namespace Packeton\Integrations\Model;
use Packeton\Entity\OAuthIntegration;
+use Packeton\Entity\OAuthIntegration as App;
use Packeton\Integrations\AppInterface;
+use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
class IntegrationUtils
{
@@ -25,6 +27,31 @@ public static function findUrl(string $externalId, OAuthIntegration $oauth, AppI
return self::clonePref($app->getConfig(), $oauth) === 'clone_ssh' && isset($repo['ssh_url']) ? $repo['ssh_url'] : $repo['url'];
}
+ public static function castError(\Throwable $e, App|array $app = null): string
+ {
+ $msg = '';
+ $app = $app instanceof App ? $app->getAccessToken() : $app;
+ if ($e instanceof HttpExceptionInterface) {
+ try {
+ $msg = $e->getResponse()->getContent(false);
+ $msg = trim(substr($msg, 0, 512));
+ } catch (\Throwable) {
+ }
+ }
+
+ if (empty($msg)) {
+ $msg = $e->getMessage();
+ }
+
+ if (isset($app['access_token']) && is_string($app['access_token'])) {
+ $msg = str_replace($app['access_token'], '***', $msg);
+ }
+ if (isset($app['refresh_token']) && is_string($app['refresh_token'])) {
+ $msg = str_replace($app['refresh_token'], '***', $msg);
+ }
+ return $msg;
+ }
+
public static function clonePref(AppConfig $config, OAuthIntegration $oauth): string
{
return $oauth->getClonePreference() ?: $config->clonePref();
diff --git a/src/Menu/MenuBuilder.php b/src/Menu/MenuBuilder.php
index 91bdf52a..aa9bd91b 100644
--- a/src/Menu/MenuBuilder.php
+++ b/src/Menu/MenuBuilder.php
@@ -5,6 +5,7 @@
use Knp\Menu\FactoryInterface;
use Knp\Menu\ItemInterface;
use Packeton\Entity\User;
+use Packeton\Integrations\IntegrationRegistry;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
@@ -16,7 +17,8 @@ public function __construct(
private readonly FactoryInterface $factory,
private readonly TokenStorageInterface $tokenStorage,
private readonly TranslatorInterface $translator,
- private readonly AuthorizationCheckerInterface $checker
+ private readonly AuthorizationCheckerInterface $checker,
+ private readonly IntegrationRegistry $integrations
) {}
public function createUserMenu()
@@ -51,6 +53,9 @@ public function createAdminMenu()
$menu->addChild($this->translator->trans('menu.ssh_keys'), ['label' => 'menu.ssh_keys_icon', 'route' => 'user_add_sshkey', 'extras' => ['safe_label' => true]]);
$menu->addChild($this->translator->trans('menu.webhooks'), ['label' => 'menu.webhooks_icon', 'route' => 'webhook_index', 'extras' => ['safe_label' => true]]);
$menu->addChild($this->translator->trans('menu.proxies'), ['label' => 'menu.proxies_icon', 'route' => 'proxies_list', 'extras' => ['safe_label' => true]]);
+ if ($this->integrations->getNames()) {
+ $menu->addChild($this->translator->trans('menu.integrations'), ['label' => 'menu.integrations_icon', 'route' => 'integration_list', 'extras' => ['safe_label' => true]]);
+ }
return $menu;
}
diff --git a/src/Model/AutoHookUser.php b/src/Model/AutoHookUser.php
index c0eabd7d..82a06c0f 100644
--- a/src/Model/AutoHookUser.php
+++ b/src/Model/AutoHookUser.php
@@ -6,8 +6,17 @@
use Symfony\Component\Security\Core\User\UserInterface;
-final class AutoHookUser implements UserInterface
+class AutoHookUser implements UserInterface
{
+ public function __construct(protected string|int $hookIdentifier)
+ {
+ }
+
+ public function getHookIdentifier(): string|int
+ {
+ return $this->hookIdentifier;
+ }
+
/**
* {@inheritdoc}
*/
@@ -28,6 +37,6 @@ public function eraseCredentials()
*/
public function getUserIdentifier(): string
{
- return 'token_hooks';
+ return 'token_hooks'.$this->hookIdentifier;
}
}
diff --git a/src/Model/PackageManager.php b/src/Model/PackageManager.php
index 17be7189..facda502 100644
--- a/src/Model/PackageManager.php
+++ b/src/Model/PackageManager.php
@@ -11,6 +11,7 @@
use Packeton\Composer\PackagistFactory;
use Packeton\Entity\User;
use Packeton\Entity\Version;
+use Packeton\Event\PackageEvent;
use Packeton\Event\UpdaterEvent;
use Packeton\Package\InMemoryDumper;
use Packeton\Repository\VersionRepository;
@@ -41,6 +42,14 @@ public function __construct(
) {
}
+ public function insetPackage(Package $package): void
+ {
+ $this->providerManager->insertPackage($package);
+ $this->dispatcher->dispatch(new PackageEvent($package), PackageEvent::PACKAGE_CREATE);
+ $em = $this->doctrine->getManager();
+ $em->flush();
+ }
+
public function deletePackage(Package $package)
{
/** @var VersionRepository $versionRepo */
diff --git a/src/Model/PatUserScores.php b/src/Model/PatUserScores.php
index 3de2abaf..0877ca00 100644
--- a/src/Model/PatUserScores.php
+++ b/src/Model/PatUserScores.php
@@ -37,4 +37,9 @@ public static function getAllowedRoutes(string|array $scores): array
return array_unique($routes);
}
+
+ public static function isAllowed(string|array $scores, $route): bool
+ {
+ return in_array($route, self::getAllowedRoutes($scores), true);
+ }
}
diff --git a/src/Security/Api/ApiTokenAuthenticator.php b/src/Security/Api/ApiTokenAuthenticator.php
index 67ac5c25..d1b556f0 100644
--- a/src/Security/Api/ApiTokenAuthenticator.php
+++ b/src/Security/Api/ApiTokenAuthenticator.php
@@ -108,10 +108,8 @@ public function supports(Request $request): bool
}
if ($username = $request->query->get('token')) {
- $username = \explode(':', $username);
- if (2 === \count($username)) {
- return true;
- }
+ $token = \explode(':', $username);
+ return 2 === \count($token) || strlen($username) > 32;
}
return false;
diff --git a/src/Security/Token/IntegrationTokenChecker.php b/src/Security/Token/IntegrationTokenChecker.php
index c8a9ce93..683dcbeb 100644
--- a/src/Security/Token/IntegrationTokenChecker.php
+++ b/src/Security/Token/IntegrationTokenChecker.php
@@ -7,6 +7,7 @@
use Doctrine\Persistence\ManagerRegistry;
use Packeton\Entity\OAuthIntegration;
use Packeton\Model\AutoHookUser;
+use Packeton\Model\PatUserScores;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAccountStatusException;
@@ -42,7 +43,7 @@ public function loadUserByToken(string $username, string $token, callable $userL
$apiToken = $token ? $this->registry->getRepository(OAuthIntegration::class)->findOneBy(['hookSecret' => $token]) : null;
if ($apiToken instanceof OAuthIntegration) {
- return new AutoHookUser();
+ return new AutoHookUser($apiToken->getId());
}
throw new BadCredentialsException('Bad credentials');
@@ -54,7 +55,7 @@ public function loadUserByToken(string $username, string $token, callable $userL
public function checkAccess(Request $request, UserInterface $user = null): void
{
$route = $request->attributes->get('_route');
- if ($route !== 'api_integration_postreceive') {
+ if ($route !== 'api_integration_postreceive' && !PatUserScores::isAllowed('webhooks', $route)) {
throw new CustomUserMessageAccountStatusException('Integration access token allowed only for web hooks route "api_integration_postreceive"');
}
}
diff --git a/src/Security/Token/PatTokenChecker.php b/src/Security/Token/PatTokenChecker.php
index 3432b6ae..eee2d41d 100644
--- a/src/Security/Token/PatTokenChecker.php
+++ b/src/Security/Token/PatTokenChecker.php
@@ -86,8 +86,7 @@ public function checkAccess(Request $request, UserInterface $user): void
throw new BadCredentialsException('Bad credentials.');
}
- $allowed = PatUserScores::getAllowedRoutes($user->getScores());
- if (!in_array($route, $allowed, true)) {
+ if (!PatUserScores::isAllowed($user->getScores(), $route)) {
throw new CustomUserMessageAccountStatusException('This access token does not grant access for this route');
}
}
diff --git a/src/Twig/PackagistExtension.php b/src/Twig/PackagistExtension.php
index 1e791b57..50d2923c 100644
--- a/src/Twig/PackagistExtension.php
+++ b/src/Twig/PackagistExtension.php
@@ -188,8 +188,8 @@ public function getZipballs()
}, $data);
}
- public function generateGravatarHash($email)
+ public function generateGravatarHash($email): string
{
- return md5(strtolower($email));
+ return md5(strtolower((string)$email));
}
}
diff --git a/templates/integration/connect.html.twig b/templates/integration/connect.html.twig
new file mode 100644
index 00000000..b76b0d7a
--- /dev/null
+++ b/templates/integration/connect.html.twig
@@ -0,0 +1,45 @@
+{% extends "layout.html.twig" %}
+
+{% block title %}Integration Connect - {{ parent() }}{% endblock %}
+
+{% block content %}
+
+
+
Available integrations
+
+
+ To use OAuth2 API synchronization with on-premises versions of GitHub, Bitbucket, Gitea or GitLab you need
+ Authorizing Packeton APP with your account remote VCS account.
+ You can also use them to login to Packeton via OAuth2.
+
+
+
+ Packeton use Symfony Http client, if you need to change http preference you may configure it globally
+ https://symfony.com/doc/current/http_client.html
+
+
+
+ {% for integration in integrations %}
+ {% set config = integration.getConfig(null, true) %}
+
+
+
+ {% if config.logo %}
+
+ {% endif %}
+
{{ config.title ?? config.name|capitalize }}
+
+ Redirect Urls
+
+
+
+
+
+
+ {% endfor %}
+
+
+
+{% endblock %}
diff --git a/templates/integration/index.html.twig b/templates/integration/index.html.twig
index 1e57f91c..352f231d 100644
--- a/templates/integration/index.html.twig
+++ b/templates/integration/index.html.twig
@@ -61,19 +61,44 @@
{% for org in orgs %}
-
+ {% set isConn = oauth.isConnected(org['identifier']) %}
+ {% set webhookInfo = oauth.getWebhookInfo(org['identifier']) %}
+