From 84bdf93706c060d2e5f8e842b135523b276b8687 Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Fri, 24 Feb 2023 04:27:50 +0100 Subject: [PATCH] Allow to configure mirror public access --- .env | 3 + README.md | 42 +++++---- config/packages/framework.yaml | 1 + config/packages/nelmio_security.yaml | 3 +- config/packages/security.yaml | 3 +- config/services.yaml | 1 + docker-compose.yml | 2 + docs/img/mirr3.png | Bin 0 -> 45996 bytes docs/usage/mirroring.md | 69 +++++++++----- src/Composer/Cache/MetadataCache.php | 48 ++++++++++ src/Controller/MirrorController.php | 22 +++-- src/DependencyInjection/Configuration.php | 2 + src/DependencyInjection/PacketonExtension.php | 3 + src/Mirror/Model/MetadataOptions.php | 5 ++ src/Mirror/Model/ProxyOptions.php | 8 ++ src/Mirror/Service/ComposeProxyRegistry.php | 6 ++ src/Model/PackageManager.php | 34 ++----- src/Security/Acl/AnonymousVoter.php | 7 +- templates/proxies/mirror.html.twig | 84 +++++++++++++++--- 19 files changed, 257 insertions(+), 86 deletions(-) create mode 100644 docs/img/mirr3.png create mode 100644 src/Composer/Cache/MetadataCache.php diff --git a/.env b/.env index ab4e3c26..14e03c38 100644 --- a/.env +++ b/.env @@ -50,3 +50,6 @@ APP_COMPOSER_HOME="%kernel.project_dir%/var/.composer" ###> nelmio/cors-bundle ### CORS_ALLOW_ORIGIN='^https?://(localhost|127\.0\.0\.1)(:[0-9]+)?$' ###< nelmio/cors-bundle ### + +TRUSTED_HOSTS= +TRUSTED_PROXIES=172.16.0.0/12 diff --git a/README.md b/README.md index 526af80c..3b4beb6d 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,9 @@ Features -------- - Compatible with Composer API v2, bases on Symfony 5.4. -- Support update webhook for GitHub, Bitbucket and GitLab or custom format. +- Support update webhook for GitHub, Gitea, Bitbucket and GitLab or custom format. - Customers user and ACL groups and limit access by vendor and versions. -- Composer Proxies. [docs](docs/usage/mirroring.md) +- Composer Proxies and Mirroring. - Generic Packeton [webhooks](docs/webhook.md) - Allow to freeze updates for the new releases after expire a customers license. - Mirroring for packages zip files and downloads it's from your host. @@ -54,6 +54,7 @@ Table of content - [Bitbucket](#bitbucket-webhooks) - [Manual hook](#manual-hook-setup) - [Custom webhook format](#custom-webhook-format-transformer) +- [Mirroring Composer repos](docs/usage/mirroring.md) - [Usage](#usage-and-authentication) - [Create admin user](#create-admin-user) @@ -104,6 +105,7 @@ docker-compose up -f docker-compose-prod.yml -d # Or split - `REDIS_URL` - Redis DB, default redis://localhost - `PACKAGIST_DIST_HOST` - Hostname, (auto) default use the current host header in the request. - `TRUSTED_PROXIES` - Ips for Reverse Proxy. See [Symfony docs](https://symfony.com/doc/current/deployment/proxies.html) +- `TRUSTED_HOSTS` - Trusted host, set if you've enabled public access and your nginx configuration uses without `server_name`. Otherwise, possible the DDoS attack with generated a big cache size for each host. - `PUBLIC_ACCESS` - Allow anonymous users access to read packages metadata, default: `false` - `MAILER_DSN` - Mailter for reset password, default disabled - `MAILER_FROM` - Mailter from @@ -246,8 +248,16 @@ it would with any other git repository. You can enable it again with env option Update Webhooks --------------- -You can use GitLab, GitHub, and Bitbucket project post-receive hook to keep your packages up to date -every time you push code. +You can use GitLab, Gitea, GitHub, and Bitbucket project post-receive hook to keep your packages up to date +every time you push code. More simple way use group webhooks, to prevent from being added it per each repository manually. + +| Provider | Group webhook support | Target Path | +|-----------|-----------------------|-----------------------------------------------------------| +| GitHub | Yes | `https://example.org/api/github?token=` | +| GitLab | Only paid plan | `https://example.org/api/update-package?token=` | +| Gitea | Yes | `https://example.org/api/update-package?token=` | +| Bitbucket | Yes | `https://example.org/api/bitbucket?token=` | +| Custom | - | `https://example.org/api/update-package/{packnam}?token=` | #### Bitbucket Webhooks To enable the Bitbucket web hook, go to your BitBucket repository, @@ -273,11 +283,11 @@ Enter `https:///api/update-package?token=user:token` as URL. To enable the GitHub webhook go to your GitHub repository. Click the "Settings" button, click "Webhooks". Add a new hook. Enter `https:///api/github?token=user:token` as URL. -#### Manual hook setup +#### Manual or other hook setup If you do not use Bitbucket or GitHub there is a generic endpoint you can call manually from a git post-receive hook or similar. You have to do a POST request to -`https://pkg.okvpn.org/api/update-package?token=user:api_token` with a request body looking like this: +`https://example.org/api/update-package?token=user:api_token` with a request body looking like this: ``` { @@ -287,24 +297,18 @@ from a git post-receive hook or similar. You have to do a POST request to } ``` -Also, you can overwrite regex that was used to parse the repository url, -see [ApiController](src/Controller/ApiController.php#L348) +It will be works with Gitea by default. + +Also, you can use package name in path parameter, see [ApiController](src/Controller/ApiController.php#L78) ``` -{ - "repository": { - "url": "PACKAGIST_PACKAGE_URL" - }, - "packeton": { - "regex": "{^(?:ssh://git@|https?://|git://|git@)?(?P[a-z0-9.-]+)(?::[0-9]+/|[:/])(scm/)?(?P[\\w.-]+(?:/[\\w.-]+?)+)(?:\\.git|/)?$}i" - } -} +https://example.org/api/update-package/acme/packet1?token= ``` You can do this using curl for example: ``` -curl -XPOST -H 'content-type:application/json' 'https://pkg.okvpn.org/api/update-package?token=user:api_token' -d' {"repository":{"url":"PACKAGIST_PACKAGE_URL"}}' +curl -XPOST -H 'content-type:application/json' 'https://example.org/api/update-package?token=user:api_token' -d' {"repository":{"url":"PACKAGIST_PACKAGE_URL"}}' ``` Instead of using repo url you can use directly composer package name. @@ -329,7 +333,7 @@ You have to do a POST request with a request body. #### Custom webhook format transformer You can create a proxy middleware to transform JSON payload to the applicable inner format. -In first you need create a new Rest Endpoint to accept external request. +In the first you need create a new Rest Endpoint to accept external request. Go to `Settings > Webhooks` and click `Add webhook`. Fill the form: - url - `https:///api/update-package?token=user:token` @@ -386,7 +390,7 @@ The customer users can only see related packages and own profile with instructio To authenticate composer access to repository needs add credentials globally into auth.json, for example: ``` -composer config --global --auth http-basic.pkg.okvpn.org +composer config --global --auth http-basic.example.org ``` API Token you can found in your Profile. diff --git a/config/packages/framework.yaml b/config/packages/framework.yaml index e3fc8525..d492e18f 100644 --- a/config/packages/framework.yaml +++ b/config/packages/framework.yaml @@ -5,6 +5,7 @@ framework: http_method_override: false trusted_proxies: '%env(TRUSTED_PROXIES)%' trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-proto', 'x-forwarded-port', 'x-forwarded-prefix'] + trusted_hosts: '%env(TRUSTED_HOSTS)%' cache: pools: diff --git a/config/packages/nelmio_security.yaml b/config/packages/nelmio_security.yaml index d2e91c9f..49823736 100644 --- a/config/packages/nelmio_security.yaml +++ b/config/packages/nelmio_security.yaml @@ -24,7 +24,8 @@ nelmio_security: - 'self' script-src: - 'self' - - 'https://cdn.jsdelivr.net' + - 'https://cdn.jsdelivr.net/npm/d3@3.5.17/' + - 'https://cdn.jsdelivr.net/npm/nvd3@1.8.6/' connect-src: - 'self' img-src: diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 833ad809..67d5f0f1 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -60,7 +60,8 @@ security: # Packagist - { path: (^(/change-password|/profile|/logout))+, roles: ROLE_USER } - { path: (^(/search|/packages/|/versions/))+, roles: ROLE_USER, allow_if: "is_granted('PACKETON_PUBLIC')" } - - { path: (^(/packages.json$|/p/|/p2/|/mirror/|/downloads/))+, roles: ROLE_USER, allow_if: "is_granted('PACKETON_PUBLIC')" } + - { path: ^/mirror/, roles: ROLE_USER, allow_if: "is_granted('PACKETON_MIRROR_PUBLIC')" } + - { path: (^(/packages.json$|/p/|/p2/|/downloads/))+, roles: ROLE_USER, allow_if: "is_granted('PACKETON_PUBLIC')" } - { path: (^(/zipball/))+, roles: ROLE_USER, allow_if: "is_granted('PACKETON_ARCHIVE_PUBLIC')" } - { path: (^(/api/webhook-invoke/))+, roles: ROLE_USER } - { path: (^(/api/(create-package|update-package|github|bitbucket)|/apidoc|/about))$, roles: ROLE_MAINTAINER } diff --git a/config/services.yaml b/config/services.yaml index 0c14a87a..eb2e5626 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -98,6 +98,7 @@ services: arguments: $isAnonymousAccess: '%anonymous_access%' $isAnonymousArchiveAccess: '%anonymous_archive_access%' + $isAnonymousMirror: '%anonymous_mirror_access%' Packeton\Service\DistConfig: arguments: diff --git a/docker-compose.yml b/docker-compose.yml index b1b81d5e..cad57e28 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,8 @@ services: 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" ports: - '127.0.0.1:8088:80' volumes: diff --git a/docs/img/mirr3.png b/docs/img/mirr3.png new file mode 100644 index 0000000000000000000000000000000000000000..3b3452b29ab22059a1bdbbf8b0c07c9bbe1c018e GIT binary patch literal 45996 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 Packagist (or any name) -> Settings +To enable strict mode, go to the Proxy Settings page and select Composer Proxies -> Packagist (or any other name) -> Settings. [![strict](../img/mirr1.png)](../img/mirr1.png) -Next go to view proxy page and click "Mass mirror packages" button +Next, go to the View Proxy page and click the "Mass Mirror Packages" button. [![strict](../img/mirr2.png)](../img/mirr2.png) + +## Mirror Public Access + +Use the following configuration: + +```yaml +packeton: + mirrors: + youname: + url: https://repo.example.org + public_access: true +``` + +[![strict](../img/mirr2.png)](../img/mirr3.png) diff --git a/src/Composer/Cache/MetadataCache.php b/src/Composer/Cache/MetadataCache.php new file mode 100644 index 00000000..709aef01 --- /dev/null +++ b/src/Composer/Cache/MetadataCache.php @@ -0,0 +1,48 @@ +requestStack->getMainRequest()?->getSchemeAndHttpHost(); + + $cacheKey = sha1($key . $httpKey); + $item = $this->packagesCachePool->getItem($cacheKey); + @[$ctime, $data] = $item->get(); + + $needRefresh = false; + if ($lastModify !== null) { + $needRefresh = $ctime < $lastModify || $ctime + $this->maxTtl < time(); + } + + if (!$item->isHit() || $needRefresh || empty($data)) { + $data = $callback($item); + + $item->set([time(), $data]); + $this->packagesCachePool->save($item); + } + + return $data; + } + + public function delete(string $key): bool + { + return $this->packagesCachePool->delete($key); + } +} diff --git a/src/Controller/MirrorController.php b/src/Controller/MirrorController.php index 250a82bd..869033a3 100644 --- a/src/Controller/MirrorController.php +++ b/src/Controller/MirrorController.php @@ -34,14 +34,14 @@ public function index(string $alias): Response { try { $this->checkAccess($alias); - $this->proxyRegistry->createRepository($alias); + $config = $this->proxyRegistry->getProxyConfig($alias); } catch (MetadataNotFoundException $e) { throw $this->createNotFoundException($e->getMessage(), $e); } $repo = $this->generateUrl('mirror_index', ['alias' => $alias], UrlGeneratorInterface::ABSOLUTE_URL); - return $this->render('proxies/mirror.html.twig', ['alias' => $alias, 'repoUrl' => $repo]); + return $this->render('proxies/mirror.html.twig', ['alias' => $alias, 'repoUrl' => $repo, 'repo' => $config]); } #[Route('/{alias}/packages.json', name: 'mirror_root', methods: ['GET'])] @@ -136,7 +136,6 @@ protected function wrap404Error(string $alias, callable $callback): JsonMetadata try { $this->checkAccess($alias); $repo = $this->proxyRegistry->createACLAwareRepository($alias); - return $callback($repo); } catch (MetadataNotFoundException $e) { throw $this->createNotFoundException($e->getMessage(), $e); @@ -145,9 +144,20 @@ protected function wrap404Error(string $alias, callable $callback): JsonMetadata protected function checkAccess(string $alias) { - // ROLE_ADMIN have access to all proxies views - if (!$this->isGranted('ROLE_ADMIN') && !$this->isGranted('VIEW', new ObjectIdentity($alias, PRI::class))) { - throw $this->createAccessDeniedException(); + try { + $config = $this->proxyRegistry->getProxyConfig($alias); + } catch (MetadataNotFoundException) { + throw $this->createNotFoundException(); + } + + if (false === $config->isPublicAccess()) { + if (null !== $this->getUser()) { + if (!$this->isGranted('ROLE_MAINTAINER') && !$this->isGranted('VIEW', new ObjectIdentity($alias, PRI::class))) { + throw $this->createAccessDeniedException(); + } + } else { + throw $this->createNotFoundException(); + } } } } diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index 11102708..41e1968f 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -109,8 +109,10 @@ private function addMirrorsRepositoriesConfiguration(ArrayNodeDefinition|NodeDef ->booleanNode('enable_dist_mirror')->defaultTrue()->end() ->booleanNode('parent_notify')->end() ->booleanNode('disable_v1')->end() + ->booleanNode('public_access')->end() ->variableNode('git_ssh_keys')->end() ->scalarNode('info_cmd_message')->end() + ->scalarNode('without_path_prefix')->end() ->scalarNode('logo')->end() ->integerNode('available_packages_count_limit')->end() ->arrayNode('available_package_patterns') diff --git a/src/DependencyInjection/PacketonExtension.php b/src/DependencyInjection/PacketonExtension.php index bde023d5..758fd01a 100644 --- a/src/DependencyInjection/PacketonExtension.php +++ b/src/DependencyInjection/PacketonExtension.php @@ -25,6 +25,9 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('packeton_archive_opts', $config['archive_options'] ?? []); } + $hasPublicMirror = array_filter($config['mirrors'] ?? [] , fn ($i) => $i['public_access'] ?? false); + $container->setParameter('anonymous_mirror_access', (bool) $hasPublicMirror); + $container->setParameter('anonymous_access', $config['anonymous_access'] ?? false); $container->setParameter('anonymous_archive_access', $config['anonymous_archive_access'] ?? false); diff --git a/src/Mirror/Model/MetadataOptions.php b/src/Mirror/Model/MetadataOptions.php index 3cb88c81..89452152 100644 --- a/src/Mirror/Model/MetadataOptions.php +++ b/src/Mirror/Model/MetadataOptions.php @@ -64,6 +64,11 @@ public function getIncludes(): ?array return $includes; } + public function withoutPathPrefix(): bool + { + return $this->config['without_path_prefix'] ?? false; + } + public function getAvailablePackages(): array { return $this->config['available_packages'] ?? []; diff --git a/src/Mirror/Model/ProxyOptions.php b/src/Mirror/Model/ProxyOptions.php index 3bb0a6ff..bc237f99 100644 --- a/src/Mirror/Model/ProxyOptions.php +++ b/src/Mirror/Model/ProxyOptions.php @@ -162,6 +162,14 @@ public function getAuthBasic(): ?array return $this->config['http_basic'] ?? null; } + /** + * @return bool + */ + public function isPublicAccess(): bool + { + return $this->config['public_access'] ?? false; + } + /** * @return array|null */ diff --git a/src/Mirror/Service/ComposeProxyRegistry.php b/src/Mirror/Service/ComposeProxyRegistry.php index 80dd60d1..73b7dd3a 100644 --- a/src/Mirror/Service/ComposeProxyRegistry.php +++ b/src/Mirror/Service/ComposeProxyRegistry.php @@ -8,6 +8,7 @@ use Packeton\Mirror\Decorator\ProxyRepositoryACLDecorator; use Packeton\Mirror\Decorator\ProxyRepositoryFacade; use Packeton\Mirror\Exception\MetadataNotFoundException; +use Packeton\Mirror\Model\ProxyOptions; use Packeton\Mirror\Model\StrictProxyRepositoryInterface as PRI; use Packeton\Mirror\ProxyRepositoryRegistry; use Packeton\Mirror\RemoteProxyRepository; @@ -30,6 +31,11 @@ public function createRepository(string $name): PRI return new ProxyRepositoryFacade($repo, $this->syncService, $this->metadataMinifier); } + public function getProxyConfig(string $name): ProxyOptions + { + return $this->getRemoteProxyRepository($name)->getConfig(); + } + public function createACLAwareRepository(string $name): PRI { $repo = $this->getRemoteProxyRepository($name); diff --git a/src/Model/PackageManager.php b/src/Model/PackageManager.php index b937622e..07241ae0 100644 --- a/src/Model/PackageManager.php +++ b/src/Model/PackageManager.php @@ -5,6 +5,7 @@ namespace Packeton\Model; use Doctrine\Persistence\ManagerRegistry; +use Packeton\Composer\Cache\MetadataCache; use Packeton\Composer\MetadataMinifier; use Packeton\Composer\PackagistFactory; use Packeton\Entity\User; @@ -19,7 +20,6 @@ use Symfony\Component\Mime\Email; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; use Symfony\Component\Security\Core\User\UserInterface; -use Symfony\Contracts\Cache\CacheInterface; use Twig\Environment; class PackageManager @@ -34,7 +34,7 @@ public function __construct( protected AuthorizationCheckerInterface $authorizationChecker, protected PackagistFactory $packagistFactory, protected EventDispatcherInterface $dispatcher, - protected CacheInterface $packagesCachePool, + protected MetadataCache $cache, protected MetadataMinifier $metadataMinifier, protected \Redis $redis, ) {} @@ -206,39 +206,17 @@ private function dumpInMemory(UserInterface $user = null, bool $ignoreLastModify $user = null; } - $cacheKey = \sha1('pkg_user_cache_' . ($user ? $user->getUserIdentifier() : 0)); + $cacheKey = 'pkg_user_cache_' . ($user ? $user->getUserIdentifier() : 0); - $item = $this->packagesCachePool->getItem($cacheKey); - @[$ctime, $data] = $item->get(); - - $needRefresh = false; - if (false === $ignoreLastModify) { - $lastModify = $this->getLastModify()?->getTimestamp() ?: 0; - $needRefresh = $ctime < $lastModify || $ctime + 1800 < time(); // 1800 sec max ttl for root package request, but default TTL = 3600 - } - - if (!$item->isHit() || $needRefresh || empty($data)) { - $data = $this->dumper->dump($user); - $item->set([time(), $data]); - $this->packagesCachePool->save($item); - } - - return $data; + $lastModify = false === $ignoreLastModify ? ($this->getLastModify()?->getTimestamp() ?: 0) : null; + return $this->cache->get($cacheKey, fn () => $this->dumper->dump($user), $lastModify); } public function getPackageNames(): array { $cacheKey = 'all_packages_'; $lastModify = $this->getLastModify()?->getTimestamp(); - - $item = $this->packagesCachePool->getItem($cacheKey); - @[$ctime, $data] = $item->get(); - - if ($ctime < $lastModify || !$item->isHit()) { - $data = $this->doctrine->getRepository(Package::class)->getPackageNames(); - $item->set([time(), $data]); - $this->packagesCachePool->save($item); - } + $data = $this->cache->get($cacheKey, fn () => $this->doctrine->getRepository(Package::class)->getPackageNames(), $lastModify); return is_array($data) ? $data : []; } diff --git a/src/Security/Acl/AnonymousVoter.php b/src/Security/Acl/AnonymousVoter.php index f5a39f6f..2fcfd236 100644 --- a/src/Security/Acl/AnonymousVoter.php +++ b/src/Security/Acl/AnonymousVoter.php @@ -9,7 +9,8 @@ class AnonymousVoter implements VoterInterface { public function __construct( private readonly bool $isAnonymousAccess, - private readonly bool $isAnonymousArchiveAccess + private readonly bool $isAnonymousArchiveAccess, + private readonly bool $isAnonymousMirror, ){} public function vote(TokenInterface $token, $subject, array $attributes): int @@ -24,6 +25,10 @@ public function vote(TokenInterface $token, $subject, array $attributes): int return self::ACCESS_GRANTED; } + if (true === $this->isAnonymousMirror && in_array('PACKETON_MIRROR_PUBLIC', $attributes, true)) { + return self::ACCESS_GRANTED; + } + return self::ACCESS_ABSTAIN; } } diff --git a/templates/proxies/mirror.html.twig b/templates/proxies/mirror.html.twig index 31fc6c09..e7e56615 100644 --- a/templates/proxies/mirror.html.twig +++ b/templates/proxies/mirror.html.twig @@ -1,10 +1,74 @@ -{% extends "layout.html.twig" %} - -{% block content %} -

Mirror {{ alias }}

-
-
- {% include 'proxies/info_repo.html.twig' %} -
-
-{% endblock %} + + + + + + {{ alias|capitalize }} Mirror + + + + + + + + + + +
+
+
+
+

+ {{ alias|capitalize }} Mirror + {% if repo.logo %} + Composer Proxy + {% endif %} +

+ +
+

+ This is PHP package repository {{ repo.alias }} mirror site. +

+

+ If you're using PHP Composer, commands like create-project, require, update, remove are often used. + When those commands are executed, Composer will download information from the packages that are needed also from dependent packages. The number of json files downloaded depends on the complexity of the packages which are going to be used. + The further you are from the location of the packagist.org server, the more time is needed to download json files. By using mirror, it will help save the time for downloading because the server location is closer. +

+

+ Please do the following command to change the PHP Composer config to use this site as default Composer repository. +

+ +
+ {% include 'proxies/info_repo.html.twig' %} +
+
+
+
+ +