From 1c96be6fe81f9654d58bc8f01bc2fddcc0a9156e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 2 Sep 2024 21:42:14 +0200 Subject: [PATCH] Add second api token for each user, with only safe api access Fixes #1458 To migrate DB, run: bin/console doctrine:schema:update --force --complete then to populate the new tokens in the DB: bin/console packagist:tokens:generate --- css/app.scss | 7 +- js/app.js | 12 ++-- src/Command/GenerateTokensCommand.php | 10 +++ src/Controller/ApiController.php | 78 ++++++++++++++++------ src/Controller/ProfileController.php | 5 +- src/Controller/RegistrationController.php | 2 - src/DataFixtures/UserFixtures.php | 2 - src/Entity/User.php | 24 ++++++- src/Entity/UserRepository.php | 12 ++++ src/Security/GitHubAuthenticator.php | 1 - templates/api_doc/index.html.twig | 38 +++++++++-- templates/user/my_profile.html.twig | 21 ++++-- tests/Controller/ApiControllerTest.php | 51 +++++++++++++- tests/Controller/ProfileControllerTest.php | 8 ++- 14 files changed, 216 insertions(+), 55 deletions(-) diff --git a/css/app.scss b/css/app.scss index 5d655592e..a58bfcd8b 100644 --- a/css/app.scss +++ b/css/app.scss @@ -683,16 +683,15 @@ input:focus:invalid:focus, textarea:focus:invalid:focus, select:focus:invalid:fo .api-token-group { display: block; - min-height: 45px; + margin-bottom: 5px; } -.api-token-group #api-token { +.api-token-group .api-token { width: 85%; - position: absolute; right: 0; text-align: center; } .api-token-group .btn-show-api-token { - width: 145px; + width: 185px; position: absolute; z-index: 10; } diff --git a/js/app.js b/js/app.js index 2b7d500a2..f0aae15a3 100644 --- a/js/app.js +++ b/js/app.js @@ -38,21 +38,23 @@ import 'bootstrap'; /** * API Token visibility toggling */ - var token = $('#api-token'); - token.val(''); + $('.api-token').val(); - $('.btn-show-api-token,#api-token').each(function() { + $('.btn-show-api-token, .api-token').each(function() { $(this).click(function (e) { + const parent = $(this).closest('.api-token-group'); + const token = parent.find('.api-token'); token.val(token.data('api-token')); token.select(); - $('.btn-show-api-token').text('Your API token'); + const button = parent.find('.btn-show-api-token').first(); + button.text(button.text().replace('Show', 'Your')); e.preventDefault(); }); }); $('.btn-rotate-api-token').click(function (e) { - if (!window.confirm('Are you sure? This will revoke your current API token and generate a new one.')) { + if (!window.confirm('Are you sure? This will revoke your current API tokens and generate new ones.')) { e.preventDefault(); } }); diff --git a/src/Command/GenerateTokensCommand.php b/src/Command/GenerateTokensCommand.php index 229cd597e..4a7453a30 100644 --- a/src/Command/GenerateTokensCommand.php +++ b/src/Command/GenerateTokensCommand.php @@ -51,6 +51,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int $user->initializeApiToken(); } $this->doctrine->getManager()->flush(); + $this->doctrine->getManager()->clear(); + + do { + $users = $userRepo->findUsersMissingSafeApiToken(); + foreach ($users as $user) { + $user->initializeSafeApiToken(); + } + $this->doctrine->getManager()->flush(); + $this->doctrine->getManager()->clear(); + } while (\count($users) > 0); return 0; } diff --git a/src/Controller/ApiController.php b/src/Controller/ApiController.php index 8f2cfcddf..004233fe2 100644 --- a/src/Controller/ApiController.php +++ b/src/Controller/ApiController.php @@ -38,11 +38,23 @@ use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; +enum ApiType { + case Safe; + case Unsafe; +} + /** * @author Jordi Boggiano */ class ApiController extends Controller { + private const REGEXES = [ + 'gitlab' => '{^(?:ssh://git@|https?://|git://|git@)?(?P[a-z0-9.-]+)(?::[0-9]+/|[:/])(?P[\w.-]+(?:/[\w.-]+?)+)(?:\.git|/)?$}i', + 'any' => '{^(?:ssh://git@|https?://|git://|git@)?(?P[a-z0-9.-]+)(?::[0-9]+/|[:/])(?P[\w.-]+(?:/[\w.-]+?)*)(?:\.git|/)?$}i', + 'bitbucket_push' => '{^(?:https?://|git://|git@)?(?:api\.)?(?Pbitbucket\.org)[/:](?P[\w.-]+/[\w.-]+?)(?:\.git)?/?$}i', + 'bitbucket_hook' => '{^(?:https?://|git://|git@)?(?Pbitbucket\.org)[/:](?P[\w.-]+/[\w.-]+?)(?:\.git)?/?$}i', + ]; + public function __construct( private Scheduler $scheduler, private LoggerInterface $logger, @@ -72,20 +84,27 @@ public function packagesAction(string $webDir): Response #[Route(path: '/api/create-package', name: 'generic_create', defaults: ['_format' => 'json'], methods: ['POST'])] public function createPackageAction(Request $request, ProviderManager $providerManager, GitHubUserMigrationWorker $githubUserMigrationWorker, RouterInterface $router, ValidatorInterface $validator): JsonResponse { - $payload = json_decode($request->getContent(), true); + $payload = json_decode((string) $request->request->get('payload'), true); + if (!$payload && $request->headers->get('Content-Type') === 'application/json') { + $payload = json_decode($request->getContent(), true); + } + if (!$payload || !is_array($payload)) { return new JsonResponse(['status' => 'error', 'message' => 'Missing payload parameter'], 406); } - if (!isset($payload['repository']['url']) || !is_string($payload['repository']['url'])) { - return new JsonResponse(['status' => 'error', 'message' => '{repository: {url: string}} expected in payload'], 406); + if (isset($payload['repository']['url']) && is_string($payload['repository']['url'])) { // supported for BC + $url = $payload['repository']['url']; + } elseif (isset($payload['repository']) && is_string($payload['repository'])) { + $url = $payload['repository']; + } else { + return new JsonResponse(['status' => 'error', 'message' => '{repository: string} expected in payload'], 406); } $user = $this->findUser($request); if (null === $user) { - return new JsonResponse(['status' => 'error', 'message' => 'Missing username or apiToken in request'], 406); + return new JsonResponse(['status' => 'error', 'message' => 'Missing or invalid username/apiToken in request'], 406); } - $url = $payload['repository']['url']; $package = new Package; $package->addMaintainer($user); $package->setRepository($url); @@ -133,20 +152,23 @@ public function updatePackageAction(Request $request, string $githubWebhookSecre } if (isset($payload['project']['git_http_url'])) { // gitlab event payload - $urlRegex = '{^(?:ssh://git@|https?://|git://|git@)?(?P[a-z0-9.-]+)(?::[0-9]+/|[:/])(?P[\w.-]+(?:/[\w.-]+?)+)(?:\.git|/)?$}i'; + $urlRegex = self::REGEXES['gitlab']; $url = $payload['project']['git_http_url']; $remoteId = null; - } elseif (isset($payload['repository']['url'])) { // github/anything hook - $urlRegex = '{^(?:ssh://git@|https?://|git://|git@)?(?P[a-z0-9.-]+)(?::[0-9]+/|[:/])(?P[\w.-]+(?:/[\w.-]+?)*)(?:\.git|/)?$}i'; + } elseif (isset($payload['repository']) && is_string($payload['repository'])) { // anything hook + $urlRegex = self::REGEXES['any']; + $url = $payload['repository']; + $remoteId = null; + } elseif (isset($payload['repository']['url']) && is_string($payload['repository']['url'])) { // github hook + $urlRegex = self::REGEXES['any']; $url = $payload['repository']['url']; - $url = str_replace('https://api.github.com/repos', 'https://github.com', $url); $remoteId = isset($payload['repository']['id']) && (is_string($payload['repository']['id']) || is_int($payload['repository']['id'])) ? $payload['repository']['id'] : null; } elseif (isset($payload['repository']['links']['html']['href'])) { // bitbucket push event payload - $urlRegex = '{^(?:https?://|git://|git@)?(?:api\.)?(?Pbitbucket\.org)[/:](?P[\w.-]+/[\w.-]+?)(?:\.git)?/?$}i'; + $urlRegex = self::REGEXES['bitbucket_push']; $url = $payload['repository']['links']['html']['href']; $remoteId = null; } elseif (isset($payload['canon_url']) && isset($payload['repository']['absolute_url'])) { // bitbucket post hook (deprecated) - $urlRegex = '{^(?:https?://|git://|git@)?(?Pbitbucket\.org)[/:](?P[\w.-]+/[\w.-]+?)(?:\.git)?/?$}i'; + $urlRegex = self::REGEXES['bitbucket_hook']; $url = $payload['canon_url'].$payload['repository']['absolute_url']; $remoteId = null; } else { @@ -155,14 +177,19 @@ public function updatePackageAction(Request $request, string $githubWebhookSecre $statsd->increment('update_pkg_api'); - return $this->receivePost($request, $url, $urlRegex, $remoteId, $githubWebhookSecret); + $url = str_replace('https://api.github.com/repos', 'https://github.com', $url); + + return $this->receiveUpdateRequest($request, $url, $urlRegex, $remoteId, $githubWebhookSecret); } #[Route(path: '/api/packages/{package}', name: 'api_edit_package', requirements: ['package' => '[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+?'], defaults: ['_format' => 'json'], methods: ['PUT'])] public function editPackageAction(Request $request, Package $package, ValidatorInterface $validator, StatsDClient $statsd): JsonResponse { $user = $this->findUser($request); - if ((!$user || !$package->getMaintainers()->contains($user)) && !$this->isGranted('ROLE_EDIT_PACKAGES')) { + if (!$user) { + return new JsonResponse(['status' => 'error', 'message' => 'Missing or invalid username/apiToken in request'], 406); + } + if (!$package->getMaintainers()->contains($user)) { throw new AccessDeniedException; } @@ -173,6 +200,10 @@ public function editPackageAction(Request $request, Package $package, ValidatorI $payload = json_decode($request->getContent(), true); } + if (!isset($payload['repository']) || !is_string($payload['repository'])) { + return new JsonResponse(['status' => 'error', 'message' => '{repository: string} expected in request body'], 406); + } + $package->setRepository($payload['repository']); $errors = $validator->validate($package, null, ["Update"]); @@ -366,9 +397,9 @@ protected function getDefaultPackageAndVersionId(string $name): array|false * Perform the package update * * @param string $url the repository's URL (deducted from the request) - * @param non-empty-string $urlRegex the regex used to split the user packages into domain and path + * @param value-of $urlRegex the regex used to split the user packages into domain and path */ - protected function receivePost(Request $request, string $url, string $urlRegex, string|int|null $remoteId, string $githubWebhookSecret): JsonResponse + protected function receiveUpdateRequest(Request $request, string $url, string $urlRegex, string|int|null $remoteId, string $githubWebhookSecret): JsonResponse { // try to parse the URL first to avoid the DB lookup on malformed requests if (!Preg::isMatchStrictGroups($urlRegex, $url, $match)) { @@ -406,7 +437,7 @@ protected function receivePost(Request $request, string $url, string $urlRegex, if (!$user) { // find the user - $user = $this->findUser($request); + $user = $this->findUser($request, ApiType::Safe); } if (!$user && $match['host'] === 'github.com' && $request->getContent()) { @@ -424,7 +455,7 @@ protected function receivePost(Request $request, string $url, string $urlRegex, if (!$packages) { if (!$user) { - return new JsonResponse(['status' => 'error', 'message' => 'Invalid credentials'], 403); + return new JsonResponse(['status' => 'error', 'message' => 'Missing or invalid username/apiToken in request'], 403); } // try to find the user package @@ -453,7 +484,7 @@ protected function receivePost(Request $request, string $url, string $urlRegex, /** * Find a user by his username and API token */ - protected function findUser(Request $request): ?User + protected function findUser(Request $request, ApiType $apiType = ApiType::Unsafe): ?User { $username = $request->request->has('username') ? $request->request->get('username') : @@ -468,7 +499,14 @@ protected function findUser(Request $request): ?User } $user = $this->getEM()->getRepository(User::class) - ->findOneBy(['usernameCanonical' => $username, 'apiToken' => $apiToken]); + ->createQueryBuilder('u') + ->where('u.usernameCanonical = :username') + ->andWhere($apiType === ApiType::Safe ? '(u.apiToken = :apiToken OR u.safeApiToken = :apiToken)' : 'u.apiToken = :apiToken') + ->setParameter('username', $username) + ->setParameter('apiToken', $apiToken) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); if ($user && !$user->isEnabled()) { return null; @@ -480,7 +518,7 @@ protected function findUser(Request $request): ?User /** * Find a user package given by its full URL * - * @param non-empty-string $urlRegex + * @param value-of $urlRegex * @return list */ protected function findPackagesByUrl(User $user, string $url, string $urlRegex, string|int|null $remoteId): array diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index fd1defd3e..1a488b075 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -165,8 +165,9 @@ public function tokenRotateAction(Request $request, #[CurrentUser] User $user, U } $user->initializeApiToken(); - $userNotifier->notifyChange($user->getEmail(), 'Your API token has been rotated'); - $this->addFlash('success', 'Your API token has been rotated'); + $user->initializeSafeApiToken(); + $userNotifier->notifyChange($user->getEmail(), 'Your API tokens have been rotated'); + $this->addFlash('success', 'Your API tokens have been rotated'); $this->getEM()->persist($user); $this->getEM()->flush(); diff --git a/src/Controller/RegistrationController.php b/src/Controller/RegistrationController.php index 228a73708..e7358c325 100644 --- a/src/Controller/RegistrationController.php +++ b/src/Controller/RegistrationController.php @@ -54,8 +54,6 @@ public function register(Request $request, UserPasswordHasherInterface $password ) ); - $user->initializeApiToken(); - $this->getEM()->persist($user); $this->getEM()->flush(); diff --git a/src/DataFixtures/UserFixtures.php b/src/DataFixtures/UserFixtures.php index 41a96efac..1fbd559c8 100644 --- a/src/DataFixtures/UserFixtures.php +++ b/src/DataFixtures/UserFixtures.php @@ -28,7 +28,6 @@ public function load(ObjectManager $manager): void $dev->setPassword($this->passwordHasher->hashPassword($dev, 'dev')); $dev->setEnabled(true); $dev->setRoles(['ROLE_SUPERADMIN']); - $dev->initializeApiToken(); $manager->persist($dev); @@ -38,7 +37,6 @@ public function load(ObjectManager $manager): void $user->setPassword($this->passwordHasher->hashPassword($user, 'user')); $user->setEnabled(true); $user->setRoles([]); - $user->initializeApiToken(); $manager->persist($user); $manager->flush(); diff --git a/src/Entity/User.php b/src/Entity/User.php index ce0a25cdb..0c836fc12 100644 --- a/src/Entity/User.php +++ b/src/Entity/User.php @@ -101,9 +101,12 @@ class User implements UserInterface, TwoFactorInterface, BackupCodeInterface, Eq #[ORM\Column(type: 'datetime')] private DateTimeInterface $createdAt; - #[ORM\Column(type: 'string', length: 20)] + #[ORM\Column(type: 'string', length: 40)] private string $apiToken; + #[ORM\Column(type: 'string', length: 40)] + private string $safeApiToken; + #[ORM\Column(type: 'string', length: 255, nullable: true)] private string|null $githubId = null; @@ -129,6 +132,8 @@ public function __construct() { $this->packages = new ArrayCollection(); $this->createdAt = new \DateTimeImmutable(); + $this->initializeApiToken(); + $this->initializeSafeApiToken(); } public function __toString(): string @@ -175,6 +180,16 @@ public function getApiToken(): string return $this->apiToken; } + public function setSafeApiToken(string $apiToken): void + { + $this->safeApiToken = $apiToken; + } + + public function getSafeApiToken(): string + { + return $this->safeApiToken; + } + public function getGithubId(): string|null { return $this->githubId; @@ -521,7 +536,12 @@ public function isEqualTo(UserInterface $user): bool public function initializeApiToken(): void { - $this->apiToken = bin2hex(random_bytes(10)); + $this->apiToken = bin2hex(random_bytes(20)); + } + + public function initializeSafeApiToken(): void + { + $this->safeApiToken = bin2hex(random_bytes(20)); } public function getConfirmationToken(): string|null diff --git a/src/Entity/UserRepository.php b/src/Entity/UserRepository.php index c18a461ca..e3ad74dfe 100644 --- a/src/Entity/UserRepository.php +++ b/src/Entity/UserRepository.php @@ -51,6 +51,18 @@ public function findUsersMissingApiToken(): array return $qb->getQuery()->getResult(); } + /** + * @return list + */ + public function findUsersMissingSafeApiToken(): array + { + $qb = $this->createQueryBuilder('u') + ->where('u.safeApiToken IS NULL') + ->setMaxResults(500); + + return $qb->getQuery()->getResult(); + } + public function getPackageMaintainersQueryBuilder(Package $package, ?User $excludeUser = null): QueryBuilder { $qb = $this->createQueryBuilder('u') diff --git a/src/Security/GitHubAuthenticator.php b/src/Security/GitHubAuthenticator.php index a41bf033f..34cce1827 100644 --- a/src/Security/GitHubAuthenticator.php +++ b/src/Security/GitHubAuthenticator.php @@ -139,7 +139,6 @@ public function authenticate(Request $request): Passport ); $user->setLastLogin(new \DateTimeImmutable()); - $user->initializeApiToken(); $user->setEnabled(true); $this->getEM()->persist($user); diff --git a/templates/api_doc/index.html.twig b/templates/api_doc/index.html.twig index 79209d54f..1fe248ef8 100644 --- a/templates/api_doc/index.html.twig +++ b/templates/api_doc/index.html.twig @@ -27,6 +27,7 @@
  • {{ 'api_doc.get_statistics'|trans }}
  • {{ 'api_doc.list_security_advisory'|trans }}
  • {{ 'api_doc.api_create_package'|trans }}
  • +
  • Edit a package
  • Update a package
  • @@ -476,26 +477,49 @@ GET https://{{ packagist_host }}/api/security-advisories/?updatedSince=[timestam

    {{ 'api_doc.api_create_package'|trans }}

    -

    This endpoint creates a package for a specific repo. Parameters username and apiToken are required. Only the POST method is allowed.

    +

    This endpoint creates a package for a specific repo. Parameters username and apiToken are required. Only the POST method is allowed. The content-type: application/json header is required.

    +

    This endpoint is considered UNSAFE and requires your main API token to be used.

    -POST https://{{ packagist_host }}/api/create-package?username=[username]&apiToken=[apiToken] -d '{"repository":{"url":"[url]"}}'
    +POST https://{{ packagist_host }}/api/create-package?username=[username]&apiToken=[apiToken] {"repository":"[url]"}
     
     {
       "status": "success"
     }
     
    -

    Working example: curl -X POST 'https://{{ packagist_host }}/api/create-package?username=zqfan&apiToken=********' -d '{"repository":{"url":"https://github.com/monolog/monolog"}}'

    +

    Working example:
    + curl -X POST -H'Content-Type:application/json' 'https://{{ packagist_host }}/api/create-package?username={{ app.user.username|default('USERNAME') }}&apiToken=********' -d '{"repository":"https://github.com/Seldaek/monolog"}' +

    + +
    + +
    +

    Edit a package

    + +

    This endpoint allows you to edit a package URL. Parameters username and apiToken are required. Only the PUT method is allowed. The content-type: application/json header is required.

    +

    This endpoint is considered UNSAFE and requires your main API token to be used.

    + +
    +PUT https://{{ packagist_host }}/api/packages/[package name]?username=[username]&apiToken=[apiToken] {"repository":"[url]"}
    +
    +{
    +  "status": "success"
    +}
    +
    +

    Working example:
    + curl -X POST -H'Content-Type:application/json' 'https://{{ packagist_host }}/api/packages/monolog/monolog?username={{ app.user.username|default('USERNAME') }}&apiToken=********' -d '{"repository":"https://github.com/Seldaek/monolog"}' +

    Update a package

    -

    This endpoint updates a package by canonical repository URL or packagist.org package URL. Parameters username and apiToken are required. Only the POST method is allowed.

    +

    This endpoint updates a package by canonical repository URL or packagist.org package URL. Parameters username and apiToken are required. Only the POST method is allowed. The content-type: application/json header is required.

    +

    This endpoint is considered SAFE and allows either your main or safe API token to be used.

    -POST https://{{ packagist_host }}/api/update-package?username=[username]&apiToken=[apiToken] -d '{"repository":{"url":"[url]"}}'
    +POST https://{{ packagist_host }}/api/update-package?username=[username]&apiToken=[apiToken] {"repository":"[url]"}
     
     {
       "status": "success",
    @@ -503,8 +527,8 @@ POST https://{{ packagist_host }}/api/update-package?username=[username]&api
     }
     

    Working examples:
    - curl -X POST 'https://{{ packagist_host }}/api/update-package?username=Seldaek&apiToken=********' -d '{"repository":{"url":"https://github.com/Seldaek/monolog"}}'
    - curl -X POST 'https://{{ packagist_host }}/api/update-package?username=Seldaek&apiToken=********' -d '{"repository":{"url":"https://packagist.org/monolog/monolog"}}' + curl -X POST -H'Content-Type:application/json' 'https://{{ packagist_host }}/api/update-package?username={{ app.user.username|default('USERNAME') }}&apiToken=********' -d '{"repository":"https://github.com/Seldaek/monolog"}'
    + curl -X POST -H'Content-Type:application/json' 'https://{{ packagist_host }}/api/update-package?username={{ app.user.username|default('USERNAME') }}&apiToken=********' -d '{"repository":"https://packagist.org/monolog/monolog"}'

    diff --git a/templates/user/my_profile.html.twig b/templates/user/my_profile.html.twig index e1b6077ee..88d5a1861 100644 --- a/templates/user/my_profile.html.twig +++ b/templates/user/my_profile.html.twig @@ -5,19 +5,28 @@ {% block user_content %}
    {%- if app.user.apiToken %} -

    Your API Token

    +

    Your API Tokens

    - +

    The main API token can be used to talk to all APIs (e.g. package creation, edit, update) and should be only used in places where it can be kept secret.

    + - +
    -

    You can use your API token to interact with the Packagist API, see details in the docs.

    -

    You can rotate your API token and generate a new one at any time.

    +
    +

    The safe API token can only be used to talk to safe APIs (e.g. package update) and is not very sensitive if leaked.

    + + + + +
    + +

    Read more about the API.

    +

    You can rotate your API tokens and generate a new one at any time.

    - +
    diff --git a/tests/Controller/ApiControllerTest.php b/tests/Controller/ApiControllerTest.php index c3400d42c..42fed097b 100644 --- a/tests/Controller/ApiControllerTest.php +++ b/tests/Controller/ApiControllerTest.php @@ -43,6 +43,7 @@ public function testGithubApi($url): void $user = new User; $user->addPackage($package); + $package->addMaintainer($user); $user->setEnabled(true); $user->setUsername('test'); $user->setEmail('test@example.org'); @@ -78,6 +79,54 @@ public static function githubApiProvider(): array ]; } + public function testUnsafeApiRejectsSafeApiToken(): void + { + $user = new User; + $user->setEnabled(true); + $user->setUsername('test'); + $user->setEmail('test@example.org'); + $user->setPassword('testtest'); + $user->setApiToken('token'); + $user->setSafeApiToken('safetoken'); + + $em = self::getEM(); + $em->persist($user); + $em->flush(); + + $payload = json_encode(['repository' => 'https://github.com/composer/composer']); + $this->client->request('POST', '/api/create-package?username=test&apiToken=safetoken', ['payload' => $payload]); + $this->assertEquals(406, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent()); + $this->assertEquals(json_encode(['status' => 'error', 'message' => 'Missing or invalid username/apiToken in request']), $this->client->getResponse()->getContent()); + } + + public function testSafeApiAcceptsBothApiTokens(): void + { + $url = 'https://github.com/composer/composer'; + $package = $this->createPackage('test/'.bin2hex(random_bytes(10)), $url); + $user = new User; + $user->addPackage($package); + $package->addMaintainer($user); + $user->setEnabled(true); + $user->setUsername('test'); + $user->setEmail('test@example.org'); + $user->setPassword('testtest'); + $user->setApiToken('token'); + $user->setSafeApiToken('safetoken'); + + $em = self::getEM(); + $em->persist($package); + $em->persist($user); + $em->flush(); + + $payload = json_encode(['repository' => $url]); + $this->client->request('POST', '/api/update-package?username=test&apiToken=safetoken', ['payload' => $payload]); + $this->assertEquals(202, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent()); + + $payload = json_encode(['repository' => 'https://packagist.org/packages/'.$package->getName()]); + $this->client->request('POST', '/api/update-package?username=test&apiToken=token', ['payload' => $payload]); + $this->assertEquals(202, $this->client->getResponse()->getStatusCode(), $this->client->getResponse()->getContent()); + } + #[Depends('testGitHubFailsWithInvalidCredentials')] #[DataProvider('urlProvider')] public function testUrlDetection($endpoint, $url, $expectedOK): void @@ -87,7 +136,7 @@ public function testUrlDetection($endpoint, $url, $expectedOK): void $absUrl = substr($url, 1); $payload = json_encode(['canon_url' => $canonUrl, 'repository' => ['absolute_url' => $absUrl]]); } else { - $payload = json_encode(['repository' => ['url' => $url]]); + $payload = json_encode(['repository' => $url]); } $this->client->request('POST', '/api/'.$endpoint.'?username=INVALID_USER&apiToken=INVALID_TOKEN', ['payload' => $payload]); diff --git a/tests/Controller/ProfileControllerTest.php b/tests/Controller/ProfileControllerTest.php index 7e69f360e..1451ddad1 100644 --- a/tests/Controller/ProfileControllerTest.php +++ b/tests/Controller/ProfileControllerTest.php @@ -59,8 +59,8 @@ public function testTokenRotate(): void $user->setUsername('test'); $user->setEmail('test@example.org'); $user->setPassword('testtest'); - $user->initializeApiToken(); $token = $user->getApiToken(); + $safeToken = $user->getSafeApiToken(); $em = self::getEM(); $em->persist($user); @@ -69,9 +69,10 @@ public function testTokenRotate(): void $this->client->loginUser($user); $crawler = $this->client->request('GET', '/profile/'); - $this->assertEquals($token, $crawler->filter('#api-token')->attr('data-api-token')); + $this->assertEquals($token, $crawler->filter('.api-token')->first()->attr('data-api-token')); + $this->assertEquals($safeToken, $crawler->filter('.api-token')->last()->attr('data-api-token')); - $form = $crawler->selectButton('Rotate API Token')->form(); + $form = $crawler->selectButton('Rotate API Tokens')->form(); $this->client->submit($form); $this->assertResponseStatusCodeSame(302); @@ -80,5 +81,6 @@ public function testTokenRotate(): void $user = $em->getRepository(User::class)->find($user->getId()); $this->assertNotNull($user); $this->assertNotEquals($token, $user->getApiToken()); + $this->assertNotEquals($safeToken, $user->getSafeApiToken()); } }