Added roles info to api key generation and api key list

This commit is contained in:
Alejandro Celaya 2021-01-11 16:32:59 +01:00
parent c49a0ca040
commit 9e9d213f20
12 changed files with 51 additions and 20 deletions

View File

@ -28,7 +28,7 @@ class RoleResolver implements RoleResolverInterface
} }
if ($domainAuthority !== null) { if ($domainAuthority !== null) {
$domain = $this->domainService->getOrCreate($domainAuthority); $domain = $this->domainService->getOrCreate($domainAuthority);
$roleDefinitions[] = RoleDefinition::forDomain($domain->getId()); $roleDefinitions[] = RoleDefinition::forDomain($domain);
} }
return $roleDefinitions; return $roleDefinitions;

View File

@ -6,6 +6,7 @@ namespace Shlinkio\Shlink\CLI\Command\Api;
use Shlinkio\Shlink\CLI\Util\ExitCodes; use Shlinkio\Shlink\CLI\Util\ExitCodes;
use Shlinkio\Shlink\CLI\Util\ShlinkTable; use Shlinkio\Shlink\CLI\Util\ShlinkTable;
use Shlinkio\Shlink\Rest\ApiKey\Role;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface; use Shlinkio\Shlink\Rest\Service\ApiKeyServiceInterface;
use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Command\Command;
@ -14,7 +15,8 @@ use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\OutputInterface;
use function array_filter; use function array_filter;
use function array_map; use function Functional\map;
use function implode;
use function sprintf; use function sprintf;
class ListKeysCommand extends Command class ListKeysCommand extends Command
@ -50,7 +52,7 @@ class ListKeysCommand extends Command
{ {
$enabledOnly = $input->getOption('enabledOnly'); $enabledOnly = $input->getOption('enabledOnly');
$rows = array_map(function (ApiKey $apiKey) use ($enabledOnly) { $rows = map($this->apiKeyService->listKeys($enabledOnly), function (ApiKey $apiKey) use ($enabledOnly) {
$expiration = $apiKey->getExpirationDate(); $expiration = $apiKey->getExpirationDate();
$messagePattern = $this->determineMessagePattern($apiKey); $messagePattern = $this->determineMessagePattern($apiKey);
@ -60,13 +62,21 @@ class ListKeysCommand extends Command
$rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey)); $rowData[] = sprintf($messagePattern, $this->getEnabledSymbol($apiKey));
} }
$rowData[] = $expiration !== null ? $expiration->toAtomString() : '-'; $rowData[] = $expiration !== null ? $expiration->toAtomString() : '-';
$rowData[] = $apiKey->isAdmin() ? '-' : implode("\n", $apiKey->mapRoles(
fn (string $roleName, array $meta) =>
empty($meta)
? Role::toFriendlyName($roleName)
: sprintf('%s: %s', Role::toFriendlyName($roleName), Role::domainAuthorityFromMeta($meta)),
));
return $rowData; return $rowData;
}, $this->apiKeyService->listKeys($enabledOnly)); });
ShlinkTable::fromOutput($output)->render(array_filter([ ShlinkTable::fromOutput($output)->render(array_filter([
'Key', 'Key',
! $enabledOnly ? 'Is enabled' : null, ! $enabledOnly ? 'Is enabled' : null,
'Expiration date', 'Expiration date',
'Roles',
]), $rows); ]), $rows);
return ExitCodes::EXIT_SUCCESS; return ExitCodes::EXIT_SUCCESS;
} }
@ -80,8 +90,6 @@ class ListKeysCommand extends Command
return $apiKey->isExpired() ? self::WARNING_STRING_PATTERN : self::SUCCESS_STRING_PATTERN; return $apiKey->isExpired() ? self::WARNING_STRING_PATTERN : self::SUCCESS_STRING_PATTERN;
} }
/**
*/
private function getEnabledSymbol(ApiKey $apiKey): string private function getEnabledSymbol(ApiKey $apiKey): string
{ {
return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++'; return ! $apiKey->isEnabled() || $apiKey->isExpired() ? '---' : '+++';

View File

@ -47,6 +47,7 @@ class RoleResolverTest extends TestCase
public function provideRoles(): iterable public function provideRoles(): iterable
{ {
$domain = (new Domain('example.com'))->setId('1');
$buildInput = function (array $definition): InputInterface { $buildInput = function (array $definition): InputInterface {
$input = $this->prophesize(InputInterface::class); $input = $this->prophesize(InputInterface::class);
@ -64,7 +65,7 @@ class RoleResolverTest extends TestCase
]; ];
yield 'domain role only' => [ yield 'domain role only' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => 'example.com', RoleResolver::AUTHOR_ONLY_PARAM => false]), $buildInput([RoleResolver::DOMAIN_ONLY_PARAM => 'example.com', RoleResolver::AUTHOR_ONLY_PARAM => false]),
[RoleDefinition::forDomain('1')], [RoleDefinition::forDomain($domain)],
1, 1,
]; ];
yield 'author role only' => [ yield 'author role only' => [
@ -74,7 +75,7 @@ class RoleResolverTest extends TestCase
]; ];
yield 'both roles' => [ yield 'both roles' => [
$buildInput([RoleResolver::DOMAIN_ONLY_PARAM => 'example.com', RoleResolver::AUTHOR_ONLY_PARAM => true]), $buildInput([RoleResolver::DOMAIN_ONLY_PARAM => 'example.com', RoleResolver::AUTHOR_ONLY_PARAM => true]),
[RoleDefinition::forAuthoredShortUrls(), RoleDefinition::forDomain('1')], [RoleDefinition::forAuthoredShortUrls(), RoleDefinition::forDomain($domain)],
1, 1,
]; ];
} }

