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.
|
||||
|
||||
### Command line gitlab-token
|
||||
### Command line forgejo-token
|
||||
|
||||
```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:
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
<?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;
|
||||
|
||||
use Composer\Cache;
|
||||
|
@ -8,6 +18,7 @@ use Composer\Downloader\TransportException;
|
|||
use Composer\IO\IOInterface;
|
||||
use Composer\Json\JsonFile;
|
||||
use Composer\Pcre\Preg;
|
||||
use Composer\Util\Forgejo;
|
||||
use Composer\Util\ForgejoRepositoryData;
|
||||
use Composer\Util\ForgejoUrl;
|
||||
use Composer\Util\Http\Response;
|
||||
|
@ -274,12 +285,16 @@ class ForgejoDriver extends VcsDriver
|
|||
|
||||
protected function getContents(string $url, bool $fetchingRepoData = false): Response
|
||||
{
|
||||
$forgejo = new Forgejo($this->io, $this->config, $this->httpDownloader);
|
||||
|
||||
try {
|
||||
return parent::getContents($url);
|
||||
} catch (TransportException $e) {
|
||||
switch ($e->getCode()) {
|
||||
case 401:
|
||||
case 403:
|
||||
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) {
|
||||
throw $e;
|
||||
}
|
||||
|
@ -289,6 +304,15 @@ class ForgejoDriver extends VcsDriver
|
|||
|
||||
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:
|
||||
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);
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
<?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\Pcre\Preg;
|
||||
|
|
|
@ -416,6 +416,7 @@ class ConfigTest extends TestCase
|
|||
'github-oauth',
|
||||
'gitlab-oauth',
|
||||
'gitlab-token',
|
||||
'forgejo-token',
|
||||
'http-basic',
|
||||
'bearer',
|
||||
];
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
<?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;
|
||||
|
||||
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);
|
||||
|
||||
/*
|
||||
* 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;
|
||||
|
|
Loading…
Reference in New Issue