Forgejo: implement and document authentication for forgejo repositories
parent
f5ea243160
commit
cad36a0fbd
|
@ -371,10 +371,10 @@ To create a new access token, go to your [applications section on Forgejo](https
|
||||||
|
|
||||||
When creating a Forgejo access token, make sure it has the `read:repository` scope.
|
When creating a Forgejo access token, make sure it has the `read:repository` scope.
|
||||||
|
|
||||||
### Command line gitlab-token
|
### Command line forgejo-token
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
php composer.phar config [--global] forgejo-token.gitlab.example.org username access-token
|
php composer.phar config [--global] forgejo-token.forgejo.example.org username access-token
|
||||||
```
|
```
|
||||||
|
|
||||||
In the above command, the config key `forgejo-token.forgejo.example.org` consists of two parts:
|
In the above command, the config key `forgejo-token.forgejo.example.org` consists of two parts:
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace Composer\Repository\Vcs;
|
namespace Composer\Repository\Vcs;
|
||||||
|
|
||||||
use Composer\Cache;
|
use Composer\Cache;
|
||||||
|
@ -8,6 +18,7 @@ use Composer\Downloader\TransportException;
|
||||||
use Composer\IO\IOInterface;
|
use Composer\IO\IOInterface;
|
||||||
use Composer\Json\JsonFile;
|
use Composer\Json\JsonFile;
|
||||||
use Composer\Pcre\Preg;
|
use Composer\Pcre\Preg;
|
||||||
|
use Composer\Util\Forgejo;
|
||||||
use Composer\Util\ForgejoRepositoryData;
|
use Composer\Util\ForgejoRepositoryData;
|
||||||
use Composer\Util\ForgejoUrl;
|
use Composer\Util\ForgejoUrl;
|
||||||
use Composer\Util\Http\Response;
|
use Composer\Util\Http\Response;
|
||||||
|
@ -274,12 +285,16 @@ class ForgejoDriver extends VcsDriver
|
||||||
|
|
||||||
protected function getContents(string $url, bool $fetchingRepoData = false): Response
|
protected function getContents(string $url, bool $fetchingRepoData = false): Response
|
||||||
{
|
{
|
||||||
|
$forgejo = new Forgejo($this->io, $this->config, $this->httpDownloader);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return parent::getContents($url);
|
return parent::getContents($url);
|
||||||
} catch (TransportException $e) {
|
} catch (TransportException $e) {
|
||||||
switch ($e->getCode()) {
|
switch ($e->getCode()) {
|
||||||
|
case 401:
|
||||||
|
case 403:
|
||||||
case 404:
|
case 404:
|
||||||
// try to authorize only if we are fetching the main /repos/foo/bar data, otherwise it must be a real 404
|
case 429:
|
||||||
if (!$fetchingRepoData) {
|
if (!$fetchingRepoData) {
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
@ -289,6 +304,15 @@ class ForgejoDriver extends VcsDriver
|
||||||
|
|
||||||
return new Response(['url' => 'dummy'], 200, [], 'null');
|
return new Response(['url' => 'dummy'], 200, [], 'null');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!$this->io->hasAuthentication($this->originUrl) &&
|
||||||
|
$forgejo->authorizeOAuthInteractively($this->forgejoUrl->originUrl, $e->getCode() === 429 ? 'API limit exhausted. Enter your Forgejo credentials to get a larger API limit (<info>'.$this->url.'</info>)' : null)
|
||||||
|
) {
|
||||||
|
return parent::getContents($url);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
default:
|
default:
|
||||||
throw $e;
|
throw $e;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Util;
|
||||||
|
|
||||||
|
use Composer\Config;
|
||||||
|
use Composer\Downloader\TransportException;
|
||||||
|
use Composer\IO\IOInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
* @readonly
|
||||||
|
*/
|
||||||
|
final class Forgejo
|
||||||
|
{
|
||||||
|
/** @var IOInterface */
|
||||||
|
private $io;
|
||||||
|
/** @var Config */
|
||||||
|
private $config;
|
||||||
|
/** @var HttpDownloader */
|
||||||
|
private $httpDownloader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param IOInterface $io
|
||||||
|
* @param Config $config
|
||||||
|
* @param HttpDownloader $httpDownloader
|
||||||
|
*/
|
||||||
|
public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader)
|
||||||
|
{
|
||||||
|
$this->io = $io;
|
||||||
|
$this->config = $config;
|
||||||
|
$this->httpDownloader = $httpDownloader;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Authorizes a Forgejo domain interactively
|
||||||
|
*
|
||||||
|
* @param string $originUrl The host this Forgejo instance is located at
|
||||||
|
* @param string $message The reason this authorization is required
|
||||||
|
* @throws \RuntimeException
|
||||||
|
* @throws TransportException|\Exception
|
||||||
|
* @return bool true on success
|
||||||
|
*/
|
||||||
|
public function authorizeOAuthInteractively(string $originUrl, ?string $message = null): bool
|
||||||
|
{
|
||||||
|
if ($message !== null) {
|
||||||
|
$this->io->writeError($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
$url = 'https://'.$originUrl.'/user/settings/applications';
|
||||||
|
$this->io->writeError('Setup a personal access token with repository:read permissions on:');
|
||||||
|
$this->io->writeError($url);
|
||||||
|
$localAuthConfig = $this->config->getLocalAuthConfigSource();
|
||||||
|
$this->io->writeError(sprintf('Tokens will be stored in plain text in "%s" for future use by Composer.', ($localAuthConfig !== null ? $localAuthConfig->getName() . ' OR ' : '') . $this->config->getAuthConfigSource()->getName()));
|
||||||
|
$this->io->writeError('For additional information, check https://getcomposer.org/doc/articles/authentication-for-private-packages.md#forgejo-token');
|
||||||
|
|
||||||
|
$storeInLocalAuthConfig = false;
|
||||||
|
if ($localAuthConfig !== null) {
|
||||||
|
$storeInLocalAuthConfig = $this->io->askConfirmation('A local auth config source was found, do you want to store the token there?', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim((string) $this->io->ask('Username: '));
|
||||||
|
$token = trim((string) $this->io->askAndHideAnswer('Token (hidden): '));
|
||||||
|
|
||||||
|
$addTokenManually = sprintf('You can also add it manually later by using "composer config --global --auth forgejo-token.%s <username> <token>"', $originUrl);
|
||||||
|
if ($token === '' || $username === '') {
|
||||||
|
$this->io->writeError('<warning>No username/token given, aborting.</warning>');
|
||||||
|
$this->io->writeError($addTokenManually);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->io->setAuthentication($originUrl, $username, $token);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->httpDownloader->get('https://'. $originUrl . '/api/v1/version', [
|
||||||
|
'retry-auth-failure' => false,
|
||||||
|
]);
|
||||||
|
} catch (TransportException $e) {
|
||||||
|
if (in_array($e->getCode(), [403, 401, 404], true)) {
|
||||||
|
$this->io->writeError('<error>Invalid access token provided.</error>');
|
||||||
|
$this->io->writeError($addTokenManually);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw $e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// store value in local/user config
|
||||||
|
$authConfigSource = $storeInLocalAuthConfig && $localAuthConfig !== null ? $localAuthConfig : $this->config->getAuthConfigSource();
|
||||||
|
$this->config->getConfigSource()->removeConfigSetting('forgejo-token.'.$originUrl);
|
||||||
|
$authConfigSource->addConfigSetting('forgejo-token.'.$originUrl, ['username' => $username, 'token' => $token]);
|
||||||
|
|
||||||
|
$this->io->writeError('<info>Token stored successfully.</info>');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,15 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace Composer\Util;
|
namespace Composer\Util;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace Composer\Util;
|
namespace Composer\Util;
|
||||||
|
|
||||||
use Composer\Pcre\Preg;
|
use Composer\Pcre\Preg;
|
||||||
|
|
|
@ -416,6 +416,7 @@ class ConfigTest extends TestCase
|
||||||
'github-oauth',
|
'github-oauth',
|
||||||
'gitlab-oauth',
|
'gitlab-oauth',
|
||||||
'gitlab-token',
|
'gitlab-token',
|
||||||
|
'forgejo-token',
|
||||||
'http-basic',
|
'http-basic',
|
||||||
'bearer',
|
'bearer',
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace Composer\Test\Repository\Vcs;
|
namespace Composer\Test\Repository\Vcs;
|
||||||
|
|
||||||
use Composer\Config;
|
use Composer\Config;
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Composer\Test\Util;
|
||||||
|
|
||||||
|
use Composer\Test\TestCase;
|
||||||
|
use Composer\Util\Forgejo;
|
||||||
|
use Composer\Util\GitHub;
|
||||||
|
|
||||||
|
class ForgejoTest extends TestCase
|
||||||
|
{
|
||||||
|
/** @var string */
|
||||||
|
private $username = 'username';
|
||||||
|
/** @var string */
|
||||||
|
private $accessToken = 'access-token';
|
||||||
|
/** @var string */
|
||||||
|
private $message = 'mymessage';
|
||||||
|
/** @var string */
|
||||||
|
private $origin = 'codeberg.org';
|
||||||
|
|
||||||
|
public function testUsernamePasswordAuthenticationFlow(): void
|
||||||
|
{
|
||||||
|
$io = $this->getIOMock();
|
||||||
|
$io->expects([
|
||||||
|
['text' => $this->message],
|
||||||
|
['ask' => 'Username: ', 'reply' => $this->username],
|
||||||
|
['ask' => 'Token (hidden): ', 'reply' => $this->accessToken],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$httpDownloader = $this->getHttpDownloaderMock();
|
||||||
|
$httpDownloader->expects(
|
||||||
|
[['url' => sprintf('https://%s/api/v1/version', $this->origin), 'body' => '{}']],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$config = $this->getConfigMock();
|
||||||
|
$config
|
||||||
|
->expects($this->exactly(2))
|
||||||
|
->method('getAuthConfigSource')
|
||||||
|
->willReturn($this->getAuthJsonMock())
|
||||||
|
;
|
||||||
|
$config
|
||||||
|
->expects($this->once())
|
||||||
|
->method('getConfigSource')
|
||||||
|
->willReturn($this->getConfJsonMock())
|
||||||
|
;
|
||||||
|
|
||||||
|
$forgejo = new Forgejo($io, $config, $httpDownloader);
|
||||||
|
|
||||||
|
self::assertTrue($forgejo->authorizeOAuthInteractively($this->origin, $this->message));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUsernamePasswordFailure(): void
|
||||||
|
{
|
||||||
|
$io = $this->getIOMock();
|
||||||
|
$io->expects([
|
||||||
|
['ask' => 'Username: ', 'reply' => $this->username],
|
||||||
|
['ask' => 'Token (hidden): ', 'reply' => $this->accessToken],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$httpDownloader = $this->getHttpDownloaderMock();
|
||||||
|
$httpDownloader->expects(
|
||||||
|
[['url' => sprintf('https://%s/api/v1/version', $this->origin), 'status' => 404]],
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
$config = $this->getConfigMock();
|
||||||
|
$config
|
||||||
|
->expects($this->exactly(1))
|
||||||
|
->method('getAuthConfigSource')
|
||||||
|
->willReturn($this->getAuthJsonMock())
|
||||||
|
;
|
||||||
|
|
||||||
|
$forgejo = new Forgejo($io, $config, $httpDownloader);
|
||||||
|
|
||||||
|
self::assertFalse($forgejo->authorizeOAuthInteractively($this->origin));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \PHPUnit\Framework\MockObject\MockObject&\Composer\Config
|
||||||
|
*/
|
||||||
|
private function getConfigMock()
|
||||||
|
{
|
||||||
|
return $this->getMockBuilder('Composer\Config')->getMock();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \PHPUnit\Framework\MockObject\MockObject&\Composer\Config\JsonConfigSource
|
||||||
|
*/
|
||||||
|
private function getAuthJsonMock()
|
||||||
|
{
|
||||||
|
$authjson = $this
|
||||||
|
->getMockBuilder('Composer\Config\JsonConfigSource')
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock()
|
||||||
|
;
|
||||||
|
$authjson
|
||||||
|
->expects($this->atLeastOnce())
|
||||||
|
->method('getName')
|
||||||
|
->willReturn('auth.json')
|
||||||
|
;
|
||||||
|
|
||||||
|
return $authjson;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return \PHPUnit\Framework\MockObject\MockObject&\Composer\Config\JsonConfigSource
|
||||||
|
*/
|
||||||
|
private function getConfJsonMock()
|
||||||
|
{
|
||||||
|
$confjson = $this
|
||||||
|
->getMockBuilder('Composer\Config\JsonConfigSource')
|
||||||
|
->disableOriginalConstructor()
|
||||||
|
->getMock()
|
||||||
|
;
|
||||||
|
$confjson
|
||||||
|
->expects($this->atLeastOnce())
|
||||||
|
->method('removeConfigSetting')
|
||||||
|
->with('forgejo-token.'.$this->origin)
|
||||||
|
;
|
||||||
|
|
||||||
|
return $confjson;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,15 @@
|
||||||
<?php declare(strict_types=1);
|
<?php declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of Composer.
|
||||||
|
*
|
||||||
|
* (c) Nils Adermann <naderman@naderman.de>
|
||||||
|
* Jordi Boggiano <j.boggiano@seld.be>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
namespace Composer\Test\Util;
|
namespace Composer\Test\Util;
|
||||||
|
|
||||||
use Composer\Test\TestCase;
|
use Composer\Test\TestCase;
|
||||||
|
|
Loading…
Reference in New Issue