mirror of
https://github.com/shlinkio/shlink.git
synced 2024-11-23 21:27:44 +03:00
Add ORPHAN_VISITS_EXCLUDED API key role
This commit is contained in:
parent
112b54ec7d
commit
8b03532ddb
11 changed files with 54 additions and 23 deletions
|
@ -22,6 +22,7 @@ class RoleResolver implements RoleResolverInterface
|
||||||
{
|
{
|
||||||
$domainAuthority = $input->getOption(Role::DOMAIN_SPECIFIC->paramName());
|
$domainAuthority = $input->getOption(Role::DOMAIN_SPECIFIC->paramName());
|
||||||
$author = $input->getOption(Role::AUTHORED_SHORT_URLS->paramName());
|
$author = $input->getOption(Role::AUTHORED_SHORT_URLS->paramName());
|
||||||
|
$noOrphanVisits = $input->getOption(Role::NO_ORPHAN_VISITS->paramName());
|
||||||
|
|
||||||
$roleDefinitions = [];
|
$roleDefinitions = [];
|
||||||
if ($author) {
|
if ($author) {
|
||||||
|
@ -30,6 +31,9 @@ class RoleResolver implements RoleResolverInterface
|
||||||
if (is_string($domainAuthority)) {
|
if (is_string($domainAuthority)) {
|
||||||
$roleDefinitions[] = $this->resolveRoleForAuthority($domainAuthority);
|
$roleDefinitions[] = $this->resolveRoleForAuthority($domainAuthority);
|
||||||
}
|
}
|
||||||
|
if ($noOrphanVisits) {
|
||||||
|
$roleDefinitions[] = RoleDefinition::forOrphanVisitsExcluded();
|
||||||
|
}
|
||||||
|
|
||||||
return $roleDefinitions;
|
return $roleDefinitions;
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,8 +25,8 @@ class GenerateKeyCommand extends Command
|
||||||
public const NAME = 'api-key:generate';
|
public const NAME = 'api-key:generate';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ApiKeyServiceInterface $apiKeyService,
|
private readonly ApiKeyServiceInterface $apiKeyService,
|
||||||
private RoleResolverInterface $roleResolver,
|
private readonly RoleResolverInterface $roleResolver,
|
||||||
) {
|
) {
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,8 @@ class GenerateKeyCommand extends Command
|
||||||
{
|
{
|
||||||
$authorOnly = Role::AUTHORED_SHORT_URLS->paramName();
|
$authorOnly = Role::AUTHORED_SHORT_URLS->paramName();
|
||||||
$domainOnly = Role::DOMAIN_SPECIFIC->paramName();
|
$domainOnly = Role::DOMAIN_SPECIFIC->paramName();
|
||||||
|
$noOrphanVisits = Role::NO_ORPHAN_VISITS->paramName();
|
||||||
|
|
||||||
$help = <<<HELP
|
$help = <<<HELP
|
||||||
The <info>%command.name%</info> generates a new valid API key.
|
The <info>%command.name%</info> generates a new valid API key.
|
||||||
|
|
||||||
|
@ -52,7 +54,8 @@ class GenerateKeyCommand extends Command
|
||||||
|
|
||||||
* Can interact with short URLs created with this API key: <info>%command.full_name% --{$authorOnly}</info>
|
* Can interact with short URLs created with this API key: <info>%command.full_name% --{$authorOnly}</info>
|
||||||
* Can interact with short URLs for one domain: <info>%command.full_name% --{$domainOnly}=example.com</info>
|
* Can interact with short URLs for one domain: <info>%command.full_name% --{$domainOnly}=example.com</info>
|
||||||
* Both: <info>%command.full_name% --{$authorOnly} --{$domainOnly}=example.com</info>
|
* Cannot see orphan visits: <info>%command.full_name% --{$noOrphanVisits}</info>
|
||||||
|
* All: <info>%command.full_name% --{$authorOnly} --{$domainOnly}=example.com --{$noOrphanVisits}</info>
|
||||||
HELP;
|
HELP;
|
||||||
|
|
||||||
$this
|
$this
|
||||||
|
@ -85,6 +88,12 @@ class GenerateKeyCommand extends Command
|
||||||
Role::DOMAIN_SPECIFIC->value,
|
Role::DOMAIN_SPECIFIC->value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
->addOption(
|
||||||
|
$noOrphanVisits,
|
||||||
|
'o',
|
||||||
|
InputOption::VALUE_NONE,
|
||||||
|
sprintf('Adds the "%s" role to the new API key.', Role::NO_ORPHAN_VISITS->value),
|
||||||
|
)
|
||||||
->setHelp($help);
|
->setHelp($help);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class ListKeysCommand extends Command
|
||||||
|
|
||||||
public const NAME = 'api-key:list';
|
public const NAME = 'api-key:list';
|
||||||
|
|
||||||
public function __construct(private ApiKeyServiceInterface $apiKeyService)
|
public function __construct(private readonly ApiKeyServiceInterface $apiKeyService)
|
||||||
{
|
{
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
@ -60,10 +60,7 @@ class ListKeysCommand extends Command
|
||||||
}
|
}
|
||||||
$rowData[] = $expiration?->toAtomString() ?? '-';
|
$rowData[] = $expiration?->toAtomString() ?? '-';
|
||||||
$rowData[] = ApiKey::isAdmin($apiKey) ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
$rowData[] = ApiKey::isAdmin($apiKey) ? 'Admin' : implode("\n", $apiKey->mapRoles(
|
||||||
fn (Role $role, array $meta) =>
|
fn (Role $role, array $meta) => $role->toFriendlyName($meta),
|
||||||
empty($meta)
|
|
||||||
? $role->toFriendlyName()
|
|
||||||
: sprintf('%s: %s', $role->toFriendlyName(), Role::domainAuthorityFromMeta($meta)),
|
|
||||||
));
|
));
|
||||||
|
|
||||||
return $rowData;
|
return $rowData;
|
||||||
|
|
|
@ -79,6 +79,7 @@ class DomainRepository extends EntitySpecificationRepository implements DomainRe
|
||||||
yield from $apiKey?->mapRoles(fn (Role $role, array $meta) => match ($role) {
|
yield from $apiKey?->mapRoles(fn (Role $role, array $meta) => match ($role) {
|
||||||
Role::DOMAIN_SPECIFIC => ['d', new IsDomain(Role::domainIdFromMeta($meta))],
|
Role::DOMAIN_SPECIFIC => ['d', new IsDomain(Role::domainIdFromMeta($meta))],
|
||||||
Role::AUTHORED_SHORT_URLS => ['s', new BelongsToApiKey($apiKey)],
|
Role::AUTHORED_SHORT_URLS => ['s', new BelongsToApiKey($apiKey)],
|
||||||
|
default => null,
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -56,6 +56,7 @@ class TagRepository extends EntitySpecificationRepository implements TagReposito
|
||||||
Role::AUTHORED_SHORT_URLS => $qb->andWhere(
|
Role::AUTHORED_SHORT_URLS => $qb->andWhere(
|
||||||
$qb->expr()->eq('s.author_api_key_id', $conn->quote($apiKey->getId())),
|
$qb->expr()->eq('s.author_api_key_id', $conn->quote($apiKey->getId())),
|
||||||
),
|
),
|
||||||
|
default => $qb,
|
||||||
});
|
});
|
||||||
|
|
||||||
// For admins and when no API key is present, we'll return tags which are not linked to any short URL
|
// For admins and when no API key is present, we'll return tags which are not linked to any short URL
|
||||||
|
|
|
@ -44,7 +44,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||||
|
|
||||||
$builder->createOneToMany('roles', ApiKeyRole::class)
|
$builder->createOneToMany('roles', ApiKeyRole::class)
|
||||||
->mappedBy('apiKey')
|
->mappedBy('apiKey')
|
||||||
->setIndexBy('roleName')
|
->setIndexBy('role')
|
||||||
->cascadePersist()
|
->cascadePersist()
|
||||||
->orphanRemoval()
|
->orphanRemoval()
|
||||||
->build();
|
->build();
|
||||||
|
|
|
@ -25,7 +25,7 @@ return static function (ClassMetadata $metadata, array $emConfig): void {
|
||||||
->build();
|
->build();
|
||||||
|
|
||||||
(new FieldBuilder($builder, [
|
(new FieldBuilder($builder, [
|
||||||
'fieldName' => 'roleName',
|
'fieldName' => 'role',
|
||||||
'type' => Types::STRING,
|
'type' => Types::STRING,
|
||||||
'enumType' => Role::class,
|
'enumType' => Role::class,
|
||||||
]))->columnName('role_name')
|
]))->columnName('role_name')
|
||||||
|
|
|
@ -25,4 +25,9 @@ final class RoleDefinition
|
||||||
['domain_id' => $domain->getId(), 'authority' => $domain->authority],
|
['domain_id' => $domain->getId(), 'authority' => $domain->authority],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function forOrphanVisitsExcluded(): self
|
||||||
|
{
|
||||||
|
return new self(Role::NO_ORPHAN_VISITS, []);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,16 +12,20 @@ use Shlinkio\Shlink\Core\ShortUrl\Spec\BelongsToDomain;
|
||||||
use Shlinkio\Shlink\Core\ShortUrl\Spec\BelongsToDomainInlined;
|
use Shlinkio\Shlink\Core\ShortUrl\Spec\BelongsToDomainInlined;
|
||||||
use Shlinkio\Shlink\Rest\Entity\ApiKeyRole;
|
use Shlinkio\Shlink\Rest\Entity\ApiKeyRole;
|
||||||
|
|
||||||
|
use function sprintf;
|
||||||
|
|
||||||
enum Role: string
|
enum Role: string
|
||||||
{
|
{
|
||||||
case AUTHORED_SHORT_URLS = 'AUTHORED_SHORT_URLS';
|
case AUTHORED_SHORT_URLS = 'AUTHORED_SHORT_URLS';
|
||||||
case DOMAIN_SPECIFIC = 'DOMAIN_SPECIFIC';
|
case DOMAIN_SPECIFIC = 'DOMAIN_SPECIFIC';
|
||||||
|
case NO_ORPHAN_VISITS = 'NO_ORPHAN_VISITS';
|
||||||
|
|
||||||
public function toFriendlyName(): string
|
public function toFriendlyName(array $meta): string
|
||||||
{
|
{
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::AUTHORED_SHORT_URLS => 'Author only',
|
self::AUTHORED_SHORT_URLS => 'Author only',
|
||||||
self::DOMAIN_SPECIFIC => 'Domain only',
|
self::DOMAIN_SPECIFIC => sprintf('Domain only: %s', Role::domainAuthorityFromMeta($meta)),
|
||||||
|
self::NO_ORPHAN_VISITS => 'No orphan visits',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +34,7 @@ enum Role: string
|
||||||
return match ($this) {
|
return match ($this) {
|
||||||
self::AUTHORED_SHORT_URLS => 'author-only',
|
self::AUTHORED_SHORT_URLS => 'author-only',
|
||||||
self::DOMAIN_SPECIFIC => 'domain-only',
|
self::DOMAIN_SPECIFIC => 'domain-only',
|
||||||
|
self::NO_ORPHAN_VISITS => 'no-orphan-visits',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,6 +43,7 @@ enum Role: string
|
||||||
return match ($role->role()) {
|
return match ($role->role()) {
|
||||||
self::AUTHORED_SHORT_URLS => new BelongsToApiKey($role->apiKey(), $context),
|
self::AUTHORED_SHORT_URLS => new BelongsToApiKey($role->apiKey(), $context),
|
||||||
self::DOMAIN_SPECIFIC => new BelongsToDomain(self::domainIdFromMeta($role->meta()), $context),
|
self::DOMAIN_SPECIFIC => new BelongsToDomain(self::domainIdFromMeta($role->meta()), $context),
|
||||||
|
default => Spec::andX(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +52,7 @@ enum Role: string
|
||||||
return match ($role->role()) {
|
return match ($role->role()) {
|
||||||
self::AUTHORED_SHORT_URLS => Spec::andX(new BelongsToApiKeyInlined($role->apiKey())),
|
self::AUTHORED_SHORT_URLS => Spec::andX(new BelongsToApiKeyInlined($role->apiKey())),
|
||||||
self::DOMAIN_SPECIFIC => Spec::andX(new BelongsToDomainInlined(self::domainIdFromMeta($role->meta()))),
|
self::DOMAIN_SPECIFIC => Spec::andX(new BelongsToDomainInlined(self::domainIdFromMeta($role->meta()))),
|
||||||
|
default => Spec::andX(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,24 @@ use Shlinkio\Shlink\Rest\ApiKey\Role;
|
||||||
|
|
||||||
class ApiKeyRole extends AbstractEntity
|
class ApiKeyRole extends AbstractEntity
|
||||||
{
|
{
|
||||||
public function __construct(private Role $roleName, private array $meta, private ApiKey $apiKey)
|
public function __construct(public readonly Role $role, private array $meta, public readonly ApiKey $apiKey)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use property access directly
|
||||||
|
*/
|
||||||
public function role(): Role
|
public function role(): Role
|
||||||
{
|
{
|
||||||
return $this->roleName;
|
return $this->role;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use property access directly
|
||||||
|
*/
|
||||||
|
public function apiKey(): ApiKey
|
||||||
|
{
|
||||||
|
return $this->apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function meta(): array
|
public function meta(): array
|
||||||
|
@ -27,9 +38,4 @@ class ApiKeyRole extends AbstractEntity
|
||||||
{
|
{
|
||||||
$this->meta = $newMeta;
|
$this->meta = $newMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function apiKey(): ApiKey
|
|
||||||
{
|
|
||||||
return $this->apiKey;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,14 +86,15 @@ class RoleTest extends TestCase
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test, DataProvider('provideRoleNames')]
|
#[Test, DataProvider('provideRoleNames')]
|
||||||
public function getsExpectedRoleFriendlyName(Role $role, string $expectedFriendlyName): void
|
public function getsExpectedRoleFriendlyName(Role $role, array $meta, string $expectedFriendlyName): void
|
||||||
{
|
{
|
||||||
self::assertEquals($expectedFriendlyName, $role->toFriendlyName());
|
self::assertEquals($expectedFriendlyName, $role->toFriendlyName($meta));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function provideRoleNames(): iterable
|
public static function provideRoleNames(): iterable
|
||||||
{
|
{
|
||||||
yield Role::AUTHORED_SHORT_URLS->value => [Role::AUTHORED_SHORT_URLS, 'Author only'];
|
yield Role::AUTHORED_SHORT_URLS->value => [Role::AUTHORED_SHORT_URLS, [], 'Author only'];
|
||||||
yield Role::DOMAIN_SPECIFIC->value => [Role::DOMAIN_SPECIFIC, 'Domain only'];
|
yield Role::DOMAIN_SPECIFIC->value => [Role::DOMAIN_SPECIFIC, ['authority' => 's.test'], 'Domain only: s.test'];
|
||||||
|
yield Role::NO_ORPHAN_VISITS->value => [Role::NO_ORPHAN_VISITS, [], 'No orphan visits'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue