From 55382f115c04d0bfc5bc3dad456a1afeac8f484e Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Fri, 8 Nov 2024 00:58:04 +0100 Subject: [PATCH 1/3] Allow public access to some packages via a sub-repository --- src/Controller/ZipballController.php | 6 +++++ src/Entity/SubRepository.php | 14 ++++++++++ src/EventListener/ProtectHostListener.php | 5 ++-- src/Form/Type/SubRepositoryType.php | 5 ++++ src/Model/PatUserScores.php | 2 +- src/Package/InMemoryDumper.php | 2 +- src/Repository/SubEntityRepository.php | 2 +- src/Security/Acl/SubRepoGrantVoter.php | 31 ++++++++++++++++++++--- src/Service/SubRepositoryHelper.php | 10 +++++--- 9 files changed, 66 insertions(+), 11 deletions(-) diff --git a/src/Controller/ZipballController.php b/src/Controller/ZipballController.php index 72be2d84..4c6d2e0e 100644 --- a/src/Controller/ZipballController.php +++ b/src/Controller/ZipballController.php @@ -85,6 +85,12 @@ public function zipballList(Request $request): Response requirements: ['package' => '%package_name_regex%', 'hash' => '[a-f0-9]{40}(\.?[A-Za-z\.]+?)?'], methods: ['GET'] )] + #[Route( + '/{slug}/zipball/{package}/{hash}', + name: 'download_dist_package_slug', + requirements: ['package' => '%package_name_regex%', 'hash' => '[a-f0-9]{40}(\.?[A-Za-z\.]+?)?'], + methods: ['GET'] + )] public function zipballAction(#[Vars('name')] Package $package, string $hash): Response { if ((false === $this->dm->isEnabled() && false === RepTypes::isBuildInDist($package->getRepoType())) diff --git a/src/Entity/SubRepository.php b/src/Entity/SubRepository.php index e7177217..a4dab603 100644 --- a/src/Entity/SubRepository.php +++ b/src/Entity/SubRepository.php @@ -32,6 +32,9 @@ class SubRepository #[ORM\Column(type: 'json', nullable: true)] private ?array $packages = null; + #[ORM\Column(name: 'public_access', type: 'boolean', nullable: true)] + private ?bool $publicAccess = null; + /** @internal */ private ?array $cachedIds = null; @@ -129,4 +132,15 @@ public function setCachedIds(?array $cachedIds): static $this->cachedIds = $cachedIds; return $this; } + + public function isPublicAccess(): ?bool + { + return $this->publicAccess; + } + + public function setPublicAccess(?bool $publicAccess): static + { + $this->publicAccess = $publicAccess; + return $this; + } } diff --git a/src/EventListener/ProtectHostListener.php b/src/EventListener/ProtectHostListener.php index 0a5fa6f4..72ec40a1 100644 --- a/src/EventListener/ProtectHostListener.php +++ b/src/EventListener/ProtectHostListener.php @@ -21,11 +21,12 @@ class ProtectHostListener 'root_package_v2' => 1, 'download_dist_package' => 1, 'track_download' => 1, - 'track_download_batch' =>1, - 'root_packages_slug' =>1, + 'track_download_batch' => 1, + 'root_packages_slug' => 1, 'root_providers_slug' => 1, 'root_package_slug' => 1, 'root_package_v2_slug' => 1, + 'download_dist_package_slug' => 1, 'mirror_root' => 1, 'mirror_metadata_v2' => 1, 'mirror_metadata_v1' => 1, diff --git a/src/Form/Type/SubRepositoryType.php b/src/Form/Type/SubRepositoryType.php index 7b1b0035..f34ea306 100644 --- a/src/Form/Type/SubRepositoryType.php +++ b/src/Form/Type/SubRepositoryType.php @@ -8,6 +8,7 @@ use Packeton\Entity\Package; use Packeton\Entity\SubRepository; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\Extension\Core\Type\TextType; @@ -52,6 +53,10 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => 'Subdomain or separate hostname', 'attr' => ['placeholder' => "e.g.: repo1.example.com\nrepo2.example.com", 'rows' => 4] ]) + ->add('publicAccess', CheckboxType::class, [ + 'required' => false, + 'label' => 'Allow public access', + ]) ->add('packages', ChoiceType::class, [ 'required' => false, 'multiple' => true, diff --git a/src/Model/PatUserScores.php b/src/Model/PatUserScores.php index 4b77f606..a8e2cfbb 100644 --- a/src/Model/PatUserScores.php +++ b/src/Model/PatUserScores.php @@ -10,7 +10,7 @@ class PatUserScores 'metadata' => [ 'root_packages', 'root_providers', 'metadata_changes', 'root_package', 'root_package_v2', 'download_dist_package', 'track_download', 'track_download_batch', - 'root_packages_slug', 'root_providers_slug', 'root_package_slug', 'root_package_v2_slug', + 'root_packages_slug', 'root_providers_slug', 'root_package_slug', 'root_package_v2_slug', 'download_dist_package_slug' ], 'mirror:read' => ['mirror_root', 'mirror_metadata_v2', 'mirror_metadata_v1', 'mirror_zipball', 'mirror_provider_includes'], 'mirror:all' => ['@mirror:read'], diff --git a/src/Package/InMemoryDumper.php b/src/Package/InMemoryDumper.php index c738f5ca..bca1e02f 100644 --- a/src/Package/InMemoryDumper.php +++ b/src/Package/InMemoryDumper.php @@ -107,7 +107,7 @@ private function dumpRootPackages(?UserInterface $user = null, ?int $apiVersion if ($this->distConfig->mirrorEnabled()) { $ref = '0000000000000000000000000000000000000000.zip'; - $zipball = $this->router->generate('download_dist_package', ['package' => 'VND/PKG', 'hash' => $ref]); + $zipball = $slug . $this->router->generate('download_dist_package', ['package' => 'VND/PKG', 'hash' => $ref]); $rootFile['mirrors'][] = ['dist-url' => \str_replace(['VND/PKG', $ref], ['%package%', '%reference%.%type%'], $zipball), 'preferred' => true]; } diff --git a/src/Repository/SubEntityRepository.php b/src/Repository/SubEntityRepository.php index fa361d6f..a38d37d1 100644 --- a/src/Repository/SubEntityRepository.php +++ b/src/Repository/SubEntityRepository.php @@ -11,7 +11,7 @@ public function getSubRepositoryData(): array { $data = $this->createQueryBuilder('s') ->resetDQLPart('select') - ->select(['s.id', 's.slug', 's.urls', 's.name']) + ->select(['s.id', 's.slug', 's.urls', 's.name', 's.publicAccess as public']) ->getQuery() ->getArrayResult(); diff --git a/src/Security/Acl/SubRepoGrantVoter.php b/src/Security/Acl/SubRepoGrantVoter.php index d217a5c6..2f071dc6 100644 --- a/src/Security/Acl/SubRepoGrantVoter.php +++ b/src/Security/Acl/SubRepoGrantVoter.php @@ -4,6 +4,7 @@ namespace Packeton\Security\Acl; +use Packeton\Service\SubRepositoryHelper; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Authorization\Voter\CacheableVoterInterface; @@ -15,15 +16,21 @@ class SubRepoGrantVoter implements CacheableVoterInterface 'root_providers_slug' => 1, 'root_package_slug' => 1, 'root_package_v2_slug' => 1, + 'download_dist_package_slug' => 1, ]; + public function __construct( + private readonly SubRepositoryHelper $helper + ) { + } + /** * {@inheritdoc} */ - public function vote(TokenInterface $token, mixed $subject, array $attributes): int + public function vote(TokenInterface $token, mixed $request, array $attributes): int { - if ($subject instanceof Request && isset(self::$subRoutes[$subject->attributes->get('_route')])) { - return $token->getUser() ? self::ACCESS_GRANTED : self::ACCESS_ABSTAIN; + if ($request instanceof Request && isset(self::$subRoutes[$request->attributes->get('_route')])) { + return $token->getUser() || $this->isPublicSubRepo($request) ? self::ACCESS_GRANTED : self::ACCESS_ABSTAIN; } return self::ACCESS_ABSTAIN; @@ -44,4 +51,22 @@ public function supportsType(string $subjectType): bool { return $subjectType === Request::class; } + + private function isPublicSubRepo(Request $request): bool + { + if (null !== ($subRepo = $this->getSubRepoForRequest($request))) { + return $this->helper->isPublicAccess($subRepo); + } + return false; + } + + private function getSubRepoForRequest(Request $request): ?int + { + $route = (string) $request->attributes->get('_route'); + if ($request->attributes->has('slug') && (SubRepoGrantVoter::$subRoutes[$route] ?? null)) { + return $this->helper->getBySlug($request->attributes->get('slug')); + } + + return $this->helper->getByHost($request->getHost()); + } } diff --git a/src/Service/SubRepositoryHelper.php b/src/Service/SubRepositoryHelper.php index b077ade8..503baf22 100644 --- a/src/Service/SubRepositoryHelper.php +++ b/src/Service/SubRepositoryHelper.php @@ -90,6 +90,12 @@ public function isAutoHost(): bool return $req->attributes->get('_sub_repo_type') === SubRepository::AUTO_HOST; } + public function isPublicAccess(?int $subRepo = null): bool + { + $subRepo ??= $this->getSubrepositoryId(); + return $this->getData()[$subRepo]['public'] ?? false; + } + public static function applyCondition(QueryBuilder $qb, ?array $allowed): QueryBuilder { if ($allowed === null) { @@ -161,8 +167,6 @@ public function getTwigData(?UserInterface $user = null): array protected function getData(): array { - return $this->cache->get('sub_repos_list', function () { - return $this->registry->getRepository(SubRepository::class)->getSubRepositoryData(); - }); + return $this->cache->get('sub_repos_list', fn () => $this->registry->getRepository(SubRepository::class)->getSubRepositoryData()); } } From 1793b4480951063debc1c71dc0e8845778e60f13 Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Sat, 9 Nov 2024 14:29:28 +0100 Subject: [PATCH 2/3] Allow public access to some packages via a sub-repository --- src/Controller/Api/ApiController.php | 14 +++- src/Controller/ProviderController.php | 7 +- src/Controller/SubRepoControllerTrait.php | 14 ++++ src/Controller/WebController.php | 17 +++++ src/Controller/ZipballController.php | 22 ++++-- src/Entity/SubRepository.php | 14 ++++ src/EventListener/PackagistListener.php | 8 ++- src/EventListener/ProtectHostListener.php | 2 + src/EventListener/SubRepositoryListener.php | 72 +++++++++++++++++-- src/Form/Type/SubRepositoryType.php | 6 +- src/Model/PackageManager.php | 4 +- src/Model/PatUserScores.php | 3 +- src/Security/Acl/SubRepoGrantVoter.php | 24 ++++++- src/Service/SubRepositoryHelper.php | 22 ++++++ templates/subrepository/public.html.twig | 77 +++++++++++++++++++++ 15 files changed, 280 insertions(+), 26 deletions(-) create mode 100644 src/Controller/SubRepoControllerTrait.php create mode 100644 templates/subrepository/public.html.twig diff --git a/src/Controller/Api/ApiController.php b/src/Controller/Api/ApiController.php index 17d4465d..30ea8c9b 100644 --- a/src/Controller/Api/ApiController.php +++ b/src/Controller/Api/ApiController.php @@ -20,6 +20,7 @@ use Packeton\Security\Provider\AuditSessionProvider; use Packeton\Service\JobPersister; use Packeton\Service\Scheduler; +use Packeton\Service\SubRepositoryHelper; use Packeton\Util\PacketonUtils; use Packeton\Webhook\HookBus; use Psr\Log\LoggerInterface; @@ -43,6 +44,7 @@ public function __construct( protected ValidatorInterface $validator, protected IntegrationRegistry $integrations, protected AuditSessionProvider $auditSessionProvider, + protected SubRepositoryHelper $subRepositoryHelper, ) { } @@ -189,12 +191,14 @@ public function editPackageAction(Request $request, #[Vars] Package $package): R #[Route('/downloads/{name}', name: 'track_download', requirements: ['name' => '%package_name_regex%'], methods: ['POST'])] + #[Route('/{slug}/downloads/{name}', name: 'track_download_slug', requirements: ['name' => '%package_name_regex%'], methods: ['POST'])] public function trackDownloadAction(Request $request, $name): Response { + $allowed = $this->subRepositoryHelper->allowedPackageIds(); $result = $this->getPackageAndVersionId($name, $request->request->get('version_normalized')); - if (!$result) { - return new JsonResponse(['status' => 'error', 'message' => 'Package not found'], 200); + if (!$result || (null !== $allowed && !in_array($result['id'], $allowed, true))) { + return new JsonResponse(['status' => 'error', 'message' => 'Package not found'], 404); } $this->downloadManager->addDownloads(['id' => $result['id'], 'vid' => $result['vid'], 'ip' => $request->getClientIp()]); @@ -223,6 +227,7 @@ public function getJobAction(string $id): Response * @inheritDoc */ #[Route('/downloads/', name: 'track_download_batch', methods: ['POST'])] + #[Route('/{slug}/downloads/', name: 'track_download_batch_slug', methods: ['POST'])] public function trackDownloadsAction(Request $request): Response { $contents = \json_decode($request->getContent(), true); @@ -234,6 +239,7 @@ public function trackDownloadsAction(Request $request): Response $ip = $request->getClientIp(); $jobs = []; + $allowed = $this->subRepositoryHelper->allowedPackageIds(); foreach ($contents['downloads'] as $package) { $result = $this->getPackageAndVersionId($package['name'], $package['version']); @@ -242,6 +248,10 @@ public function trackDownloadsAction(Request $request): Response continue; } + if (null !== $allowed && !in_array($result['id'], $allowed, true)) { + continue; + } + $audit[] = "{$package['name']}: {$package['version']}"; $jobs[] = ['id' => $result['id'], 'vid' => $result['vid'], 'ip' => $ip]; } diff --git a/src/Controller/ProviderController.php b/src/Controller/ProviderController.php index 6efaa36c..abd05442 100644 --- a/src/Controller/ProviderController.php +++ b/src/Controller/ProviderController.php @@ -21,6 +21,7 @@ class ProviderController extends AbstractController { use ControllerTrait; + use SubRepoControllerTrait; public function __construct( private readonly PackageManager $packageManager, @@ -154,12 +155,6 @@ protected function createNotFound(?string $msg = null): Response return new JsonResponse(['status' => 'error', 'message' => $msg ?: 'Not Found'], 404); } - protected function checkSubrepositoryAccess(string $name): bool - { - $packages = $this->subRepositoryHelper->allowedPackageNames(); - return $packages === null || in_array($name, $packages, true); - } - protected function createJsonResponse(array $data): JsonResponse { $response = new JsonResponse($data); diff --git a/src/Controller/SubRepoControllerTrait.php b/src/Controller/SubRepoControllerTrait.php new file mode 100644 index 00000000..22a4a811 --- /dev/null +++ b/src/Controller/SubRepoControllerTrait.php @@ -0,0 +1,14 @@ +subRepositoryHelper->allowedPackageNames(); + return $packages === null || in_array($name, $packages, true); + } +} diff --git a/src/Controller/WebController.php b/src/Controller/WebController.php index d4fdeef1..c2f06b1d 100644 --- a/src/Controller/WebController.php +++ b/src/Controller/WebController.php @@ -16,6 +16,7 @@ use Doctrine\Persistence\ManagerRegistry; use Packeton\Entity\Group; use Packeton\Entity\Package; +use Packeton\Entity\SubRepository; use Packeton\Entity\Version; use Packeton\Form\Model\SearchQuery; use Packeton\Form\Type\SearchQueryType; @@ -52,6 +53,22 @@ public function indexAction(Request $request): Response ]); } + #[Route('/{slug}', name: 'sub_repository_home', methods: ['GET'], priority: -50)] + public function subRepoAction(Request $request, string $slug): Response + { + $repo = $this->registry->getRepository(SubRepository::class)->findOneBy(['slug' => $slug]); + if (!$repo instanceof SubRepository) { + throw $this->createNotFoundException(); + } + + $isHost = $this->subRepositoryHelper->getByHost($request->getHost()); + + return $this->render('subrepository/public.html.twig', [ + 'repo' => $repo, + 'repoUrl' => $request->getSchemeAndHttpHost() . ($isHost ? '' : '/'.$slug), + ]); + } + /** * Rendered by views/Web/searchSection.html.twig */ diff --git a/src/Controller/ZipballController.php b/src/Controller/ZipballController.php index 4c6d2e0e..8e597115 100644 --- a/src/Controller/ZipballController.php +++ b/src/Controller/ZipballController.php @@ -12,6 +12,7 @@ use Packeton\Model\UploadZipballStorage; use Packeton\Package\RepTypes; use Packeton\Service\DistManager; +use Packeton\Service\SubRepositoryHelper; use Packeton\Util\PacketonUtils; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -27,11 +28,14 @@ #[Route(defaults: ['_format' => 'json'])] class ZipballController extends AbstractController { + use SubRepoControllerTrait; + public function __construct( protected DistManager $dm, protected UploadZipballStorage $storage, protected ManagerRegistry $registry, protected EventDispatcherInterface $dispatcher, + protected SubRepositoryHelper $subRepositoryHelper, protected LoggerInterface $logger, ) { } @@ -94,16 +98,24 @@ public function zipballList(Request $request): Response public function zipballAction(#[Vars('name')] Package $package, string $hash): Response { if ((false === $this->dm->isEnabled() && false === RepTypes::isBuildInDist($package->getRepoType())) - || !\preg_match('{[a-f0-9]{40}}i', $hash, $match) || !($reference = $match[0]) + || !\preg_match('{[a-f0-9]{40}}i', $hash, $match) + || !($reference = $match[0]) + || !$this->checkSubrepositoryAccess($package->getName()) ) { return $this->createNotFound(); } - $isGranted = $this->isGranted('VIEW_ALL_VERSION', $package) || $this->isGranted('ROLE_FULL_CUSTOMER', $package); - foreach ($package->getAllVersionsByReference($reference) as $version) { - $isGranted |= $this->isGranted('ROLE_FULL_CUSTOMER', $version); + $isGranted = $this->subRepositoryHelper->isPublicAccess() + || $this->isGranted('VIEW_ALL_VERSION', $package) + || $this->isGranted('ROLE_FULL_CUSTOMER', $package); + + if (false === $isGranted) { + foreach ($package->getAllVersionsByReference($reference) as $version) { + $isGranted = $isGranted || $this->isGranted('ROLE_FULL_CUSTOMER', $version); + } } - if (!$isGranted) { + + if (true !== $isGranted) { return $this->createNotFound(); } diff --git a/src/Entity/SubRepository.php b/src/Entity/SubRepository.php index a4dab603..6ae06521 100644 --- a/src/Entity/SubRepository.php +++ b/src/Entity/SubRepository.php @@ -35,6 +35,9 @@ class SubRepository #[ORM\Column(name: 'public_access', type: 'boolean', nullable: true)] private ?bool $publicAccess = null; + #[ORM\Column(name: 'html_markup', type: 'text', nullable: true)] + private ?string $htmlMarkup = null; + /** @internal */ private ?array $cachedIds = null; @@ -143,4 +146,15 @@ public function setPublicAccess(?bool $publicAccess): static $this->publicAccess = $publicAccess; return $this; } + + public function getHtmlMarkup(): ?string + { + return $this->htmlMarkup; + } + + public function setHtmlMarkup(?string $htmlMarkup): static + { + $this->htmlMarkup = $htmlMarkup; + return $this; + } } diff --git a/src/EventListener/PackagistListener.php b/src/EventListener/PackagistListener.php index cde14ef8..6ab3dd52 100644 --- a/src/EventListener/PackagistListener.php +++ b/src/EventListener/PackagistListener.php @@ -18,6 +18,7 @@ use Packeton\Event\FormHandlerEvent; use Packeton\Model\ProviderManager; use Packeton\Service\DistConfig; +use Packeton\Service\SubRepositoryHelper; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; @@ -38,7 +39,8 @@ class PackagistListener public function __construct( private readonly RequestStack $requestStack, private readonly ProviderManager $providerManager, - ){ + private readonly SubRepositoryHelper $subRepositoryHelper, + ) { } /** @@ -56,8 +58,10 @@ public function postLoad(Version $version, PostLoadEventArgs $event) $dist = $version->getDist(); if (isset($dist['url']) && \str_starts_with($dist['url'], DistConfig::HOSTNAME_PLACEHOLDER)) { $currentHost = $request->getSchemeAndHttpHost(); + $slug = $this->subRepositoryHelper->getCurrentSlug(); + $replacement = null !== $slug ? $currentHost . '/' . $slug : $currentHost; - $dist['url'] = \str_replace(DistConfig::HOSTNAME_PLACEHOLDER, $currentHost, $dist['url']); + $dist['url'] = \str_replace(DistConfig::HOSTNAME_PLACEHOLDER, $replacement, $dist['url']); $version->distNormalized = $dist; } } diff --git a/src/EventListener/ProtectHostListener.php b/src/EventListener/ProtectHostListener.php index 72ec40a1..ab156758 100644 --- a/src/EventListener/ProtectHostListener.php +++ b/src/EventListener/ProtectHostListener.php @@ -27,6 +27,8 @@ class ProtectHostListener 'root_package_slug' => 1, 'root_package_v2_slug' => 1, 'download_dist_package_slug' => 1, + 'track_download_batch_slug' => 1, + 'track_download_slug' => 1, 'mirror_root' => 1, 'mirror_metadata_v2' => 1, 'mirror_metadata_v1' => 1, diff --git a/src/EventListener/SubRepositoryListener.php b/src/EventListener/SubRepositoryListener.php index 4a117916..9dcd4ee1 100644 --- a/src/EventListener/SubRepositoryListener.php +++ b/src/EventListener/SubRepositoryListener.php @@ -9,18 +9,21 @@ use Packeton\Model\PacketonUserInterface; use Packeton\Security\Acl\SubRepoGrantVoter; use Packeton\Service\SubRepositoryHelper; +use Symfony\Component\DependencyInjection\Attribute\AutowireServiceClosure; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Event\RequestEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Twig\Environment; class SubRepositoryListener { - public static $skipRoutes = [ - 'download_dist_package' => 1, - 'track_download_batch' => 1, - 'track_download' => 1, + private static array $skipRoutes = [ 'login' => 1, 'logout' => 1, 'subrepository_switch' => 1, @@ -28,12 +31,62 @@ class SubRepositoryListener 'api_health' => 1, ]; + private static array $downloadRoutes = [ + 'download_dist_package' => 1, + 'track_download_batch' => 1, + 'track_download' => 1, + ]; + + private static array $loginRoutes = [ + 'login' => 1, + 'change_password' => 1, + 'request_pwd_check_email' => 1, + 'request_pwd_reset' => 1, + 'oauth_login' => 1, + 'oauth_check' => 1, + ]; + public function __construct( protected SubRepositoryHelper $helper, - protected TokenStorageInterface $tokenStorage + protected TokenStorageInterface $tokenStorage, + #[AutowireServiceClosure(service: 'twig')] + protected $twig, ) { } + #[AsEventListener(event: 'kernel.exception', priority: 10)] + public function onKernelException(ExceptionEvent $event): void + { + if (!$event->getThrowable() instanceof AccessDeniedException) { + return; + } + + $request = $event->getRequest(); + $route = (string)$request->attributes->get('_route'); + $isPublicSubRepoHost = null !== ($subRepo = $this->helper->getByHost($request->getHost())) + && $this->helper->isPublicAccess($subRepo) + && null === $this->tokenStorage->getToken()?->getUser(); + + if (!$isPublicSubRepoHost || isset(SubRepoGrantVoter::$rootRoutes[$route])) { + return; + } + + $repo = $this->helper->findSubRepo($subRepo); + if ($route === 'home' && null !== $repo) { + $response = $this->getTwig()->render('subrepository/public.html.twig', ['repo' => $repo]); + $event->setResponse(new Response($response)); + $event->allowCustomResponseCode(); + return; + } + + if (isset(self::$loginRoutes[$route])) { + return; + } + + $event->setResponse(new JsonResponse(['error' => 'Not found'], 404)); + $event->allowCustomResponseCode(); + } + #[AsEventListener(event: 'kernel.request')] public function onKernelRequest(RequestEvent $event): void { @@ -70,6 +123,10 @@ public function onKernelRequest(RequestEvent $event): void $request->attributes->set('_sub_repo_type', !$withSlug ? SubRepository::AUTO_HOST : null); } + if (isset(self::$downloadRoutes[$route])) { + return; + } + if ($user instanceof PacketonUserInterface) { $allowedRepos = $user->getSubRepos() ?: []; $isAdmin = $user instanceof User ? $user->isAdmin() : in_array('ROLE_ADMIN', $token->getRoleNames()); @@ -96,4 +153,9 @@ public function onKernelRequest(RequestEvent $event): void } } } + + private function getTwig(): Environment + { + return ($this->twig)(); + } } diff --git a/src/Form/Type/SubRepositoryType.php b/src/Form/Type/SubRepositoryType.php index f34ea306..a0c8c742 100644 --- a/src/Form/Type/SubRepositoryType.php +++ b/src/Form/Type/SubRepositoryType.php @@ -51,12 +51,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('urls', TextareaType::class, [ 'required' => false, 'label' => 'Subdomain or separate hostname', - 'attr' => ['placeholder' => "e.g.: repo1.example.com\nrepo2.example.com", 'rows' => 4] + 'attr' => ['placeholder' => "e.g.: repo1.example.com\nrepo2.example.com", 'rows' => 4], ]) ->add('publicAccess', CheckboxType::class, [ 'required' => false, 'label' => 'Allow public access', ]) + ->add('htmlMarkup', TextareaType::class, [ + 'required' => false, + 'label' => 'HTML public page markup', + ]) ->add('packages', ChoiceType::class, [ 'required' => false, 'multiple' => true, diff --git a/src/Model/PackageManager.php b/src/Model/PackageManager.php index 374defe5..f09afc45 100644 --- a/src/Model/PackageManager.php +++ b/src/Model/PackageManager.php @@ -182,7 +182,7 @@ public function getProvidersJson(?UserInterface $user, $hash, ?int $subRepo = nu */ public function getPackageJson(?UserInterface $user, string $package) { - if ($user && $this->authorizationChecker->isGranted('ROLE_FULL_CUSTOMER')) { + if ($user && ($this->authorizationChecker->isGranted('ROLE_FULL_CUSTOMER') || $this->subRepositoryHelper->isPublicAccess())) { $user = null; } @@ -223,7 +223,7 @@ public function getCachedPackageJson(?UserInterface $user, string $package, ?str private function dumpInMemory(?UserInterface $user = null, bool $ignoreLastModify = true, ?int $apiVersion = null, ?int $subRepo = null) { - if ($user && $this->authorizationChecker->isGranted('ROLE_FULL_CUSTOMER')) { + if ($user && ($this->authorizationChecker->isGranted('ROLE_FULL_CUSTOMER') || $this->subRepositoryHelper->isPublicAccess($subRepo))) { $user = null; } diff --git a/src/Model/PatUserScores.php b/src/Model/PatUserScores.php index a8e2cfbb..e5b978d4 100644 --- a/src/Model/PatUserScores.php +++ b/src/Model/PatUserScores.php @@ -10,7 +10,8 @@ class PatUserScores 'metadata' => [ 'root_packages', 'root_providers', 'metadata_changes', 'root_package', 'root_package_v2', 'download_dist_package', 'track_download', 'track_download_batch', - 'root_packages_slug', 'root_providers_slug', 'root_package_slug', 'root_package_v2_slug', 'download_dist_package_slug' + 'root_packages_slug', 'root_providers_slug', 'root_package_slug', 'root_package_v2_slug', 'download_dist_package_slug', + 'track_download_slug', 'track_download_batch_slug', ], 'mirror:read' => ['mirror_root', 'mirror_metadata_v2', 'mirror_metadata_v1', 'mirror_zipball', 'mirror_provider_includes'], 'mirror:all' => ['@mirror:read'], diff --git a/src/Security/Acl/SubRepoGrantVoter.php b/src/Security/Acl/SubRepoGrantVoter.php index 2f071dc6..843df394 100644 --- a/src/Security/Acl/SubRepoGrantVoter.php +++ b/src/Security/Acl/SubRepoGrantVoter.php @@ -11,12 +11,25 @@ class SubRepoGrantVoter implements CacheableVoterInterface { - public static $subRoutes = [ + public static array $subRoutes = [ 'root_packages_slug' => 1, 'root_providers_slug' => 1, 'root_package_slug' => 1, 'root_package_v2_slug' => 1, 'download_dist_package_slug' => 1, + 'track_download_batch_slug' => 1, + 'track_download_slug' => 1, + 'sub_repository_home' => 1, + ]; + + public static array $rootRoutes = [ + 'root_packages' => 1, + 'root_providers' => 1, + 'root_package' => 1, + 'root_package_v2' => 1, + 'download_dist_package' => 1, + 'track_download_batch' => 1, + 'track_download' => 1, ]; public function __construct( @@ -29,7 +42,14 @@ public function __construct( */ public function vote(TokenInterface $token, mixed $request, array $attributes): int { - if ($request instanceof Request && isset(self::$subRoutes[$request->attributes->get('_route')])) { + if (!$request instanceof Request) { + return self::ACCESS_ABSTAIN; + } + + $route = $request->attributes->get('_route'); + if (isset(self::$subRoutes[$route]) + || (isset(self::$rootRoutes[$route]) && null !== $this->helper->getByHost($request->getHost())) + ) { return $token->getUser() || $this->isPublicSubRepo($request) ? self::ACCESS_GRANTED : self::ACCESS_ABSTAIN; } diff --git a/src/Service/SubRepositoryHelper.php b/src/Service/SubRepositoryHelper.php index 503baf22..54b0bd14 100644 --- a/src/Service/SubRepositoryHelper.php +++ b/src/Service/SubRepositoryHelper.php @@ -53,6 +53,14 @@ public function allowedPackageNames(): ?array return $entity === null ? null : (empty($packages) ? [] : $packages); } + public function getRepoOption(?int $subRepo, ?string $name = null): mixed + { + $subRepo ??= $this->getSubrepositoryId(); + $data = $this->getData()[$subRepo] ?? null; + + return null === $name ? $data : ($data[$name] ?? null); + } + public function allowedPackageIds(?array $moreAllowed = null): ?array { $entity = $this->getCurrentSubrepository(); @@ -120,6 +128,14 @@ public function applySubRepository(QueryBuilder $qb): QueryBuilder return self::applyCondition($qb, $allowed); } + public function findSubRepo(null|int|string $subRepoOrSlug = null): ?SubRepository + { + $subRepoOrSlug ??= $this->getSubrepositoryId(); + $subRepo = is_string($subRepoOrSlug) ? $this->getBySlug($subRepoOrSlug) : $subRepoOrSlug; + + return null !== $subRepo ? $this->registry->getRepository(SubRepository::class)->find($subRepo) : null; + } + public function getCurrentSubrepository(): ?SubRepository { if (!$req = $this->requestStack->getMainRequest()) { @@ -130,9 +146,15 @@ public function getCurrentSubrepository(): ?SubRepository $subRepo = $req->attributes->get('_sub_repo'); $entity = $subRepo > 0 ? $this->registry->getRepository(SubRepository::class)->find($subRepo) : null; } + return $entity; } + public function getCurrentSlug(): ?string + { + return ($subRepo = $this->getCurrentSubrepository()) && !$this->isAutoHost() ? $subRepo->getSlug() : null; + } + public function getSubrepositoryId(): ?int { if (!$req = $this->requestStack->getMainRequest()) { diff --git a/templates/subrepository/public.html.twig b/templates/subrepository/public.html.twig new file mode 100644 index 00000000..5ec06bb0 --- /dev/null +++ b/templates/subrepository/public.html.twig @@ -0,0 +1,77 @@ + + + + + + {{ repo.name|capitalize }} - Repository + + + + + + + + + +
+
+
+
+

