Thanks to visit codestin.com
Credit goes to github.com

Skip to content

[Security] Handle placeholders in role hierarchy #52099

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

Open
wants to merge 5 commits into
base: 7.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/SecurityBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ CHANGELOG
6.4
---

* Add the `debug:roles` command to debug role hierarchy
* Deprecate `Security::ACCESS_DENIED_ERROR`, `AUTHENTICATION_ERROR` and `LAST_USERNAME` constants, use the ones on `SecurityRequestAttributes` instead
* Allow an array of `pattern` in firewall configuration
* Add `$badges` argument to `Security::login`
Expand Down
207 changes: 207 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/Command/DebugRolesCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Command;

use Symfony\Bundle\SecurityBundle\Debug\DebugRoleHierarchy;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;

#[AsCommand(name: 'debug:roles', description: 'Debug the role hierarchy configuration.')]
final class DebugRolesCommand extends Command
{
public function __construct(private readonly RoleHierarchyInterface $roleHierarchy)
{
parent::__construct();
}

protected function configure(): void
{
$this->setHelp(<<<EOF
This <info>%command.name%</info> command display the current role hierarchy:

<info>php %command.full_name%</info>

You can pass one or multiple role names to display the effective roles:

<info>php %command.full_name% ROLE_USER</info>

To get a tree view of the inheritance, use the <info>tree</info> option:

<info>php %command.full_name% --tree</info>
<info>php %command.full_name% ROLE_USER --tree</info>

<comment>Note:</comment> With a custom implementation for <info>security.role_hierarchy</info>, the <info>--tree</info> option is ignored and the <info>roles</info> argument is required.

EOF
)
->setDefinition([
new InputArgument('roles', ($this->isBuiltInRoleHierarchy() ? InputArgument::OPTIONAL : InputArgument::REQUIRED) | InputArgument::IS_ARRAY, 'The role(s) to resolve'),
new InputOption('tree', 't', InputOption::VALUE_NONE, 'Show the hierarchy in a tree view'),
]);
}

protected function initialize(InputInterface $input, OutputInterface $output): void
{
if (!$this->isBuiltInRoleHierarchy()) {
$io = new SymfonyStyle($input, $output);

if ($input->getOption('tree')) {
$io->warning('Ignoring option "--tree" because of a custom role hierarchy implementation.');
$input->setOption('tree', null);
}
}
}

protected function interact(InputInterface $input, OutputInterface $output): void
{
if (!$this->isBuiltInRoleHierarchy() && empty($input->getArgument('roles'))) {
$io = new SymfonyStyle($input, $output);

$roles[] = $io->ask('Enter a role to debug', validator: function (?string $role) {
$role = trim($role);
if (empty($role)) {
throw new \RuntimeException('You must enter a non empty role name.');
}

return $role;
});
while ($role = trim($io->ask('Add another role? (press enter to skip)') ?? '')) {
$roles[] = $role;
}

$input->setArgument('roles', $roles);
}
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$roles = $input->getArgument('roles');

if (empty($roles)) {
// Full configuration output
$io->title('Current role hierarchy configuration:');

if ($input->getOption('tree')) {
$this->outputTree($io, $this->getBuiltInDebugHierarchy()->getHierarchy());
} else {
$this->outputMap($io, $this->getBuiltInDebugHierarchy()->getMap());
}

$io->comment('To show reachable roles for a given role, re-run this command with role names. (e.g. <comment>debug:roles ROLE_USER</comment>)');

return self::SUCCESS;
}

// Matching roles output
$io->title(sprintf('Effective roles for %s:', implode(', ', array_map(fn ($v) => sprintf('<info>%s</info>', $v), $roles))));

if ($input->getOption('tree')) {
$this->outputTree($io, $this->getBuiltInDebugHierarchy()->getHierarchy($roles));
} else {
$io->listing($this->roleHierarchy->getReachableRoleNames($roles));
}

return self::SUCCESS;
}

private function outputMap(OutputInterface $output, array $map): void
{
foreach ($map as $main => $roles) {
if ($this->getBuiltInDebugHierarchy()->isPlaceholder($main)) {
$main = $this->stylePlaceholder($main);
}

$output->writeln(sprintf('%s:', $main));
foreach ($roles as $r) {
$output->writeln(sprintf(' - %s', $r));
}
$output->writeln('');
}
}

private function outputTree(OutputInterface $output, array $tree): void
{
foreach ($tree as $role => $hierarchy) {
$output->writeln($this->generateTree($role, $hierarchy));
$output->writeln('');
}
}

/**
* Generates a tree representation, line by line, in the tree unix style.
*
* Example output:
*
* ROLE_A
* └── ROLE_B
*
* ROLE_C
* ├── ROLE_A
* │ └── ROLE_B
* └── ROLE_D
*/
private function generateTree(string $name, array $tree, string $indent = '', bool $last = true, bool $root = true): \Generator
{
if ($this->getBuiltInDebugHierarchy()->isPlaceholder($name)) {
$name = $this->stylePlaceholder($name);
}

if ($root) {
// Yield root node as it is
yield $name;
} else {
// Generate line in the tree:
// Line: [indent]├── [name]
// Last line: [indent]└── [name]
yield sprintf('%s%s%s %s', $indent, $last ? "\u{2514}" : "\u{251c}", str_repeat("\u{2500}", 2), $name);

// Update indent for next nested:
// Append "| " for a nested tree
// Append " " for last nested tree
$indent .= ($last ? ' ' : "\u{2502}").str_repeat(' ', 3);
}

$i = 0;
$count = \count($tree);
foreach ($tree as $key => $value) {
yield from $this->generateTree($key, $value, $indent, $i === $count - 1, false);
++$i;
}
}

private function stylePlaceholder(string $role): string
{
return sprintf('<info>%s</info>', $role);
}

private function isBuiltInRoleHierarchy(): bool
{
return $this->roleHierarchy instanceof DebugRoleHierarchy;
}

private function getBuiltInDebugHierarchy(): DebugRoleHierarchy
{
if (!$this->roleHierarchy instanceof DebugRoleHierarchy) {
throw new \LogicException('Cannot use the built-in debug hierarchy with a custom implementation.');
}

return $this->roleHierarchy;
}
}
105 changes: 105 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/Debug/DebugRoleHierarchy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\Debug;

