Lock transaction to avoid race conditions when renaming an API key

This commit is contained in:
Alejandro Celaya
2024-11-09 11:09:34 +01:00
parent 95685d958d
commit d228b88e51
5 changed files with 69 additions and 36 deletions

View File

@@ -21,18 +21,20 @@ readonly class ApiKeyService implements ApiKeyServiceInterface
public function create(ApiKeyMeta $apiKeyMeta): ApiKey
{
// TODO If name is auto-generated, do not throw. Instead, re-generate a new key
$apiKey = ApiKey::fromMeta($apiKeyMeta);
if ($this->existsWithName($apiKey->name)) {
throw new InvalidArgumentException(
sprintf('Another API key with name "%s" already exists', $apiKeyMeta->name),
);
}
return $this->em->wrapInTransaction(function () use ($apiKeyMeta) {
$apiKey = ApiKey::fromMeta($apiKeyMeta);
// TODO If name is auto-generated, do not throw. Instead, re-generate a new key
if ($this->repo->nameExists($apiKey->name)) {
throw new InvalidArgumentException(
sprintf('Another API key with name "%s" already exists', $apiKeyMeta->name),
);
}
$this->em->persist($apiKey);
$this->em->flush();
$this->em->persist($apiKey);
$this->em->flush();
return $apiKey;
return $apiKey;
});
}
public function createInitial(string $key): ApiKey|null
@@ -85,9 +87,6 @@ readonly class ApiKeyService implements ApiKeyServiceInterface
/**
* @inheritDoc
* @todo This method should be transactional and to a SELECT ... FROM UPDATE when checking if the new name exists,
* to avoid a race condition where the method is called twice in parallel for a new name that doesn't exist,
* causing two API keys to end up with the same name.
*/
public function renameApiKey(Renaming $apiKeyRenaming): ApiKey
{
@@ -102,25 +101,22 @@ readonly class ApiKeyService implements ApiKeyServiceInterface
return $apiKey;
}
if ($this->existsWithName($apiKeyRenaming->newName)) {
throw new InvalidArgumentException(
sprintf('Another API key with name "%s" already exists', $apiKeyRenaming->newName),
);
}
return $this->em->wrapInTransaction(function () use ($apiKeyRenaming, $apiKey) {
if ($this->repo->nameExists($apiKeyRenaming->newName)) {
throw new InvalidArgumentException(
sprintf('Another API key with name "%s" already exists', $apiKeyRenaming->newName),
);
}
$apiKey->name = $apiKeyRenaming->newName;
$this->em->flush();
$apiKey->name = $apiKeyRenaming->newName;
$this->em->flush();
return $apiKey;
return $apiKey;
});
}
private function findByKey(string $key): ApiKey|null
{
return $this->repo->findOneBy(['key' => ApiKey::hashKey($key)]);
}
private function existsWithName(string $apiKeyName): bool
{
return $this->repo->count(['name' => $apiKeyName]) > 0;
}
}