-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[DI] Randomize private services #19683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace Symfony\Component\DependencyInjection\Compiler; | ||
|
||
use Symfony\Component\DependencyInjection\ContainerBuilder; | ||
use Symfony\Component\DependencyInjection\Alias; | ||
use Symfony\Component\DependencyInjection\Reference; | ||
|
||
/** | ||
* Randomizes private service identifiers, effectively making them unaccessable from outside. | ||
* | ||
* @author Roland Franssen <franssen.roland@gmail.com> | ||
*/ | ||
class RandomizePrivateServiceIdentifiers implements CompilerPassInterface | ||
{ | ||
private $idMap; | ||
private $randomizer; | ||
|
||
public function process(ContainerBuilder $container) | ||
{ | ||
// update private definitions + build id map | ||
$this->idMap = array(); | ||
foreach ($container->getDefinitions() as $id => $definition) { | ||
if (!$definition->isPublic()) { | ||
$this->idMap[$id] = $this->randomizer ? (string) call_user_func($this->randomizer, $id) : hash('sha256', mt_rand().$id); | ||
$definition->setPublic(true); | ||
} | ||
} | ||
|
||
// rename definitions | ||
$aliases = $container->getAliases(); | ||
foreach ($this->idMap as $oldId => $newId) { | ||
$container->setDefinition($newId, $container->getDefinition($oldId)); | ||
$container->removeDefinition($oldId); | ||
} | ||
|
||
// update referencing definitions | ||
foreach ($container->getDefinitions() as $id => $definition) { | ||
$definition->setArguments($this->processArguments($definition->getArguments())); | ||
$definition->setMethodCalls($this->processArguments($definition->getMethodCalls())); | ||
$definition->setProperties($this->processArguments($definition->getProperties())); | ||
$definition->setFactory($this->processFactory($definition->getFactory())); | ||
if (null !== ($decorated = $definition->getDecoratedService()) && isset($this->idMap[$decorated[0]])) { | ||
$definition->setDecoratedService($this->idMap[$decorated[0]], $decorated[1], $decorated[2]); | ||
} | ||
} | ||
|
||
// update alias map | ||
$aliases = $container->getAliases(); | ||
foreach ($container->getAliases() as $oldId => $alias) { | ||
if(isset($this->idMap[$oldId]) && $oldId === (string) $alias) { | ||
$container->setAlias(new Alias($this->idMap[$oldId], $alias->isPublic()), $this->idMap[$oldId]); | ||
} | ||
} | ||
|
||
// for BC | ||
$reflectionProperty = new \ReflectionProperty(ContainerBuilder::class, 'privates'); | ||
$reflectionProperty->setAccessible(true); | ||
$reflectionProperty->setValue($container, $this->idMap); | ||
} | ||
|
||
public function setRandomizer(callable $randomizer = null) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no need for this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added it to allow mocking in tests..(see diff), but many tests are affected so maybe it needs a different approach. But before continuing i want to know if this PR is viable. |
||
{ | ||
$this->randomizer = $randomizer; | ||
} | ||
|
||
private function processArguments(array $arguments) | ||
{ | ||
foreach ($arguments as $k => $argument) { | ||
if (is_array($argument)) { | ||
$arguments[$k] = $this->processArguments($argument); | ||
} elseif ($argument instanceof Reference && isset($this->idMap[$id = (string) $argument])) { | ||
$arguments[$k] = new Reference($this->idMap[$id], $argument->getInvalidBehavior()); | ||
} | ||
} | ||
|
||
return $arguments; | ||
} | ||
|
||
private function processFactory($factory) | ||
{ | ||
if (null === $factory || !is_array($factory) || !$factory[0] instanceof Reference) { | ||
return $factory; | ||
} | ||
if (isset($this->idMap[$id = (string) $factory[0]])) { | ||
$factory[0] = new Reference($this->idMap[$id], $factory[0]->getInvalidBehavior()); | ||
} | ||
|
||
return $factory; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -839,6 +839,7 @@ public function __construct() | |
|
||
$code .= "\n \$this->services = array();\n"; | ||
$code .= $this->addMethodMap(); | ||
$code .= $this->addPrivateServices(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needed to trigger deprecations in |
||
$code .= $this->addAliases(); | ||
|
||
$code .= <<<'EOF' | ||
|
@@ -892,6 +893,8 @@ private function addMethodMap() | |
/** | ||
* Adds the privates property definition. | ||
* | ||
* This method is intented for BC only. | ||
* | ||
* @return string | ||
*/ | ||
private function addPrivateServices() | ||
|
@@ -900,12 +903,13 @@ private function addPrivateServices() | |
return ''; | ||
} | ||
|
||
$reflectionProperty = new \ReflectionProperty(ContainerBuilder::class, 'privates'); | ||
$reflectionProperty->setAccessible(true); | ||
|
||
$code = ''; | ||
ksort($definitions); | ||
foreach ($definitions as $id => $definition) { | ||
if (!$definition->isPublic()) { | ||
$code .= ' '.var_export($id, true)." => true,\n"; | ||
} | ||
foreach ($reflectionProperty->getValue($this->container) as $originId => $id) { | ||
$code .= ' '.var_export($originId, true).' => '.var_export($id, true).",\n"; | ||
} | ||
|
||
if (empty($code)) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, randomization shouldn't be handled by a compiler pass, but by the PhpDumper directly. Otherwise it won't be applied without the compiler, and many apps won't have it enabled when migrating to 4.0
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I considered this. But eventually chose to separate it into a compiler pass (yes; making it an optional feature). Without compiling you have a normal container.
Thing is i dont really like the concept of coupliing privates with a container. The container shouldnt care about privates (it's just a normal service from that perspective). Hence
$this->privates
could be totally unneeded...No need to bypass
get
or checking private definitions inPhpDumper
. We work with the container :)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What you call a normal container doesn't have private services. The container shouldn't care about private services, right, but the PhpDumper should (and already does). So I still think that the PhpDumper should do this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We disagree :) i think we have compiler passes for that (optional) or otherwise
Container::compile
(by design).A dumper (eg.
PhpDumper
) dumps the given container as is. Like others do, YAML, XML etc. I dont know whyPhpDumer
would randomize your services, whereasXmlDumper
doesnt.Currently it only deals with privates to hack the dumped container (ie. output), saying; here's a list of privates for you to validate business logic (which it shouldnt care about :)).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because it makes no sense to randomize with
XmlDumper
and in fact would be wrong: it would rename services for no reason and make dumps harder and not accurate.What matters is having dumpers deal with private services, which
XmlDumper
does by adding thepublic="false"
attribute, andPhpDumper
doing some randomization. That! would make sense.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Reconsidered :)
My first thought was, yeah this totally counts for a
PhpDumper
as well. Which in some ways is still true, but i guess you're right. Randomizing could be the way a php dumper deals with privates by design (whereas xml addspublic="false"
👍 ).Do we need to consider DX here?
DebugPhpDumper
)Forget about randomizing, and compile different method names (thinkgetPrivate(.+)Service
)Container
work with "privates" again 😕There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure how DX is involved here. So maybe but I don't what it'd mean :)
maybe :) the goal is to have private services be hidden from the outside, no interference with the public interface.
How it's done is an implementation detail We should chose the simplest one... :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True.
DX, in the sense that you may not want to have random id's in the output, or at least customize/mock it in some way (think
setRandomizer
).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then maybe the simplest and most DX friendly is to make private services leak a bit by reserving their id, throwing an exception whenever one uses or fetches an id that collides with a private service.