diff --git a/README.md b/README.md index 78cfe59f..ecbeba5b 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,9 @@ Packeton - Private PHP package repository for vendors Fork of [Packagist](https://github.com/composer/packagist). The Open Source alternative of [Private Packagist for vendors](https://packagist.com), that based on [Satis](https://github.com/composer/satis) and [Packagist](https://github.com/composer/packagist). -### Legacy Symfony 3.4 version +**All** documentation here [docs.packeton.org](https://docs.packeton.org/) -[Legacy docs](../1.4/README.md) +### Legacy Symfony 3.4 version Update to 2.0. [UPGRADE.md](./UPGRADE.md) diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index 34f2a5a9..e56304f3 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -10,7 +10,17 @@ # Usage - [Administration](usage/README.md) - [Admin API](usage/api.md) - - [Webhooks](webhook.md) + - [Outgoing Webhook](webhook.md) + - [Intro](webhook.md) + - [Examples](webhook.md) + - [Secrets](webhook/webhook-secrets.md) + - [Security Policy](webhook/wh-security.md) + - [Testing](webhook/wh-test.md) + - [Update Webhooks](usage/update-packages.md) + - [User Authentication](authentication.md) - [JWT Configuration](authentication-jwt.md) - [LDAP Configuration](authentication-ldap.md) + +# Development + - [Contributing](dev/contributing.md) diff --git a/docs/authentication-jwt.md b/docs/authentication-jwt.md index 74399f26..3b0819d3 100644 --- a/docs/authentication-jwt.md +++ b/docs/authentication-jwt.md @@ -100,6 +100,3 @@ Simply use the JWT, like standard API token for composer api. Since 2.0 composer downloads all packages in parallel, it may run more 12 request at the same time. To prevent calls external LDAP provider each time for JWT token verify, the obtained LDAP user object placed to cache with 60 sec TTL. - - - diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md new file mode 100644 index 00000000..7141a77b --- /dev/null +++ b/docs/dev/contributing.md @@ -0,0 +1,33 @@ +# Contributing + +Everyone is welcome to contribute code to https://github.com/vtsykun/packeton.git + +### 1. Development environment. + +If you are running Windows, the Linux Windows Subsystem (WSL) is recommended for development. +The code of Packeton is written in PHP. + +#### Requirements + +- PHP 8.1+ +- Redis (or Docker) for some functionality. +- Symfony CLI / nginx / php-fpm to run the web server. +- (optional) MySQL or PostgresSQL for the main data store, default SQLite. + +### 2. Get the source. + +Make a fork on GitHub, and then create a pull request to provide your changes. + +``` +git clone git@github.com:YOUR_GITHUB_NAME/packeton.git +git checkout -b patch-1 +``` + +### 4. Install the dependencies + +Run composer install + +``` +cd packeton +composer install +``` diff --git a/docs/img/secrets.png b/docs/img/secrets.png new file mode 100644 index 00000000..82d776da Binary files /dev/null and b/docs/img/secrets.png differ diff --git a/docs/img/wh-test.png b/docs/img/wh-test.png new file mode 100644 index 00000000..0d6e21af Binary files /dev/null and b/docs/img/wh-test.png differ diff --git a/docs/usage/README.md b/docs/usage/README.md index e69de29b..090972ec 100644 --- a/docs/usage/README.md +++ b/docs/usage/README.md @@ -0,0 +1,31 @@ +# Administration + +This section contains information on managing your Packeton application. + +Table of content +---------------- + +- [API Usage](api.md) +- [Outgoing Webhook](../webhook.md) + - [Intro](../webhook.md#introduction) + - [Examples](../webhook.md#examples) + - [Telegram notification](../webhook.md#telegram-notification) + - [Slack notification](../webhook.md#slack-notification) + - [JIRA issue fix version](../webhook.md#jira-create-a-new-release-and-set-fix-version) + - [Gitlab setup auto webhook](../webhook.md#gitlab-auto-webhook) +- [Application Roles](#application-roles) + + +## Application Roles + +- `ROLE_USER` - minimal customer access level, these users only can read metadata only for selected packages. +- `ROLE_FULL_CUSTOMER` - Can read all packages metadata. +- `ROLE_MAINTAINER` - Can submit a new package and read all metadata. +- `ROLE_ADMIN` - Can create a new customer users, management webhooks and credentials. + +You can create a user and then promote to admin or maintainer via console using fos user bundle commands. + +``` +php bin/console packagist:user:manager username --email=admin@example.com --password=123456 --admin # create admin user +php bin/console packagist:user:manager user1 --add-role=ROLE_MAINTAINER # Add ROLE_MAINTAINER to user user1 +``` diff --git a/docs/usage/api.md b/docs/usage/api.md index e69de29b..6a3b1e07 100644 --- a/docs/usage/api.md +++ b/docs/usage/api.md @@ -0,0 +1,153 @@ +# API documentation + +About API authorization methods see [here](../authentication.md#composer-api-authentication) + +#### Submit package + +``` +POST https://example.com/api/create-package?token= +Content-Type: application/json + +{ + "repository": { + "url": "git@github.com:symfony/mime.git" + } +} +``` + +#### Listing package names + +``` +GET https://example.com/packages/list.json?token= + +# Result +{ + "packageNames": [ + "[vendor]/[package]", + ... + ] +} +``` + +#### List packages by vendor + +``` + +GET https://example.com/packages/list.json?vendor=[vendor]&token= + +{ + "packageNames": [ + "[vendor]/[package]", + ... + ] +} +``` + +#### List packages by type + +``` +GET https://example.com/packages/list.json?type=[type]&token= + +{ + "packageNames": [ + "[vendor]/[package]", + ... + ] +} +``` + + +#### Get the package git changelog + +Get git diff between two commits or tags. **WARNING** Working only if repository was cloned by git. +If you want to use this feature for GitHub you need set composer config flag no-api see here + +``` +GET https://example.com/packages/{name}/changelog?token=&from=3.1.14&to=3.1.15 + +{ + "result": [ + "BAP-18660: ElasticSearch 6", + "BB-17293: Back-office >Wrong height" + ], + "error": null, + "metadata": { + "from": "3.1.14", + "to": "3.1.15", + "package": "okvpn/platform" + } +} +``` + +#### Getting package data + +This is the preferred way to access the data as it is always up-to-date, and dumped to static files so it is very efficient on our end. + +You can also send If-Modified-Since headers to limit your bandwidth usage +and cache the files on your end with the proper filemtime set according to our Last-Modified header. + +There are a few gotchas though with using this method: +* It only provides you with the package metadata but not information about the maintainers, download stats or github info. +* It contains providers information which must be ignored but can appear confusing at first. This will disappear in the future though. + +``` +GET https://example.com/p/[vendor]/[package].json?token= + +{ + "packages": { + "[vendor]/[package]": { + "[version1]": { + "name": "[vendor]/[package], + "description": [description], + // ... + }, + "[version2]": { + // ... + } + // ... + } + } +} +``` + +**Composer v2** + +``` +GET https://example.com/p2/firebase/php-jwt.json + + +{ + "minified": "composer/2.0", + "packages": { + "[vendor]/[package]": [... list versions ] + } +} +``` + +**Using the API** + +The JSON API for packages gives you all the infos we have including downloads, +dependents count, github info, etc. However, it is generated dynamically so for performance reason we cache the responses +for twelve hours. As such if the static file endpoint described above is enough please use it instead. + +``` +GET https://example.com/packages/[vendor]/[package].json?token= + +{ + "package": { + "name": "[vendor]/[package], + "description": [description], + "time": [time of the last release], + "maintainers": [list of maintainers], + "versions": [list of versions and their dependencies, the same data of composer.json] + "type": [package type], + "repository": [repository url], + "downloads": { + "total": [numbers of download], + "monthly": [numbers of download per month], + "daily": [numbers of download per day] + }, + "favers": [number of favers] + } +} +``` diff --git a/docs/usage/update-packages.md b/docs/usage/update-packages.md new file mode 100644 index 00000000..704da986 --- /dev/null +++ b/docs/usage/update-packages.md @@ -0,0 +1,63 @@ +# How to auto update packages? + +You can use GitLab, GitHub, and Bitbucket project post-receive hook to keep your packages up to date every time you push code. + +## Into + +Webhook API request authorization with minimum access level `ROLE_MAINTAINER`. +You can use `token` query parameter with `` to call it. + +Also support Packagist.org authorization with `username` and `apiToken` query parameters. + +## Bitbucket Webhooks +To enable the Bitbucket web hook, go to your BitBucket repository, +open the settings and select "Webhooks" in the menu. Add a new hook. Y +ou have to enter the Packagist endpoint, containing both your username and API token. +Enter `https:///api/bitbucket?token=user:token` as URL. Save your changes and you're done. + +## GitLab Service + +To enable the GitLab service integration, go to your GitLab repository, open +the Settings > Integrations page from the menu. +Search for Packagist in the list of Project Services. Check the "Active" box, +enter your `packeton.org` username and API token. Save your changes and you're done. + +## GitLab Group Hooks + +Group webhooks will apply to all projects in a group and allow to sync all projects. +To enable the Group GitLab webhook you must have the paid plan. +Go to your GitLab Group > Settings > Webhooks. +Enter `https:///api/update-package?token=user:token` as URL. + +## GitHub Webhooks +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. + +## Gitea Webhooks + +To enable the Gitea web hook, go to your Gitea repository, open the settings, select "Webhooks" +in the menu and click on 'Add Webhook'. From the dropdown menu select Gitea. +You have to enter the Packagist endpoint, containing both your username and API token. +Enter `https:///api/update-package?token=user:token` as URL. +The HTTP method has to be POST and content type is application/json. Save your changes and you're done. + +## Manual 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://g/api/update-package?token=user:api_token` with a request body looking like this: + + +``` +{ + "repository": { + "url": "PACKAGIST_PACKAGE_URL" + } +} +``` + +You can also send a GET request with query parameter `composer_package_name` + +``` +curl 'https:///api/update-package?token=&composer_package_name=vender/name' +``` diff --git a/docs/webhook.md b/docs/webhook.md index 4201eeb7..10aa4044 100644 --- a/docs/webhook.md +++ b/docs/webhook.md @@ -26,7 +26,7 @@ update "fix version" attribute of all the related issues from that release. To build a custom request payload uses Twig expression language. This allows you to create custom queries. Untrusted template code is evaluate in a Twig sandbox mode, so you will get an error if try to get access for security sensitive information. -By default only admin users can use Webhooks. +By default, only admin users can use Webhooks. ``` Exception (Twig\Sandbox\SecurityNotAllowedMethodError). Calling "setemail" method on a "Packagist\WebBundle\Entity\User" object is not allowed in "__string_template__0d2344b042278505e67568413272d80429f07ecccea43af39cb33608fa747830" at line 1. @@ -348,4 +348,4 @@ Here you need replace `request.url` on your packagist. New twig functions ----------------- -See [WebhookExtension](/src/Packagist/WebBundle/Webhook/Twig/WebhookExtension.php) for details. +See [WebhookExtension](/src/Webhook/Twig/WebhookExtension.php) for details. diff --git a/docs/webhook/webhook-secrets.md b/docs/webhook/webhook-secrets.md new file mode 100644 index 00000000..ddf920bf --- /dev/null +++ b/docs/webhook/webhook-secrets.md @@ -0,0 +1,60 @@ +# Manage secrets + +Before adding sensitive data such as API credentials to your webhook payload strongly +recommended to encrypt it. Secret data will not be save to database or shows on webhooks' status. +Alternative way you can set own visibility for webhook entity to prevent edit by other admin users. + +### Encrypt secrets + +To add secrets to your webhook, put JSON body to `Request options` field, for example + +```json +{ +// "headers" more other options + "secrets": { + "allowed-domains": ["api.telegram.org"], + "TOKEN": "167000000:AAzddkPzfgzkqzzFghiwPutin_khuylo", + "CHART_ID": "-1000017160005" + } +} +``` + +[![secrets](../img/secrets.png)](../img/secrets.png) + +Once the form is submitted, the secret params will be encrypted and sign and cannot be changed. +The sign algo is hmac sha256 with `APP_SECRET` as key. Digital signature required to prevent modification +encrypted data and attack on change `allowed-domains` + +#### Secrets option + +`allowed-domains` - you can restrict webhook call to untrusted hosts to prevent modify the change URL parameter. + +### Usage secrets + +Use secrets params in request, url or headers options, example: + +``` +https://api.telegram.org/bot${secrets.TOKEN}/sendMessage +``` + +In body + +```twig +{% set request = { + 'chat_id': '${secrets.CHART_ID}', + 'text': 'example text' +} %} +{{ request|json_encode }} +``` + +But this example will not work. + +```twig +{% set request = { + 'chat_id': '${secrets.CHART_ID}', + 'text': 'example text' +} %} +{% do log('${secrets.CHART_ID}') + +{{ request|json_encode }} +``` diff --git a/docs/webhook/wh-security.md b/docs/webhook/wh-security.md new file mode 100644 index 00000000..c569e83d --- /dev/null +++ b/docs/webhook/wh-security.md @@ -0,0 +1,36 @@ +# Webhook security + +To evaluate expressions uses a Twig sandbox mode. + +You will get an error if try to get access for security sensitive information. + +**SSRF** - To prevent SSRF attacks to make HTTP requests to inner private networks uses `NoPrivateNetworkHttpClient`, +so not possible call url like `http://10.8.100.1/` etc. + +``` +# This code is not works. + +{% set text = "*New Releases*\n" %} +{% set title = package.name ~ ' (' ~ versions|map(v => "#{v.version}")|join(',') ~ ')' %} + +{% set text = text ~ package.credentials.key %} + +{% set request = { + 'channel': 'jenkins', + 'text': text +} %} + +{{ request|json_encode }} + +``` + +``` +Exception (Twig\Sandbox\SecurityNotAllowedMethodError). Calling "getcredentials" method on a "Packeton\Entity\Package" +object is not allowed in "__string_template__4b1d9dd7416b75a6c353bd4750fe5490" at line 4. +``` + +Block SSRF +``` +Exception (Symfony\Component\HttpClient\Exception\TransportException). IP "127.0.0.1" is blocked for "https://pack.loc.example.org/webhooks". + * Prev exception (Symfony\Component\HttpClient\Exception\TransportException). IP "127.0.0.1" is blocked for "https://pack.loc.example.org/webhooks". +``` diff --git a/docs/webhook/wh-test.md b/docs/webhook/wh-test.md new file mode 100644 index 00000000..1bec9b7c --- /dev/null +++ b/docs/webhook/wh-test.md @@ -0,0 +1,5 @@ +# Test Webhook Twig payload. + +You can use test action to check result + +[![Test](../img/wh-test.png)](../img/wh-test.png) diff --git a/public/packeton/css/main.css b/public/packeton/css/main.css index b0416522..77491146 100644 --- a/public/packeton/css/main.css +++ b/public/packeton/css/main.css @@ -610,7 +610,7 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo min-height: 45px; } .api-token-group #api-token { - width: 85%; + width: 83%; position: absolute; right: 0; text-align: center; diff --git a/src/Composer/VcsDriverFactory.php b/src/Composer/VcsDriverFactory.php index 0bb7e5ef..d268fe85 100644 --- a/src/Composer/VcsDriverFactory.php +++ b/src/Composer/VcsDriverFactory.php @@ -85,6 +85,11 @@ public function createDriver(array $repoConfig, IOInterface $io, Config $config, } } + if (null === $driver) { + $repoUrl = $options['url'] ?? null; + throw new \UnexpectedValueException("VCS Driver not found for repository $repoUrl"); + } + $driver->initialize(); return $driver; diff --git a/src/Controller/ApiController.php b/src/Controller/ApiController.php index 1360786a..adaaa8d2 100644 --- a/src/Controller/ApiController.php +++ b/src/Controller/ApiController.php @@ -6,6 +6,7 @@ use Doctrine\Persistence\ManagerRegistry; use Packeton\Entity\Package; +use Packeton\Entity\User; use Packeton\Entity\Webhook; use Packeton\Model\DownloadManager; use Packeton\Model\PackageManager; @@ -38,14 +39,17 @@ public function __construct( */ public function createPackageAction(Request $request) { - $payload = json_decode($request->getContent(), true); - if (!$payload) { - return new JsonResponse(['status' => 'error', 'message' => 'Missing payload parameter'], 406); + $payload = $this->getJsonPayload($request); + + if (!$payload || empty($url = $payload['repository']['url'] ?? null)) { + return new JsonResponse(['status' => 'error', 'message' => 'Missing payload repository->url parameter'], 406); } - $url = $payload['repository']['url']; + $package = new Package; - $user = $this->getUser(); - $package->addMaintainer($user); + if ($this->getUser() instanceof User) { + $package->addMaintainer($this->getUser()); + } + $package->setRepository($url); $this->container->get(PackageManager::class)->updatePackageUrl($package); $errors = $this->validator->validate($package, null, ['Create']); @@ -60,6 +64,7 @@ public function createPackageAction(Request $request) $em = $this->registry->getManager(); $em->persist($package); $em->flush(); + } catch (\Exception $e) { $this->logger->critical($e->getMessage(), ['exception', $e]); return new JsonResponse(['status' => 'error', 'message' => 'Error saving package'], 500); diff --git a/src/Security/Acl/OwnerVoter.php b/src/Security/Acl/OwnerVoter.php index 1d08edd1..b32e20bc 100644 --- a/src/Security/Acl/OwnerVoter.php +++ b/src/Security/Acl/OwnerVoter.php @@ -20,7 +20,8 @@ public function vote(TokenInterface $token, $object, array $attributes) $user = $token->getUser(); if (!$user instanceof User) { - return $object->getVisibility() === OwnerAwareInterface::GLOBAL_VISIBLE ? self::ACCESS_GRANTED : self::ACCESS_DENIED; + return $object->getVisibility() === OwnerAwareInterface::GLOBAL_VISIBLE || null === $object->getVisibility() + ? self::ACCESS_GRANTED : self::ACCESS_DENIED; } if ($object->getVisibility() === OwnerAwareInterface::USER_VISIBLE && $object->getOwner() && $object->getOwner()->getId() !== $user->getId()) {