View File

@ -72,12 +72,12 @@ class DomainRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();
$authorAndDomainApiKey->registerRole(RoleDefinition::forDomain($fooDomain->getId())); $authorAndDomainApiKey->registerRole(RoleDefinition::forDomain($fooDomain));
$fooDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($fooDomain->getId())); $fooDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($fooDomain));
$this->getEntityManager()->persist($fooDomainApiKey); $this->getEntityManager()->persist($fooDomainApiKey);
$barDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($barDomain->getId())); $barDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($barDomain));
$this->getEntityManager()->persist($fooDomainApiKey); $this->getEntityManager()->persist($fooDomainApiKey);
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();

View File

@ -335,9 +335,9 @@ class ShortUrlRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist($apiKey); $this->getEntityManager()->persist($apiKey);
$otherApiKey = ApiKey::withRoles(RoleDefinition::forAuthoredShortUrls()); $otherApiKey = ApiKey::withRoles(RoleDefinition::forAuthoredShortUrls());
$this->getEntityManager()->persist($otherApiKey); $this->getEntityManager()->persist($otherApiKey);
$wrongDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($wrongDomain->getId())); $wrongDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($wrongDomain));
$this->getEntityManager()->persist($wrongDomainApiKey); $this->getEntityManager()->persist($wrongDomainApiKey);
$rightDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($rightDomain->getId())); $rightDomainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($rightDomain));
$this->getEntityManager()->persist($rightDomainApiKey); $this->getEntityManager()->persist($rightDomainApiKey);
$shortUrl = new ShortUrl('foo', ShortUrlMeta::fromRawData( $shortUrl = new ShortUrl('foo', ShortUrlMeta::fromRawData(

View File

@ -114,7 +114,7 @@ class TagRepositoryTest extends DatabaseTestCase
$authorApiKey = ApiKey::withRoles(RoleDefinition::forAuthoredShortUrls()); $authorApiKey = ApiKey::withRoles(RoleDefinition::forAuthoredShortUrls());
$this->getEntityManager()->persist($authorApiKey); $this->getEntityManager()->persist($authorApiKey);
$domainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($domain->getId())); $domainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($domain));
$this->getEntityManager()->persist($domainApiKey); $this->getEntityManager()->persist($domainApiKey);
$names = ['foo', 'bar', 'baz', 'another']; $names = ['foo', 'bar', 'baz', 'another'];

View File

@ -221,7 +221,7 @@ class VisitRepositoryTest extends DatabaseTestCase
$this->getEntityManager()->persist($shortUrl3); $this->getEntityManager()->persist($shortUrl3);
$this->createVisitsForShortUrl($shortUrl3, 7); $this->createVisitsForShortUrl($shortUrl3, 7);
$domainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($domain->getId())); $domainApiKey = ApiKey::withRoles(RoleDefinition::forDomain($domain));
$this->getEntityManager()->persist($domainApiKey); $this->getEntityManager()->persist($domainApiKey);
$this->getEntityManager()->flush(); $this->getEntityManager()->flush();

View File

@ -51,7 +51,7 @@ class DomainServiceTest extends TestCase
{ {
$default = new DomainItem('default.com', true); $default = new DomainItem('default.com', true);
$adminApiKey = new ApiKey(); $adminApiKey = new ApiKey();
$domainSpecificApiKey = ApiKey::withRoles(RoleDefinition::forDomain('123')); $domainSpecificApiKey = ApiKey::withRoles(RoleDefinition::forDomain((new Domain(''))->setId('123')));
yield 'empty list without API key' => [[], [$default], null]; yield 'empty list without API key' => [[], [$default], null];
yield 'one item without API key' => [ yield 'one item without API key' => [

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Shlinkio\Shlink\Rest\ApiKey\Model; namespace Shlinkio\Shlink\Rest\ApiKey\Model;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Rest\ApiKey\Role; use Shlinkio\Shlink\Rest\ApiKey\Role;
final class RoleDefinition final class RoleDefinition
@ -22,9 +23,12 @@ final class RoleDefinition
return new self(Role::AUTHORED_SHORT_URLS, []); return new self(Role::AUTHORED_SHORT_URLS, []);
} }
public static function forDomain(string $domainId): self public static function forDomain(Domain $domain): self
{ {
return new self(Role::DOMAIN_SPECIFIC, ['domain_id' => $domainId]); return new self(
Role::DOMAIN_SPECIFIC,
['domain_id' => $domain->getId(), 'authority' => $domain->getAuthority()],
);
} }
public function roleName(): string public function roleName(): string

View File

@ -16,6 +16,10 @@ class Role
{ {
public const AUTHORED_SHORT_URLS = 'AUTHORED_SHORT_URLS'; public const AUTHORED_SHORT_URLS = 'AUTHORED_SHORT_URLS';
public const DOMAIN_SPECIFIC = 'DOMAIN_SPECIFIC'; public const DOMAIN_SPECIFIC = 'DOMAIN_SPECIFIC';
private const ROLE_FRIENDLY_NAMES = [
self::AUTHORED_SHORT_URLS => 'Author only',
self::DOMAIN_SPECIFIC => 'Domain only',
];
public static function toSpec(ApiKeyRole $role, bool $inlined): Specification public static function toSpec(ApiKeyRole $role, bool $inlined): Specification
{ {
@ -35,4 +39,14 @@ class Role
{ {
return $meta['domain_id'] ?? '-1'; return $meta['domain_id'] ?? '-1';
} }
public static function domainAuthorityFromMeta(array $meta): string
{
return $meta['authority'] ?? '';
}
public static function toFriendlyName(string $roleName): string
{
return self::ROLE_FRIENDLY_NAMES[$roleName] ?? '';
}
} }

View File

@ -33,7 +33,7 @@ class ApiKeyFixture extends AbstractFixture implements DependentFixtureInterface
/** @var Domain $exampleDomain */ /** @var Domain $exampleDomain */
$exampleDomain = $this->getReference('example_domain'); $exampleDomain = $this->getReference('example_domain');
$domainApiKey = $this->buildApiKey('domain_api_key', true); $domainApiKey = $this->buildApiKey('domain_api_key', true);
$domainApiKey->registerRole(RoleDefinition::forDomain($exampleDomain->getId())); $domainApiKey->registerRole(RoleDefinition::forDomain($exampleDomain));
$manager->persist($domainApiKey); $manager->persist($domainApiKey);
$manager->flush(); $manager->flush();

View File

@ -12,6 +12,7 @@ use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Prophecy\Prophecy\ObjectProphecy; use Prophecy\Prophecy\ObjectProphecy;
use Shlinkio\Shlink\Common\Exception\InvalidArgumentException; use Shlinkio\Shlink\Common\Exception\InvalidArgumentException;
use Shlinkio\Shlink\Core\Entity\Domain;
use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition; use Shlinkio\Shlink\Rest\ApiKey\Model\RoleDefinition;
use Shlinkio\Shlink\Rest\Entity\ApiKey; use Shlinkio\Shlink\Rest\Entity\ApiKey;
use Shlinkio\Shlink\Rest\Service\ApiKeyService; use Shlinkio\Shlink\Rest\Service\ApiKeyService;
@ -51,7 +52,10 @@ class ApiKeyServiceTest extends TestCase
{ {
yield 'no expiration date' => [null, []]; yield 'no expiration date' => [null, []];
yield 'expiration date' => [Chronos::parse('2030-01-01'), []]; yield 'expiration date' => [Chronos::parse('2030-01-01'), []];
yield 'roles' => [null, [RoleDefinition::forDomain('123'), RoleDefinition::forAuthoredShortUrls()]]; yield 'roles' => [null, [
RoleDefinition::forDomain((new Domain(''))->setId('123')),
RoleDefinition::forAuthoredShortUrls(),
]];
} }
/** /**