use Symfony\Component\Security\Core\Role\RoleHierarchy;

/**
* Extended Role Hierarchy to access inner configuration data.
*
* @author Nicolas Rigaud <[email protected]>
*
* @internal
*/
final class DebugRoleHierarchy extends RoleHierarchy
{
private readonly array $debugHierarchy;

public function __construct(array $hierarchy)
{
$this->debugHierarchy = $hierarchy;

parent::__construct($hierarchy);
}

/**
* Get the hierarchy tree.
*
* Example output:
*
* [
* 'ROLE_A' => [
* 'ROLE_B' => [],
* 'ROLE_C' => [
* 'ROLE_D' => []
* ]
* ],
* 'ROLE_C' => [
* 'ROLE_D' => []
* ]
* ]
*
* @param string[] $roles Optionally restrict the tree to these roles
*
* @return array<string,array<string,array>>
*/
public function getHierarchy(array $roles = []): array
{
$hierarchy = [];

foreach ($roles ?: array_keys($this->debugHierarchy) as $role) {
$hierarchy += $this->buildHierarchy([$role]);
}

return $hierarchy;
}

/**
* Get the computed role map.
*
* @return array<string,string[]>
*/
public function getMap(): array
{
return $this->map;
}

/**
* Return whether a given role is processed as a placeholder.
*/
public function isPlaceholder(string $role): bool
{
return \in_array($role, array_keys($this->rolePlaceholdersPatterns));
}

private function buildHierarchy(array $roles, array &$visited = []): array
{
$tree = [];
foreach ($roles as $role) {
$visited[] = $role;

$tree[$role] = [];

// Get placeholders matches
$placeholders = array_diff($this->getMatchingPlaceholders([$role]), $visited) ?? [];
array_push($visited, ...$placeholders);
$tree[$role] += $this->buildHierarchy($placeholders, $visited);

// Get regular inherited roles
$inherited = array_diff($this->debugHierarchy[$role] ?? [], $visited);
array_push($visited, ...$inherited);
$tree[$role] += $this->buildHierarchy($inherited, $visited);
}

return $tree;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;

use Symfony\Bundle\SecurityBundle\Debug\DebugRoleHierarchy;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\Security\Core\Role\RoleHierarchy;

class RegisterDebugRoleHierarchyPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
if (!$container->hasDefinition('security.role_hierarchy')) {
$container->removeDefinition('security.command.debug_role_hierarchy');

return;
}

$definition = $container->findDefinition('security.role_hierarchy');

if (RoleHierarchy::class === $definition->getClass()) {
$hierarchy = $definition->getArgument(0);
$definition = new Definition(DebugRoleHierarchy::class, [$hierarchy]);
}
$container->setDefinition('debug.security.role_hierarchy', $definition);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\DependencyInjection\Loader\Configurator;

use Symfony\Bundle\SecurityBundle\Command\DebugFirewallCommand;
use Symfony\Bundle\SecurityBundle\Command\DebugRolesCommand;

return static function (ContainerConfigurator $container) {
$container->services()
Expand All @@ -24,5 +25,10 @@
false,
])
->tag('console.command', ['command' => 'debug:firewall'])
->set('security.command.debug_role_hierarchy', DebugRolesCommand::class)
->args([
service('debug.security.role_hierarchy'),
])
->tag('console.command', ['command' => 'debug:roles'])
;
};
3 changes: 3 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/SecurityBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\CleanRememberMeVerifierPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\MakeFirewallsEventDispatcherTraceablePass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfFeaturesPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterDebugRoleHierarchyPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterEntryPointPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterGlobalSecurityEventListenersPass;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass;
Expand Down Expand Up @@ -105,5 +106,7 @@ public function build(ContainerBuilder $container): void

// must be registered before DecoratorServicePass
$container->addCompilerPass(new MakeFirewallsEventDispatcherTraceablePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);

$container->addCompilerPass(new RegisterDebugRoleHierarchyPass(), PassConfig::TYPE_OPTIMIZE);
}
}
Loading