<?php
namespace Platform\SecurityBundle\Security;
use App\Model\Cacher;
use App\Security\Core\Authorization\Voter\TesterVoterInterface;
use Cms\FrontendBundle\Service\ResolverManager;
use Cms\ModuleBundle\Service\ModuleManager;
use Doctrine\Common\Util\ClassUtils;
use Platform\SecurityBundle\Entity\Identity\Account;
use Platform\SecurityBundle\Model\PlatformSubject;
use Platform\SecurityBundle\Service\Sentry;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
/**
* Class PlatformVoter
* @package Platform\SecurityBundle\Security
*/
abstract class PlatformVoter implements VoterInterface, TesterVoterInterface
{
/**
* @var array
*/
public static array $votes = [];
/**
* @var array
*/
public static array $polls = [];
/**
* @var Sentry
*/
protected Sentry $sentry;
/**
* @var Cacher|null
*/
protected static ?Cacher $cache = null;
/**
* @var ResolverManager
*/
protected ResolverManager $rm;
/**
* @var ParameterBagInterface
*/
protected ParameterBagInterface $params;
/**
* @var ModuleManager
*/
protected ModuleManager $moduleManager;
/**
* @param Sentry $sentry
* @param ResolverManager $rm
* @param ParameterBagInterface $params
* @param ModuleManager $moduleManager
*/
public function __construct(
Sentry $sentry,
ResolverManager $rm,
ParameterBagInterface $params,
ModuleManager $moduleManager
)
{
if ( ! self::$cache) {
self::$cache = new Cacher();
}
$this->sentry = $sentry;
$this->rm = $rm;
$this->params = $params;
$this->moduleManager = $moduleManager;
}
/**
* {@inheritdoc}
*/
public function vote(TokenInterface $token, $subject, array $attributes): int
{
// we are only interested if the user object is an account type
$account = $token->getUser();
if ( ! $account instanceof Account) {
return VoterInterface::ACCESS_ABSTAIN;
}
// normalize the subject
$subject = PlatformSubject::factory($subject);
// clean up the atttributes
$attrs = $this->attributes($attributes, $account, $subject);
// DEBUGGING
$timestamp = microtime(true);
// determine if we actually checked anything
// may affect the final result
$checking = 0;
// loop over the attributes that we have to check
foreach ($attrs as $attr) {
// generate a cache key
$lookup = implode('|', [
static::class,
$account->getId(),
$attr,
$subject->getContext() ? $subject->getContext()->getId() : null,
]);
// attempt to grab a cached value
$cached = self::$cache->get(__FUNCTION__, $lookup);
// a false return means the support check failed last time through
// continue on in that case as we don't have anything to do
// also, see if we need to skip the check
// flag in the cache if we do and haven't cached yet
if ($cached === false || ! $this->isSupported($account, $attr, $subject)) {
if ($cached !== false) {
self::$cache->set(__FUNCTION__, $lookup, false);
}
continue;
}
// we are running some kind of check
$checking++;
// DEBUGGING
$polled = microtime(true);
// if a miss, try to grab it
$result = $cached;
if ($result === null) {
// try checking
$result = $this->poll($account, $attr, $subject);
// set in the cache
self::$cache->set(__FUNCTION__, $lookup, $result);
}
// DEBUGGING
self::$polls[] = [
'time' => $polled,
'op' => str_replace(__NAMESPACE__.'\\Voter\\', '', static::class) . '::poll',
'cached' => is_int($cached),
'account' => $account->getEmail(),
'permission' => $attr,
'context' => $subject->getContext() ? [
ClassUtils::getClass($subject->getContext()),
$subject->getContext()->getId(),
$subject->getContext()->getName(),
] : null,
'thing' => $subject->getThing() ? [
ClassUtils::getClass($subject->getThing()),
$subject->getThing()->getId(),
] : null,
'access' => $result,
];
// if we're not abstaining, we want to return immediately
if ($result !== VoterInterface::ACCESS_ABSTAIN) {
// DEBUGGING
self::$votes[] = [
'time' => $timestamp,
'op' => str_replace(__NAMESPACE__.'\\Voter\\', '', static::class) . '::' . __FUNCTION__,
'cached' => false,
'account' => $account->getEmail(),
'permission' => $attrs,
'context' => $subject->getContext() ? [
ClassUtils::getClass($subject->getContext()),
$subject->getContext()->getId(),
$subject->getContext()->getName(),
] : null,
'thing' => $subject->getThing() ? [
ClassUtils::getClass($subject->getThing()),
$subject->getThing()->getId(),
] : null,
'access' => $result,
];
return $result;
}
}
// if all polls didn't abstain by this point, we should deny access as no other voters should need to trigger (all perms covered by this voter)
$result = ($checking === count($attrs))
? VoterInterface::ACCESS_DENIED
: VoterInterface::ACCESS_ABSTAIN;
// DEBUGGING
if ($checking) {
self::$votes[] = [
'time' => $timestamp,
'op' => str_replace(__NAMESPACE__.'\\Voter\\', '', static::class) . '::' . __FUNCTION__,
'cached' => false,
'account' => $account->getEmail(),
'permission' => $attrs,
'context' => $subject->getContext() ? [
ClassUtils::getClass($subject->getContext()),
$subject->getContext()->getId(),
$subject->getContext()->getName(),
] : null,
'thing' => $subject->getThing() ? [
ClassUtils::getClass($subject->getThing()),
$subject->getThing()->getId(),
] : null,
'access' => $result,
];
}
// abstain by default
return $result;
}
/**
* {@inheritdoc}
*/
public function test(TokenInterface $token, array $attributes): int
{
// we are only interested if the user object is an account type
$account = $token->getUser();
if ( ! $account instanceof Account) {
return VoterInterface::ACCESS_ABSTAIN;
}
// clean up the atttributes
$attrs = $this->attributes($attributes, $account);
// DEBUGGING
$timestamp = microtime(true);
// determine if we actually checked anything
// may affect the final result
$checking = 0;
// loop over the attributes that we have to check
foreach ($attrs as $attr) {
// if we have an alias still, that may be a problem
if ($this->sentry->isAlias($attr)) {
// if we are debugging, this is expected, so just skip it
if ($this->params->get('kernel.debug')) {
continue;
}
// not debugging, so unexpected...
throw new \LogicException();
}
// generate a cache key
$lookup = implode('|', [
static::class,
$account->getId(),
$attr,
]);
// attempt to grab a cached value
$cached = self::$cache->get(__FUNCTION__, $lookup);
// a false return means the support check failed last time through
// continue on in that case as we don't have anything to do
// also, see if we need to skip the check
// flag in the cache if we do and haven't cached yet
if ($cached === false || ! $this->isSupported($account, $attr)) {
if ($cached !== false) {
self::$cache->set(__FUNCTION__, $lookup, false);
}
continue;
}
// we are running some kind of check
$checking++;
// DEBUGGING
$polled = microtime(true);
// if a miss, try to grab it
$result = $cached;
if ($result === null) {
// try checking
$result = $this->try($account, $attr);
// set in the cache
self::$cache->set(__FUNCTION__, $lookup, $result);
}
// DEBUGGING
self::$polls[] = [
'time' => $polled,
'op' => str_replace(__NAMESPACE__.'\\Voter\\', '', static::class) . '::try',
'cached' => is_int($cached),
'account' => $account->getEmail(),
'permission' => $attr,
'context' => null,
'thing' => null,
'access' => $result,
];
// if we're not abstaining, we want to return immediately
if ($result !== VoterInterface::ACCESS_ABSTAIN) {
// DEBUGGING
self::$votes[] = [
'time' => $timestamp,
'op' => str_replace(__NAMESPACE__.'\\Voter\\', '', static::class) . '::' . __FUNCTION__,
'cached' => false,
'account' => $account->getEmail(),
'permission' => $attrs,
'context' => null,
'thing' => null,
'access' => $result,
];
return $result;
}
}
// if all polls didn't abstain by this point, we should deny access as no other voters should need to trigger (all perms covered by this voter)
$result = ($checking === count($attrs))
? VoterInterface::ACCESS_DENIED
: VoterInterface::ACCESS_ABSTAIN;
// DEBUGGING
if ($checking) {
self::$votes[] = [
'time' => $timestamp,
'op' => str_replace(__NAMESPACE__.'\\Voter\\', '', static::class) . '::' . __FUNCTION__,
'cached' => false,
'account' => $account->getEmail(),
'permission' => $attrs,
'context' => null,
'thing' => null,
'access' => $result,
];
}
// abstain by default
return $result;
}
/**
* @param Account $account
* @param string $permission
* @param PlatformSubject|null $subject
* @return int
*/
abstract protected function poll(
Account $account,
string $permission,
?PlatformSubject $subject
): int;
/**
* @param Account $account
* @param string $permission
* @return int
*/
abstract protected function try(
Account $account,
string $permission
): int;
/**
* @param array<string> $attributes
* @param Account $account
* @param PlatformSubject|null $subject
* @return array<string>
*/
protected function attributes(
array $attributes,
Account $account,
?PlatformSubject $subject = null
): array
{
return array_unique($attributes);
}
/**
* @param Account $account
* @param string $attribute
* @param PlatformSubject|null $subject
* @return bool
*/
abstract protected function supports(
Account $account,
string $attribute,
?PlatformSubject $subject = null
): bool;
/**
* @param Account $account
* @param string $attribute
* @param PlatformSubject|null $subject
* @return bool
*/
protected function isSupported(
Account $account,
string $attribute,
?PlatformSubject $subject = null
): bool
{
return (
$this->sentry->isPermission($attribute)
|| $this->sentry->isSpecialPermission($attribute)
|| $this->sentry->isInternalPermission($attribute)
)
&& $this->supports($account, $attribute, $subject)
;
}
}