Created new env var to programatically provide an initial API key

This commit is contained in:
Alejandro Celaya 2022-09-11 10:45:03 +02:00
parent e6ee4ceae2
commit f5138385be
9 changed files with 119 additions and 10 deletions

View File

@ -8,8 +8,8 @@ return [
'debug' => false, 'debug' => false,
// Disabling config cache for cli, ensures it's never used for openswoole and also that console commands don't // Disabling config cache for cli, ensures it's never used for openswoole/RoadRunner, and also that console
// generate a cache file that's then used by non-openswoole web executions // commands don't generate a cache file that's then used by php-fpm web executions
ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli', ConfigAggregator::ENABLE_CACHE => PHP_SAPI !== 'cli',
]; ];

View File

@ -47,6 +47,7 @@ enum EnvVars: string
case PORT = 'PORT'; case PORT = 'PORT';
case TASK_WORKER_NUM = 'TASK_WORKER_NUM'; case TASK_WORKER_NUM = 'TASK_WORKER_NUM';
case WEB_WORKER_NUM = 'WEB_WORKER_NUM'; case WEB_WORKER_NUM = 'WEB_WORKER_NUM';
case INITIAL_API_KEY = 'INITIAL_API_KEY';
case ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR'; case ANONYMIZE_REMOTE_ADDR = 'ANONYMIZE_REMOTE_ADDR';
case TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS'; case TRACK_ORPHAN_VISITS = 'TRACK_ORPHAN_VISITS';
case DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM'; case DISABLE_TRACK_PARAM = 'DISABLE_TRACK_PARAM';

View File

@ -15,7 +15,8 @@ use function Shlinkio\Shlink\Core\determineTableName;
return static function (ClassMetadata $metadata, array $emConfig): void { return static function (ClassMetadata $metadata, array $emConfig): void {
$builder = new ClassMetadataBuilder($metadata); $builder = new ClassMetadataBuilder($metadata);
$builder->setTable(determineTableName('api_keys', $emConfig)); $builder->setTable(determineTableName('api_keys', $emConfig))
->setCustomRepositoryClass(ApiKey\Repository\ApiKeyRepository::class);
$builder->createField('id', Types::BIGINT) $builder->createField('id', Types::BIGINT)
->makePrimaryKey() ->makePrimaryKey()

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest;
use Mezzio\Application;
use Shlinkio\Shlink\Core\Config\EnvVars;
use const PHP_SAPI;
return [
'initial_api_key' => PHP_SAPI !== 'cli' ? null : EnvVars::INITIAL_API_KEY->loadFromEnv(),
'dependencies' => [
'delegators' => [
Application::class => [
ApiKey\InitialApiKeyDelegator::class,
],
],
],
];

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\ApiKey;
use Doctrine\ORM\EntityManager;
use Mezzio\Application;
use Psr\Container\ContainerInterface;
use Shlinkio\Shlink\Rest\ApiKey\Repository\ApiKeyRepositoryInterface;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class InitialApiKeyDelegator
{
public function __invoke(ContainerInterface $container, string $serviceName, callable $callback): Application
{
$initialApiKey = $container->get('config')['initial_api_key'] ?? null;
if ($initialApiKey !== null) {
$this->createInitialApiKey($initialApiKey, $container);
}
return $callback();
}
private function createInitialApiKey(string $initialApiKey, ContainerInterface $container): void
{
/** @var ApiKeyRepositoryInterface $repo */
$repo = $container->get(EntityManager::class)->getRepository(ApiKey::class);
$repo->createInitialApiKey($initialApiKey);
}
}

View File

@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\ApiKey\Repository;
use Doctrine\DBAL\LockMode;
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepository;
use Shlinkio\Shlink\Rest\Entity\ApiKey;
class ApiKeyRepository extends EntitySpecificationRepository implements ApiKeyRepositoryInterface
{
public function createInitialApiKey(string $apiKey): void
{
$this->getEntityManager()->wrapInTransaction(function () use ($apiKey): void {
$qb = $this->getEntityManager()->createQueryBuilder();
$amountOfApiKeys = $qb->select('COUNT(a.id)')
->from(ApiKey::class, 'a')
->getQuery()
->setLockMode(LockMode::PESSIMISTIC_WRITE)
->getSingleScalarResult();
if ($amountOfApiKeys === 0) {
$this->getEntityManager()->persist(ApiKey::fromKey($apiKey));
$this->getEntityManager()->flush();
}
});
}
}

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\ApiKey\Repository;
use Doctrine\Persistence\ObjectRepository;
use Happyr\DoctrineSpecification\Repository\EntitySpecificationRepositoryInterface;
interface ApiKeyRepositoryInterface extends ObjectRepository, EntitySpecificationRepositoryInterface
{
/**
* Will create provided API key only if there's no API keys yet
*/
public function createInitialApiKey(string $apiKey): void;
}

View File

@ -23,16 +23,14 @@ class ApiKey extends AbstractEntity
private bool $enabled; private bool $enabled;
/** @var Collection|ApiKeyRole[] */ /** @var Collection|ApiKeyRole[] */
private Collection $roles; private Collection $roles;
private ?string $name; private ?string $name = null;
/** /**
* @throws Exception * @throws Exception
*/ */
private function __construct(?string $name = null, ?Chronos $expirationDate = null) private function __construct(?string $key = null)
{ {
$this->key = Uuid::uuid4()->toString(); $this->key = $key ?? Uuid::uuid4()->toString();
$this->expirationDate = $expirationDate;
$this->name = $name;
$this->enabled = true; $this->enabled = true;
$this->roles = new ArrayCollection(); $this->roles = new ArrayCollection();
} }
@ -44,7 +42,10 @@ class ApiKey extends AbstractEntity
public static function fromMeta(ApiKeyMeta $meta): self public static function fromMeta(ApiKeyMeta $meta): self
{ {
$apiKey = new self($meta->name, $meta->expirationDate); $apiKey = self::create();
$apiKey->name = $meta->name;
$apiKey->expirationDate = $meta->expirationDate;
foreach ($meta->roleDefinitions as $roleDefinition) { foreach ($meta->roleDefinitions as $roleDefinition) {
$apiKey->registerRole($roleDefinition); $apiKey->registerRole($roleDefinition);
} }
@ -52,6 +53,11 @@ class ApiKey extends AbstractEntity
return $apiKey; return $apiKey;
} }
public static function fromKey(string $key): self
{
return new self($key);
}
public function getExpirationDate(): ?Chronos public function getExpirationDate(): ?Chronos
{ {
return $this->expirationDate; return $this->expirationDate;

View File

@ -22,10 +22,11 @@ class ConfigProviderTest extends TestCase
{ {
$config = ($this->configProvider)(); $config = ($this->configProvider)();
self::assertCount(4, $config); self::assertCount(5, $config);
self::assertArrayHasKey('dependencies', $config); self::assertArrayHasKey('dependencies', $config);
self::assertArrayHasKey('auth', $config); self::assertArrayHasKey('auth', $config);
self::assertArrayHasKey('entity_manager', $config); self::assertArrayHasKey('entity_manager', $config);
self::assertArrayHasKey('initial_api_key', $config);
self::assertArrayHasKey(ConfigAbstractFactory::class, $config); self::assertArrayHasKey(ConfigAbstractFactory::class, $config);
} }