1<?php
2/**
3 * Copyright since 2007 PrestaShop SA and Contributors
4 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
5 *
6 * NOTICE OF LICENSE
7 *
8 * This source file is subject to the Open Software License (OSL 3.0)
9 * that is bundled with this package in the file LICENSE.md.
10 * It is also available through the world-wide-web at this URL:
11 * https://opensource.org/licenses/OSL-3.0
12 * If you did not receive a copy of the license and are unable to
13 * obtain it through the world-wide-web, please send an email
14 * to license@prestashop.com so we can send you a copy immediately.
15 *
16 * DISCLAIMER
17 *
18 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
19 * versions in the future. If you wish to customize PrestaShop for your
20 * needs please refer to https://devdocs.prestashop.com/ for more information.
21 *
22 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
23 * @copyright Since 2007 PrestaShop SA and Contributors
24 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
25 */
26
27namespace PrestaShopBundle\Command;
28
29use PrestaShopBundle\Routing\Linter\Exception\LinterException;
30use PrestaShopBundle\Routing\Linter\SecurityAnnotationLinter;
31use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
32use Symfony\Component\Console\Input\InputArgument;
33use Symfony\Component\Console\Input\InputInterface;
34use Symfony\Component\Console\Output\OutputInterface;
35use Symfony\Component\Console\Style\SymfonyStyle;
36use Symfony\Component\Routing\Route;
37
38/**
39 * Checks if all admin routes have @AdminSecurity configured
40 *
41 * @see \PrestaShopBundle\Security\Annotation\AdminSecurity
42 */
43final class SecurityAnnotationLinterCommand extends ContainerAwareCommand
44{
45    public const ACTION_LIST_ALL = 'list';
46    public const ACTION_FIND_MISSING = 'find-missing';
47
48    /**
49     * @param string $expression
50     *
51     * @return string
52     */
53    public static function parseExpression($expression)
54    {
55        $pattern1 = '#\[(.*)\]#';
56        $pattern2 = '#is_granted\((.*),#';
57        $matches1 = [];
58        $matches2 = [];
59        preg_match($pattern1, $expression, $matches1);
60
61        if (count($matches1) > 1) {
62            return $matches1[1];
63        }
64        preg_match($pattern2, $expression, $matches2);
65        if (count($matches2) > 1) {
66            return $matches2[1];
67        }
68
69        return '';
70    }
71
72    /**
73     * @return string[]
74     */
75    public static function getAvailableActions()
76    {
77        return [self::ACTION_LIST_ALL, self::ACTION_FIND_MISSING];
78    }
79
80    /**
81     * {@inheritdoc}
82     */
83    public function configure()
84    {
85        $description = 'Checks if Back Office route controllers has configured Security annotations.';
86        $actionDescription = sprintf(
87            'Action to perform, must be one of: %s',
88            implode(', ', self::getAvailableActions())
89        );
90
91        $this
92            ->setName('prestashop:linter:security-annotation')
93            ->setDescription($description)
94            ->addArgument('action', InputArgument::REQUIRED, $actionDescription);
95    }
96
97    /**
98     * {@inheritdoc}
99     */
100    protected function execute(InputInterface $input, OutputInterface $output)
101    {
102        $actionToPerform = $input->getArgument('action');
103
104        if (!in_array($actionToPerform, self::getAvailableActions())) {
105            throw new \InvalidArgumentException(sprintf(
106                    'Action must be one of: %s',
107                    implode(', ', self::getAvailableActions())
108                )
109            );
110        }
111
112        switch ($actionToPerform) {
113            case self::ACTION_LIST_ALL:
114                $this->listAllRoutesAndRelatedPermissions($input, $output);
115                break;
116            case self::ACTION_FIND_MISSING:
117                $this->findRoutesWithMissingSecurityAnnotations($input, $output);
118                break;
119
120            default:
121                throw new \RuntimeException(sprintf('Unknown action %s', $actionToPerform));
122        }
123
124        return 0;
125    }
126
127    /**
128     * @param InputInterface $input
129     * @param OutputInterface $output
130     */
131    private function listAllRoutesAndRelatedPermissions(InputInterface $input, OutputInterface $output)
132    {
133        $container = $this->getContainer();
134
135        $adminRouteProvider = $container
136            ->get('prestashop.bundle.routing.linter.admin_route_provider');
137        /** @var SecurityAnnotationLinter $securityAnnotationLinter */
138        $securityAnnotationLinter = $container
139            ->get('prestashop.bundle.routing.linter.security_annotation_linter');
140
141        $listing = [];
142
143        foreach ($adminRouteProvider->getRoutes() as $routeName => $route) {
144            /* @var Route $route */
145            try {
146                $annotation = $securityAnnotationLinter->getRouteSecurityAnnotation($routeName, $route);
147                $listing[] = [
148                    $route->getDefault('_controller'),
149                    implode(', ', $route->getMethods()),
150                    'Yes',
151                    self::parseExpression($annotation->getExpression()),
152                ];
153            } catch (LinterException $e) {
154                $listing[] = [
155                    $route->getDefault('_controller'),
156                    implode(', ', $route->getMethods()),
157                    'No',
158                    '',
159                ];
160            }
161        }
162
163        $io = new SymfonyStyle($input, $output);
164        $headers = ['Controller action', 'Methods', 'Is secured ?', 'Permissions'];
165
166        $io->table($headers, $listing);
167    }
168
169    /**
170     * @param InputInterface $input
171     * @param OutputInterface $output
172     */
173    private function findRoutesWithMissingSecurityAnnotations(InputInterface $input, OutputInterface $output)
174    {
175        $container = $this->getContainer();
176
177        $adminRouteProvider = $container
178            ->get('prestashop.bundle.routing.linter.admin_route_provider');
179        /** @var SecurityAnnotationLinter $securityAnnotationLinter */
180        $securityAnnotationLinter = $container
181            ->get('prestashop.bundle.routing.linter.security_annotation_linter');
182
183        $notConfiguredRoutes = [];
184
185        /** @var Route $route */
186        foreach ($adminRouteProvider->getRoutes() as $routeName => $route) {
187            try {
188                $securityAnnotationLinter->lint($routeName, $route);
189            } catch (LinterException $e) {
190                $notConfiguredRoutes[] = $routeName;
191            }
192        }
193
194        $io = new SymfonyStyle($input, $output);
195
196        if (!empty($notConfiguredRoutes)) {
197            $io->warning(sprintf(
198                '%s routes are not configured with @AdminSecurity annotation:',
199                count($notConfiguredRoutes)
200            ));
201            $io->listing($notConfiguredRoutes);
202
203            return;
204        }
205
206        $io->success('All admin routes are secured with @AdminSecurity.');
207    }
208}
209