+ {{ repo.name|capitalize }} +

+
+ {% if repoUrl is not defined %} + {% set repoUrl = app.request.getSchemeAndHttpHost() %} + {% endif %} + + {% if repo.htmlMarkup %} + {{- repo.htmlMarkup|raw -}} + {% else %} +

+ This is PHP package repository {{ repo.name }} site. +

+
+ To enable this composer repository, please add these lines to your composer.json file: +
{
+  "repositories": [
+    {
+      "type": "composer",
+      "url": "{{ repoUrl }}"
+    }
+  ]
+}
+ +
+ {% endif %} +
+
+
+ + From b6e5fd22eb243b7bad087ccf53deeb280cda7316 Mon Sep 17 00:00:00 2001 From: Uladzimir Tsykun Date: Sat, 9 Nov 2024 16:14:31 +0100 Subject: [PATCH 3/3] Sub repo public access --- src/Controller/ProviderController.php | 7 ++++--- src/Package/InMemoryDumper.php | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Controller/ProviderController.php b/src/Controller/ProviderController.php index abd05442..381261a6 100644 --- a/src/Controller/ProviderController.php +++ b/src/Controller/ProviderController.php @@ -129,9 +129,6 @@ public function packageV2Action(Request $request, string $package): Response { $isDev = str_ends_with($package, '~dev'); $packageName = preg_replace('/~dev$/', '', $package); - if (!$this->checkSubrepositoryAccess($packageName)) { - return $this->createNotFound(); - } $response = new JsonResponse([]); $response->setLastModified($this->providerManager->getLastModify($package)); @@ -139,6 +136,10 @@ public function packageV2Action(Request $request, string $package): Response return $response; } + if (!$this->checkSubrepositoryAccess($packageName)) { + return $this->createNotFound(); + } + $package = $this->packageManager->getPackageV2Json($this->getUser(), $packageName, $isDev); if (!$package) { return $this->createNotFound(); diff --git a/src/Package/InMemoryDumper.php b/src/Package/InMemoryDumper.php index bca1e02f..c3ca16d2 100644 --- a/src/Package/InMemoryDumper.php +++ b/src/Package/InMemoryDumper.php @@ -100,8 +100,8 @@ private function dumpRootPackages(?UserInterface $user = null, ?int $apiVersion $url = $this->router->generate('track_download', ['name' => 'VND/PKG']); $slug = $subRepo && !$this->subRepositoryHelper->isAutoHost() ? '/'. $subRepo->getSlug() : ''; - $rootFile['notify'] = str_replace('VND/PKG', '%package%', $url); - $rootFile['notify-batch'] = $this->router->generate('track_download_batch'); + $rootFile['notify'] = $slug . str_replace('VND/PKG', '%package%', $url); + $rootFile['notify-batch'] = $slug . $this->router->generate('track_download_batch'); $rootFile['metadata-changes-url'] = $this->router->generate('metadata_changes'); $rootFile['providers-url'] = $slug . '/p/%package%$%hash%.json';