* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ namespace JMS\SecurityExtraBundle\DependencyInjection\Compiler; use JMS\SecurityExtraBundle\Analysis\ServiceAnalyzer; use JMS\SecurityExtraBundle\Metadata\ServiceMetadata; use JMS\SecurityExtraBundle\Metadata\ClassMetadata; use JMS\SecurityExtraBundle\Generator\ProxyClassGenerator; use JMS\SecurityExtraBundle\Metadata\Driver\DriverChain; use \ReflectionClass; use \ReflectionMethod; use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Security\Core\SecurityContext; /** * Modifies the container, and sets the proxy classes where needed * * @author Johannes M. Schmitt */ class SecureMethodInvocationsPass implements CompilerPassInterface { private $cacheDir; private $generator; private $cacheMetadata; public function __construct($cacheDir) { $cacheDir .= '/security/'; if (!file_exists($cacheDir)) { @mkdir($cacheDir, 0777, true); } if (false === is_writable($cacheDir)) { throw new \RuntimeException('Cannot write to cache folder: '.$cacheDir); } $this->cacheDir = $cacheDir; if (!file_exists($cacheDir.'SecurityProxies/')) { @mkdir($cacheDir.'SecurityProxies/', 0777, true); } if (false === is_writeable($cacheDir.'SecurityProxies/')) { throw new \RuntimeException('Cannot write to cache folder: '.$cacheDir.'SecurityProxies/'); } $this->generator = new ProxyClassGenerator(); $this->createOrLoadCacheMetadata(); } /** * {@inheritDoc} */ public function process(ContainerBuilder $container) { if (!$container->hasDefinition('security.access.method_interceptor')) { return; } $services = $container->findTaggedServiceIds('security.secure_service'); $secureNotAll = !$container->getParameter('security.extra.secure_all_services'); $parameterBag = $container->getParameterBag(); foreach ($container->getDefinitions() as $id => $definition) { if ($secureNotAll && !isset($services[$id])) { continue; } if ((null === $class = $definition->getClass()) || !class_exists($class)) { if ($secureNotAll) { throw new \RuntimeException(sprintf('Could not find class "%s" for "%s".', $class, $id)); } continue; } if (null !== $definition->getFactoryMethod() || $definition->isAbstract() || $definition->isSynthetic()) { if ($secureNotAll) { throw new \RuntimeException(sprintf('You cannot secure service "%s", because it is either created by a factory, or an abstract/synthetic service.', $id)); } continue; } $this->processDefinition($container, $id, $definition); } $this->writeCacheMetadata(); } private function processDefinition(ContainerBuilder $container, $id, Definition $definition) { if ($this->needsReAssessment($id, $definition)) { $analyzer = new ServiceAnalyzer($definition->getClass(), $container->get('annotation_reader')); $analyzer->analyze(); $files = array(); foreach ($analyzer->getFiles() as $file) { $container->addResource($file = new FileResource($file)); $files[] = $file; } $metadata = $analyzer->getMetadata(); $proxyClass = $path = null; if (true === $metadata->isProxyRequired()) { list($newClassName, $content) = $this->generator->generate($definition, $metadata); file_put_contents($path = $this->cacheDir.'SecurityProxies/'.$newClassName.'.php', $content); $definition->setFile($path); $definition->setClass($proxyClass = 'SecurityProxies\\'.$newClassName); $definition->addMethodCall('jmsSecurityExtraBundle__setMethodSecurityInterceptor', array(new Reference('security.access.method_interceptor'))); } else if (isset($this->cacheMetadata[$id]['proxy_class'])) { @unlink($this->cacheDir.$this->cacheMetadata[$id]['proxy_class'].'.php'); } $this->cacheMetadata[$id] = array( 'class' => $definition->getClass(), 'proxy_class' => $proxyClass, 'proxy_file' => $path, 'analyze_time' => time(), 'files' => $files, ); } else { foreach ($this->cacheMetadata[$id]['files'] as $file) { $container->addResource($file); } if (null !== $proxyClass = $this->cacheMetadata[$id]['proxy_class']) { $definition->setFile($this->cacheMetadata[$id]['proxy_file']); $definition->setClass($proxyClass); $definition->addMethodCall('jmsSecurityExtraBundle__setMethodSecurityInterceptor', array(new Reference('security.access.method_interceptor'))); } } } private function needsReAssessment($id, Definition $definition) { if (!isset($this->cacheMetadata[$id])) { return true; } $metadata = $this->cacheMetadata[$id]; if ($metadata['class'] !== $definition->getClass()) { return true; } $lastAnalyzed = $metadata['analyze_time']; foreach ($metadata['files'] as $file) { if (false === $file->isFresh($lastAnalyzed)) { return true; } } return false; } private function createOrLoadCacheMetadata() { if (file_exists($this->cacheDir.'cache.meta')) { if (!is_readable($this->cacheDir.'cache.meta')) { throw new \RuntimeException('Cannot load security cache meta data from: '.$this->cacheDir.'cache.meta'); } $this->cacheMetadata = unserialize(file_get_contents($this->cacheDir.'cache.meta')); } else { set_time_limit(0); $this->cacheMetadata = array(); } } private function writeCacheMetadata() { if (false === file_put_contents($this->cacheDir.'cache.meta', serialize($this->cacheMetadata))) { throw new \RuntimeException('Could not write to cache file: '.$this->cacheDir.'cache.meta'); } } }