├── docs ├── .gitignore ├── source │ ├── index.rst │ ├── overview.rst │ └── conf.py ├── requirements.txt └── Makefile ├── .gitignore ├── Exceptions └── LogicalAuthorizationException.php ├── PermissionTypes ├── Flag │ ├── Exceptions │ │ └── FlagNotRegisteredException.php │ ├── FlagInterface.php │ ├── Flags │ │ ├── UserHasAccount.php │ │ ├── UserCanBypassAccess.php │ │ └── UserIsAuthor.php │ ├── FlagManagerInterface.php │ └── FlagManager.php ├── Host │ └── Host.php ├── Method │ └── Method.php ├── Ip │ └── Ip.php └── Role │ └── Role.php ├── Tests ├── Fixtures │ ├── BypassAccessChecker │ │ ├── AlwaysAllow.php │ │ └── AlwaysDeny.php │ ├── ModelDecorator │ │ └── ModelDecorator.php │ ├── PermissionTypes │ │ ├── TestType.php │ │ └── TestFlag.php │ ├── Controller │ │ ├── XmlController.php │ │ ├── YmlController.php │ │ └── DefaultController.php │ ├── Security │ │ └── User │ │ │ └── CustomUserProvider.php │ ├── Model │ │ ├── TestModelHasAccountNoInterface.php │ │ ├── ErroneousModel.php │ │ ├── TestModelNoBypass.php │ │ ├── TestModelRoleAuthor.php │ │ ├── TestModelBoolean.php │ │ ├── ErroneousUser.php │ │ └── TestUser.php │ └── ControllerDir │ │ └── DefaultController.php ├── config │ ├── security.yml │ ├── routing.xml │ ├── services.yml │ ├── routing.yml │ └── config.yml ├── bootstrap.php ├── AppKernel.php └── Functional │ └── Services │ ├── LogicalAuthorizationTwigTest.php │ ├── LogicalAuthorizationBase.php │ ├── LogicalAuthorizationModelTest.php │ └── LogicalAuthorizationRouteTest.php ├── Resources ├── views │ ├── DataCollector │ │ └── permission_checks.html.twig │ └── Icon │ │ └── icon.svg └── config │ ├── services.debug.yml │ └── services.yml ├── .travis.yml ├── Interfaces ├── ModelDecoratorInterface.php ├── ModelInterface.php └── UserInterface.php ├── Routing ├── AnnotationFileLoader.php ├── RouteInterface.php ├── AnnotationDirectoryLoader.php ├── AnnotationClassLoader.php ├── YamlLoader.php ├── Route.php └── schema │ └── routing │ └── routing-1.0.xsd ├── Event ├── AddPermissionsEventInterface.php └── AddPermissionsEvent.php ├── Annotation └── Routing │ └── Permissions.php ├── OrdermindLogicalAuthorizationBundle.php ├── Services ├── HelperInterface.php ├── PermissionTreeBuilderInterface.php ├── LogicalAuthorizationInterface.php ├── LogicalAuthorizationRouteInterface.php ├── LogicalAuthorization.php ├── Helper.php ├── LogicalPermissionsProxyInterface.php ├── LogicalAuthorizationModelInterface.php ├── LogicalPermissionsProxy.php ├── PermissionTreeBuilder.php └── LogicalAuthorizationRoute.php ├── DependencyInjection ├── Configuration.php ├── Compiler │ ├── FlagRegistrationPass.php │ └── PermissionTypeRegistrationPass.php └── LogAuthExtension.php ├── EventListener ├── AddAppConfigPermissions.php ├── PermissionsCacheWarmer.php └── AddRoutePermissions.php ├── phpunit.xml.dist ├── LICENSE ├── BypassAccessChecker └── BypassAccessChecker.php ├── composer.json ├── README.md ├── DataCollector ├── CollectorInterface.php └── Collector.php ├── Security └── ExpressionProvider.php ├── Command └── DumpPermissionTreeCommand.php └── Twig └── LogicalAuthorizationExtension.php /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /build/ 2 | *.kate-swp 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .php_cs.cache 2 | /app 3 | /vendor 4 | /composer* 5 | !/composer.json 6 | /*.sh 7 | /Tests/cache 8 | /Tests/logs 9 | -------------------------------------------------------------------------------- /Exceptions/LogicalAuthorizationException.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | {{ check.permissions | json_encode }} 5 | 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | env: 3 | global: 4 | - MIN_PHP=7.1.3 5 | php: 6 | - '7.1' 7 | - '7.2' 8 | - '7.3' 9 | cache: 10 | directories: 11 | - cached 12 | install: composer install 13 | before_script: "[ -e cached/phpunit.phar ] || wget -O cached/phpunit.phar https://phar.phpunit.de/phpunit-7.phar && chmod u+x cached/phpunit.phar" 14 | script: cached/phpunit.phar -c ./phpunit.xml.dist 15 | notifications: 16 | email: false 17 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Logical Authorization Bundle documentation master file, created by 2 | sphinx-quickstart on Fri Jul 6 09:18:09 2018. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Logical Authorization Bundle's documentation! 7 | ======================================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | overview 14 | quickstart 15 | -------------------------------------------------------------------------------- /Interfaces/ModelDecoratorInterface.php: -------------------------------------------------------------------------------- 1 | =4.2b1 6 | Pygments==2.0.2 7 | Sphinx==1.3.1 8 | sphinxcontrib-phpdomain==0.1.4 9 | alabaster==0.7.6 10 | argh==0.26.1 11 | argparse==1.2.1 12 | docutils==0.12 13 | html5lib==0.999 14 | meld3==0.6.10 15 | pathtools==0.1.2 16 | pytz==2015.4 17 | recommonmark==0.2.0 18 | six==1.5.2 19 | snowballstemmer==1.2.0 20 | sphinx-autobuild==0.5.2 21 | sphinx-rtd-theme==0.4.0 22 | sphinx-tabs==1.1.7 23 | wheel==0.24.0 24 | -------------------------------------------------------------------------------- /Tests/Fixtures/ModelDecorator/ModelDecorator.php: -------------------------------------------------------------------------------- 1 | model = $model; 14 | } 15 | 16 | public function getModel() 17 | { 18 | return $this->model; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Tests/Fixtures/PermissionTypes/TestType.php: -------------------------------------------------------------------------------- 1 | name; 15 | } 16 | 17 | public function setName($name) 18 | { 19 | $this->name = $name; 20 | } 21 | 22 | public function checkFlag(array $context): bool 23 | { 24 | return true; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Routing/RouteInterface.php: -------------------------------------------------------------------------------- 1 | boot(); 22 | -------------------------------------------------------------------------------- /Event/AddPermissionsEventInterface.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = LogicalAuthorizationBundle 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /Resources/config/services.debug.yml: -------------------------------------------------------------------------------- 1 | services: 2 | logauth.command.dump_permissions: 3 | class: Ordermind\LogicalAuthorizationBundle\Command\DumpPermissionTreeCommand 4 | arguments: ['@logauth.service.permission_tree_builder'] 5 | tags: 6 | - {name: console.command} 7 | 8 | logauth.debug.collector: 9 | class: Ordermind\LogicalAuthorizationBundle\DataCollector\Collector 10 | arguments: ['@logauth.service.permission_tree_builder', '@logauth.service.logical_permissions_proxy'] 11 | tags: 12 | - {name: data_collector, template: '@OrdermindLogicalAuthorization/DataCollector/collector.html.twig', id: logauth.collector} 13 | public: false 14 | -------------------------------------------------------------------------------- /PermissionTypes/Flag/FlagInterface.php: -------------------------------------------------------------------------------- 1 | permissions = $data['value']; 24 | } 25 | 26 | /** 27 | * Gets the permission tree for this route 28 | * 29 | * @return array|string|bool The permission tree for this route 30 | */ 31 | public function getPermissions() 32 | { 33 | return $this->permissions; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Interfaces/ModelInterface.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new PermissionTypeRegistrationPass()); 17 | $container->addCompilerPass(new FlagRegistrationPass()); 18 | } 19 | 20 | public function getContainerExtension() 21 | { 22 | return new LogAuthExtension(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Services/HelperInterface.php: -------------------------------------------------------------------------------- 1 | root('logauth'); 23 | 24 | $rootNode 25 | ->children() 26 | ->variableNode('permissions')->end() 27 | ->end() 28 | ; 29 | 30 | return $treeBuilder; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Services/PermissionTreeBuilderInterface.php: -------------------------------------------------------------------------------- 1 | has('logauth.permission_type.flag')) { 21 | return; 22 | } 23 | $definition = $container->findDefinition('logauth.permission_type.flag'); 24 | $taggedServices = $container->findTaggedServiceIds('logauth.tag.permission_type.flag'); 25 | foreach ($taggedServices as $id => $tags) { 26 | $definition->addMethodCall('addFlag', array(new Reference($id))); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /EventListener/AddAppConfigPermissions.php: -------------------------------------------------------------------------------- 1 | config = $config; 26 | } 27 | 28 | /** 29 | * Event listener callback for adding permissions to the tree 30 | * 31 | * @param Ordermind\LogicalAuthorizationBundle\Event\AddPermissionsEventInterface $event 32 | */ 33 | public function onAddPermissions(AddPermissionsEventInterface $event) 34 | { 35 | if (!empty($this->config['permissions'])) { 36 | $event->insertTree($this->config['permissions']); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /DependencyInjection/Compiler/PermissionTypeRegistrationPass.php: -------------------------------------------------------------------------------- 1 | has('logauth.service.logical_permissions_proxy')) { 21 | return; 22 | } 23 | $definition = $container->findDefinition('logauth.service.logical_permissions_proxy'); 24 | $taggedServices = $container->findTaggedServiceIds('logauth.tag.permission_type'); 25 | foreach ($taggedServices as $id => $tags) { 26 | $definition->addMethodCall('addType', array(new Reference($id))); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ./Tests/ 20 | 21 | 22 | 23 | 24 | 25 | ./ 26 | 27 | ./Resources 28 | ./Tests 29 | ./vendor 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Tests/AppKernel.php: -------------------------------------------------------------------------------- 1 | getEnvironment(), array('test'))) { 15 | $bundles[] = new Symfony\Bundle\FrameworkBundle\FrameworkBundle(); 16 | $bundles[] = new Symfony\Bundle\MonologBundle\MonologBundle(); 17 | $bundles[] = new Symfony\Bundle\SecurityBundle\SecurityBundle(); 18 | $bundles[] = new Symfony\Bundle\TwigBundle\TwigBundle(); 19 | $bundles[] = new Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(); 20 | $bundles[] = new Ordermind\LogicalAuthorizationBundle\OrdermindLogicalAuthorizationBundle(); 21 | } 22 | return $bundles; 23 | } 24 | 25 | public function registerContainerConfiguration(LoaderInterface $loader) 26 | { 27 | $loader->load(__DIR__.'/config/config.yml'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kristofer Tengström 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Tests/config/routing.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Controller\XmlController::routeXmlAction 9 | 10 | ROLE_ADMIN 11 | 12 | 13 | 14 | Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Controller\XmlController::routeXmlAllowedAction 15 | TRUE 16 | 17 | 18 | Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Controller\XmlController::routeXmlDeniedAction 19 | FALSE 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tests/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | cache.adapter.null: 3 | class: Symfony\Component\Cache\Adapter\NullAdapter 4 | abstract: true 5 | arguments: [~, ~, ~] 6 | tags: 7 | - {name: cache.pool, clearer: cache.default_clearer} 8 | 9 | custom_user_provider: 10 | class: Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Security\User\CustomUserProvider 11 | 12 | test.logauth.service.logical_permissions_proxy: 13 | alias: logauth.service.logical_permissions_proxy 14 | public: true 15 | 16 | test.logauth.service.helper: 17 | alias: logauth.service.helper 18 | public: true 19 | 20 | test.logauth.service.permission_tree_builder: 21 | alias: logauth.service.permission_tree_builder 22 | public: true 23 | 24 | test.logauth.service.logauth_route: 25 | alias: logauth.service.logauth_route 26 | public: true 27 | 28 | test.logauth.service.logauth_model: 29 | alias: logauth.service.logauth_model 30 | public: true 31 | 32 | test.logauth.service.logauth: 33 | alias: logauth.service.logauth 34 | public: true 35 | -------------------------------------------------------------------------------- /BypassAccessChecker/BypassAccessChecker.php: -------------------------------------------------------------------------------- 1 | lpProxy = $lpProxy; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function checkBypassAccess($context) 33 | { 34 | return $this->lpProxy->checkAccess(['flag' => 'user_can_bypass_access'], $context, false); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /EventListener/PermissionsCacheWarmer.php: -------------------------------------------------------------------------------- 1 | treeBuilder = $treeBuilder; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function warmUp($cacheDir) 33 | { 34 | $this->treeBuilder->getTree(true); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | public function isOptional(): bool 41 | { 42 | return true; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Tests/config/routing.yml: -------------------------------------------------------------------------------- 1 | test_routes: 2 | resource: "@OrdermindLogicalAuthorizationBundle/Tests/Fixtures/Controller/DefaultController.php" 3 | type: logauth_annotation 4 | prefix: /test 5 | 6 | test_routes_dir: 7 | resource: "@OrdermindLogicalAuthorizationBundle/Tests/Fixtures/ControllerDir/" 8 | type: logauth_annotation 9 | prefix: /test 10 | 11 | xml_routes: 12 | resource: "@OrdermindLogicalAuthorizationBundle/Tests/config/routing.xml" 13 | type: logauth_xml 14 | prefix: /test 15 | 16 | yml_route: 17 | path: /test/route-yml 18 | defaults: { _controller: Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Controller\YmlController::routeYmlAction } 19 | permissions: 20 | role: ROLE_ADMIN 21 | 22 | yml_route_allowed: 23 | path: /test/route-yml-allowed 24 | defaults: { _controller: Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Controller\YmlController::routeYmlAllowedAction } 25 | permissions: 26 | true 27 | 28 | yml_route_denied: 29 | path: /test/route-yml-denied 30 | defaults: { _controller: Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Controller\YmlController::routeYmlDeniedAction } 31 | permissions: 32 | false 33 | 34 | 35 | -------------------------------------------------------------------------------- /Services/LogicalAuthorizationInterface.php: -------------------------------------------------------------------------------- 1 | reader->getMethodAnnotations($method) as $configuration) { 24 | if ($configuration instanceof Permissions) { 25 | $route->setPermissions($configuration->getPermissions()); 26 | } 27 | } 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | protected function createRoute($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition): Route 34 | { 35 | return new Route($path, $defaults, $requirements, $options, $host, $schemes, $methods, $condition); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /DependencyInjection/LogAuthExtension.php: -------------------------------------------------------------------------------- 1 | processConfiguration($configuration, $configs); 25 | $container->setParameter('logauth.config', $processedConfig); 26 | 27 | $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 28 | $loader->load('services.yml'); 29 | 30 | if ($container->getParameter('kernel.debug')) { 31 | $loader->load('services.debug.yml'); 32 | } 33 | } 34 | 35 | /** 36 | * {@inheritdoc} 37 | */ 38 | public function getAlias(): string 39 | { 40 | return 'logauth'; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PermissionTypes/Flag/Flags/UserHasAccount.php: -------------------------------------------------------------------------------- 1 | getName())); 33 | } 34 | 35 | $user = $context['user']; 36 | if (is_string($user)) { //Anonymous user 37 | return false; 38 | } 39 | 40 | return true; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /PermissionTypes/Flag/FlagManagerInterface.php: -------------------------------------------------------------------------------- 1 | =4.8", 35 | "sensio/framework-extra-bundle": ">=3.0", 36 | "symfony/monolog-bundle": ">=3.0", 37 | "symfony/twig-bundle": ">=3.0", 38 | "symfony/browser-kit": ">=4.0", 39 | "squizlabs/php_codesniffer": ">=3.0", 40 | "escapestudios/symfony2-coding-standard": ">=3.0" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Services/LogicalAuthorizationRouteInterface.php: -------------------------------------------------------------------------------- 1 | ['route-path-1' => 'route-path-1', ...], route_patterns => ['^route-pattern-1' => '^route-pattern-1', ...]]. 16 | * 17 | * @param object|string $user (optional) Either a user object or a string to signify an anonymous user. If no user is supplied, the current user will be used. 18 | * 19 | * @return array A map of available routes and patterns. 20 | */ 21 | public function getAvailableRoutes($user = null): array; 22 | 23 | /** 24 | * Checks route access for a given user. 25 | * 26 | * If something goes wrong an error will be logged and the method will return FALSE. If no permissions are defined for the provided route it will return TRUE. 27 | * 28 | * @param string $routeName The name of the route 29 | * @param object|string $user (optional) Either a user object or a string to signify an anonymous user. If no user is supplied, the current user will be used. 30 | * 31 | * @return bool TRUE if access is granted or FALSE if access is denied. 32 | */ 33 | public function checkRouteAccess(string $routeName, $user = null): bool; 34 | } 35 | -------------------------------------------------------------------------------- /EventListener/AddRoutePermissions.php: -------------------------------------------------------------------------------- 1 | router = $router; 30 | } 31 | 32 | /** 33 | * Event listener callback for adding permissions to the tree 34 | * 35 | * @param Ordermind\LogicalAuthorizationBundle\Event\AddPermissionsEventInterface $event 36 | */ 37 | public function onAddPermissions(AddPermissionsEventInterface $event) 38 | { 39 | $permissionTree = ['routes' => []]; 40 | foreach ($this->router->getRouteCollection()->getIterator() as $name => $route) { 41 | if (!($route instanceof RouteInterface)) { 42 | continue; 43 | } 44 | 45 | $permissions = $route->getPermissions(); 46 | if (is_null($permissions)) { 47 | continue; 48 | } 49 | 50 | $permissionTree['routes'][$name] = $permissions; 51 | } 52 | $event->insertTree($permissionTree); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ordermind/symfony-logical-authorization-bundle.svg?branch=master)](https://travis-ci.org/ordermind/symfony-logical-authorization-bundle) 2 | 3 | # Logical Authorization Bundle 4 | 5 | This Symfony bundle provides a unifying solution for authorization that aims to be flexible, convenient and consistent. It combines the expressive power of https://github.com/ordermind/logical-permissions-php with the philosophy of Matthias Noback in his blog post https://matthiasnoback.nl/2014/05/inject-a-repository-instead-of-an-entity-manager to create a solid authorization experience for the developer. 6 | 7 | - Declare your permissions in the mappings for your routes, entities and fields 8 | - Combine multiple permissions with logic gates such as AND and OR 9 | - Support for routes, Doctrine ORM and Doctrine MongoDB 10 | - Review all of your permissions in a single overview tree 11 | - Filter results from repositories automatically with repository decorators 12 | - Intercept interactions with entities automatically with entity decorators 13 | - Export your permissions for easy synchronization with client-side applications 14 | - Debug each access check with detailed information 15 | 16 | ## Installation 17 | 18 | Requirements: Symfony 4.1 or higher. 19 | 20 | **Main bundle** 21 | 22 | ``` 23 | composer require ordermind/logical-authorization-bundle 24 | ``` 25 | 26 | **Support for Doctrine ORM** 27 | 28 | ``` 29 | composer require ordermind/logical-authorization-doctrine-orm-bundle 30 | ``` 31 | 32 | **Support for Doctrine MongoDB** 33 | 34 | ``` 35 | composer require ordermind/logical-authorization-doctrine-mongo-bundle 36 | ``` 37 | 38 | ## Getting started 39 | 40 | Find the documentation here: https://ordermindlogical-authorization-bundle.readthedocs.io 41 | -------------------------------------------------------------------------------- /PermissionTypes/Host/Host.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public static function getName(): string 31 | { 32 | return 'host'; 33 | } 34 | 35 | /** 36 | * Checks if the current request is sent to an approved host 37 | * 38 | * @param string $host The host to evaluate 39 | * @param array $context The context for evaluating the host 40 | * 41 | * @return bool TRUE if the host is allowed or FALSE if it is not allowed 42 | */ 43 | public function checkPermission($host, $context) 44 | { 45 | if (!is_string($host)) { 46 | throw new \TypeError('The host parameter must be a string.'); 47 | } 48 | if (!$host) { 49 | throw new \InvalidArgumentException('The host parameter cannot be empty.'); 50 | } 51 | 52 | $currentRequest = $this->requestStack->getCurrentRequest(); 53 | 54 | if (!$currentRequest) { 55 | return false; 56 | } 57 | 58 | return !!preg_match('{'.$host.'}i', $currentRequest->getHost()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /DataCollector/CollectorInterface.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 25 | } 26 | 27 | /** 28 | * {@inheritdoc} 29 | */ 30 | public static function getName(): string 31 | { 32 | return 'method'; 33 | } 34 | 35 | /** 36 | * Checks if the current request uses an allowed method 37 | * 38 | * @param string $method The method to evaluate 39 | * @param array $context The context for evaluating the method 40 | * 41 | * @return bool TRUE if the method is allowed or FALSE if it is not allowed 42 | */ 43 | public function checkPermission($method, $context) 44 | { 45 | if (!is_string($method)) { 46 | throw new \TypeError('The method parameter must be a string.'); 47 | } 48 | if (!$method) { 49 | throw new \InvalidArgumentException('The method parameter cannot be empty.'); 50 | } 51 | 52 | $currentRequest = $this->requestStack->getCurrentRequest(); 53 | 54 | if (!$currentRequest) { 55 | return false; 56 | } 57 | 58 | return strcasecmp($currentRequest->getMethod(), $method) == 0; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Security/ExpressionProvider.php: -------------------------------------------------------------------------------- 1 | laRoute = $laRoute; 27 | } 28 | 29 | /** 30 | * {@inheritdoc} 31 | */ 32 | public function getFunctions() 33 | { 34 | return [ 35 | new ExpressionFunction( 36 | 'logauth_route', 37 | function () { 38 | return '$routeName = $request->get(\'_route\'); return $routeName ? $this->get(\'logauth.service.logauth_route\')->checkRouteAccess($routeName) : true;'; 39 | }, 40 | function (array $arguments) { 41 | $request = $arguments['request']; 42 | $routeName = $request->get('_route'); 43 | if ($routeName) { 44 | return $this->laRoute->checkRouteAccess($routeName); 45 | } 46 | 47 | return true; 48 | } 49 | ), 50 | ]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /PermissionTypes/Ip/Ip.php: -------------------------------------------------------------------------------- 1 | requestStack = $requestStack; 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public static function getName(): string 35 | { 36 | return 'ip'; 37 | } 38 | 39 | /** 40 | * Checks if the current request comes from an approved ip address 41 | * 42 | * @param string $ip The ip to evaluate 43 | * @param array $context The context for evaluating the ip 44 | * 45 | * @return bool TRUE if the ip is allowed or FALSE if it is not allowed 46 | */ 47 | public function checkPermission($ip, $context) 48 | { 49 | if (!is_string($ip)) { 50 | throw new \TypeError('The ip parameter must be a string.'); 51 | } 52 | if (!$ip) { 53 | throw new \InvalidArgumentException('The ip parameter cannot be empty.'); 54 | } 55 | 56 | $currentRequest = $this->requestStack->getCurrentRequest(); 57 | 58 | if (!$currentRequest) { 59 | return false; 60 | } 61 | 62 | return IpUtils::checkIp($currentRequest->getClientIp(), $ip); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Event/AddPermissionsEvent.php: -------------------------------------------------------------------------------- 1 | permissionKeys = $permissionKeys; 24 | } 25 | 26 | /** 27 | * {@inheritdoc} 28 | */ 29 | public function getTree(): array 30 | { 31 | return $this->tree; 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function insertTree(array $tree) 38 | { 39 | $this->setTree($this->mergeTrees([$this->getTree(), $tree])); 40 | } 41 | 42 | /** 43 | * @internal 44 | * 45 | * @param array $tree 46 | */ 47 | protected function setTree(array $tree) 48 | { 49 | $this->tree = $tree; 50 | } 51 | 52 | /** 53 | * @internal 54 | * 55 | * @param array $trees 56 | * 57 | * @return array 58 | */ 59 | protected function mergeTrees(array $trees): array 60 | { 61 | if (count($trees) == 0) { 62 | return []; 63 | } 64 | 65 | $tree1 = array_shift($trees); 66 | while (count($trees)) { 67 | $tree2 = array_shift($trees); 68 | foreach ($tree2 as $key => $value) { 69 | if (in_array($key, $this->permissionKeys)) { 70 | $tree1 = $tree2; 71 | break; 72 | } 73 | if (isset($tree1[$key]) && is_array($value)) { 74 | $tree1[$key] = $this->mergeTrees([$tree1[$key], $tree2[$key]]); 75 | continue; 76 | } 77 | $tree1[$key] = $value; 78 | } 79 | } 80 | 81 | return $tree1; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Routing/YamlLoader.php: -------------------------------------------------------------------------------- 1 | add($name, $route); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/Fixtures/Security/User/CustomUserProvider.php: -------------------------------------------------------------------------------- 1 | [ 15 | 'password' => 'userpass', 16 | 'roles' => ['ROLE_USER'], 17 | 'email' => 'user@email.com', 18 | ], 19 | 'admin_user' => [ 20 | 'password' => 'adminpass', 21 | 'roles' => [ 22 | 'ROLE_USER', 23 | 'ROLE_ADMIN', 24 | ], 25 | 'email' => 'admin@email.com', 26 | ], 27 | 'superadmin_user' => [ 28 | 'password' => 'superadminpass', 29 | 'roles' => ['ROLE_USER'], 30 | 'email' => 'superadmin@email.com', 31 | 'bypass_access' => true, 32 | ], 33 | ]; 34 | 35 | public function loadUserByUsername($username) 36 | { 37 | if (!empty($this->users[$username])) { 38 | $user_data = $this->users[$username]; 39 | return new TestUser($username, $user_data['password'], $user_data['roles'], $user_data['email'], !empty($user_data['bypass_access'])); 40 | } 41 | 42 | throw new UsernameNotFoundException( 43 | sprintf('Username "%s" does not exist.', $username) 44 | ); 45 | } 46 | 47 | public function refreshUser(UserInterface $user) 48 | { 49 | if (!$user instanceof TestUser) { 50 | throw new UnsupportedUserException( 51 | sprintf('Instances of "%s" are not supported.', get_class($user)) 52 | ); 53 | } 54 | 55 | return $this->loadUserByUsername($user->getUsername()); 56 | } 57 | 58 | public function supportsClass($class) 59 | { 60 | return TestUser::class === $class; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /PermissionTypes/Flag/Flags/UserCanBypassAccess.php: -------------------------------------------------------------------------------- 1 | getName())); 34 | } 35 | 36 | $user = $context['user']; 37 | if (is_string($user)) { //Anonymous user 38 | return false; 39 | } 40 | if (!($user instanceof UserInterface)) { 41 | throw new \InvalidArgumentException(sprintf('The user class must implement Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface to be able to evaluate the %s flag.', $this->getName())); 42 | } 43 | 44 | $access = $user->getBypassAccess(); 45 | if (!is_bool($access)) { 46 | throw new \UnexpectedValueException(sprintf('The method getBypassAccess() on the user object must return a boolean. Returned type is %s.', gettype($access))); 47 | } 48 | 49 | return $access; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Command/DumpPermissionTreeCommand.php: -------------------------------------------------------------------------------- 1 | treeBuilder = $treeBuilder; 34 | parent::__construct(); 35 | } 36 | 37 | /** 38 | * {@inheritdoc} 39 | */ 40 | protected function configure() 41 | { 42 | $this->setName('logauth:dump-permission-tree'); 43 | $this->setDescription('Logical Authorization: Outputs the whole permission tree.'); 44 | $this->addOption( 45 | 'format', 46 | null, 47 | InputOption::VALUE_OPTIONAL, 48 | 'Select the output format. Available formats: yml, json', 49 | 'yml' 50 | ); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | protected function execute(InputInterface $input, OutputInterface $output) 57 | { 58 | $tree = $this->treeBuilder->getTree(); 59 | $format = $input->getOption('format'); 60 | 61 | if ('yml' === $format) { 62 | $output->write(Yaml::dump($tree, 20)); 63 | } elseif ('json' === $format) { 64 | $output->write(json_encode($tree)); 65 | } else { 66 | $output->writeln('Error outputting permission tree: Unrecognized format. Available formats: yml, json'); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Services/LogicalAuthorization.php: -------------------------------------------------------------------------------- 1 | lpProxy = $lpProxy; 35 | if (!$this->lpProxy->getBypassAccessChecker()) { 36 | $this->lpProxy->setBypassAccessChecker($bypassAccessChecker); 37 | } 38 | $this->helper = $helper; 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | public function checkAccess($permissions, array $context, bool $allowBypass = true): bool 45 | { 46 | try { 47 | return $this->lpProxy->checkAccess($permissions, $context, $allowBypass); 48 | } catch (\Exception $e) { 49 | $class = get_class($e); 50 | $message = $e->getMessage(); 51 | $this->helper->handleError("An exception was caught while checking access: \"$message\" at ".$e->getFile()." line ".$e->getLine(), array('exception' => $class, 'permissions' => $permissions, 'context' => $context)); 52 | } 53 | 54 | return false; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Services/Helper.php: -------------------------------------------------------------------------------- 1 | environment = $environment; 41 | $this->tokenStorage = $tokenStorage; 42 | $this->logger = $logger; 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function getCurrentUser() 49 | { 50 | $token = $this->tokenStorage->getToken(); 51 | if (!is_null($token)) { 52 | return $token->getUser(); 53 | } 54 | 55 | return null; 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | public function handleError(string $message, array $context) 62 | { 63 | if ('prod' === $this->environment && !is_null($this->logger)) { 64 | $this->logger->error($message, $context); 65 | } else { 66 | $message .= "\nContext:\n"; 67 | foreach ($context as $key => $value) { 68 | $message .= "$key => ".print_r($value, true)."\n"; 69 | } 70 | throw new LogicalAuthorizationException($message); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Tests/Functional/Services/LogicalAuthorizationTwigTest.php: -------------------------------------------------------------------------------- 1 | twig->getFunction('logauth_check_route_access'); 13 | $this->assertTrue($function instanceof \Twig_SimpleFunction); 14 | $callable = $function->getCallable(); 15 | $this->assertTrue($callable('route_role', static::$admin_user)); 16 | $this->assertFalse($callable('route_role', static::$authenticated_user)); 17 | } 18 | 19 | public function testTwigCheckModelAccess() 20 | { 21 | $function = $this->twig->getFunction('logauth_check_model_access'); 22 | $this->assertTrue($function instanceof \Twig_SimpleFunction); 23 | $callable = $function->getCallable(); 24 | $model = new TestModelRoleAuthor(); 25 | $this->assertTrue($callable(get_class($model), 'create', static::$admin_user)); 26 | $this->assertTrue($callable(get_class($model), 'read', static::$admin_user)); 27 | $this->assertTrue($callable(get_class($model), 'update', static::$admin_user)); 28 | $this->assertTrue($callable(get_class($model), 'delete', static::$admin_user)); 29 | $this->assertFalse($callable(get_class($model), 'create', static::$authenticated_user)); 30 | $this->assertFalse($callable(get_class($model), 'read', static::$authenticated_user)); 31 | $this->assertFalse($callable(get_class($model), 'update', static::$authenticated_user)); 32 | $this->assertFalse($callable(get_class($model), 'delete', static::$authenticated_user)); 33 | } 34 | 35 | public function testTwigCheckFieldAccess() 36 | { 37 | $function = $this->twig->getFunction('logauth_check_field_access'); 38 | $this->assertTrue($function instanceof \Twig_SimpleFunction); 39 | $callable = $function->getCallable(); 40 | $model = new TestModelRoleAuthor(); 41 | $this->assertTrue($callable(get_class($model), 'field1', 'set', static::$admin_user)); 42 | $this->assertTrue($callable(get_class($model), 'field1', 'get', static::$admin_user)); 43 | $this->assertFalse($callable(get_class($model), 'field1', 'set', static::$authenticated_user)); 44 | $this->assertFalse($callable(get_class($model), 'field1', 'get', static::$authenticated_user)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Services/LogicalPermissionsProxyInterface.php: -------------------------------------------------------------------------------- 1 | id; 29 | } 30 | 31 | /** 32 | * Set field1 33 | * 34 | * @param string $field1 35 | * 36 | * @return TestModelHasAccountNoInterface 37 | */ 38 | public function setField1($field1) 39 | { 40 | $this->field1 = $field1; 41 | 42 | return $this; 43 | } 44 | 45 | /** 46 | * Get field1 47 | * 48 | * @return string 49 | */ 50 | public function getField1() 51 | { 52 | return $this->field1; 53 | } 54 | 55 | /** 56 | * Set field2 57 | * 58 | * @param string $field2 59 | * 60 | * @return TestModelHasAccountNoInterface 61 | */ 62 | public function setField2($field2) 63 | { 64 | $this->field2 = $field2; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Get field2 71 | * 72 | * @return string 73 | */ 74 | public function getField2() 75 | { 76 | return $this->field2; 77 | } 78 | 79 | /** 80 | * Set field3 81 | * 82 | * @param string $field3 83 | * 84 | * @return TestModelHasAccountNoInterface 85 | */ 86 | public function setField3($field3) 87 | { 88 | $this->field3 = $field3; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Get field3 95 | * 96 | * @return string 97 | */ 98 | public function getField3() 99 | { 100 | return $this->field3; 101 | } 102 | 103 | /** 104 | * Set author 105 | * 106 | * @param \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface $author 107 | * 108 | * @return TestModelHasAccountNoInterface 109 | */ 110 | public function setAuthor(UserInterface $author) 111 | { 112 | $this->author = $author; 113 | 114 | return $this; 115 | } 116 | 117 | /** 118 | * Get authorId 119 | * 120 | * @return \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface 121 | */ 122 | public function getAuthor(): ?UserInterface { 123 | return $this->author; 124 | } 125 | 126 | } 127 | 128 | -------------------------------------------------------------------------------- /Tests/Fixtures/Model/ErroneousModel.php: -------------------------------------------------------------------------------- 1 | id; 30 | } 31 | 32 | /** 33 | * Set field1 34 | * 35 | * @param string $field1 36 | * 37 | * @return ErroneousModel 38 | */ 39 | public function setField1($field1) 40 | { 41 | $this->field1 = $field1; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Get field1 48 | * 49 | * @return string 50 | */ 51 | public function getField1() 52 | { 53 | return $this->field1; 54 | } 55 | 56 | /** 57 | * Set field2 58 | * 59 | * @param string $field2 60 | * 61 | * @return ErroneousModel 62 | */ 63 | public function setField2($field2) 64 | { 65 | $this->field2 = $field2; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Get field2 72 | * 73 | * @return string 74 | */ 75 | public function getField2() 76 | { 77 | return $this->field2; 78 | } 79 | 80 | /** 81 | * Set field3 82 | * 83 | * @param string $field3 84 | * 85 | * @return ErroneousModel 86 | */ 87 | public function setField3($field3) 88 | { 89 | $this->field3 = $field3; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Get field3 96 | * 97 | * @return string 98 | */ 99 | public function getField3() 100 | { 101 | return $this->field3; 102 | } 103 | 104 | /** 105 | * Set author 106 | * 107 | * @param \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface $author 108 | * 109 | * @return entity implementing ModelInterface 110 | */ 111 | public function setAuthor(UserInterface $author) 112 | { 113 | $this->author = $author; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get authorId 120 | * 121 | * @return \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface 122 | */ 123 | public function getAuthor(): ?UserInterface { 124 | return 'hej'; 125 | } 126 | 127 | } 128 | 129 | -------------------------------------------------------------------------------- /Tests/Fixtures/Model/TestModelNoBypass.php: -------------------------------------------------------------------------------- 1 | id; 30 | } 31 | 32 | /** 33 | * Set field1 34 | * 35 | * @param string $field1 36 | * 37 | * @return TestModelNoBypass 38 | */ 39 | public function setField1($field1) 40 | { 41 | $this->field1 = $field1; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Get field1 48 | * 49 | * @return string 50 | */ 51 | public function getField1() 52 | { 53 | return $this->field1; 54 | } 55 | 56 | /** 57 | * Set field2 58 | * 59 | * @param string $field2 60 | * 61 | * @return TestModelNoBypass 62 | */ 63 | public function setField2($field2) 64 | { 65 | $this->field2 = $field2; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Get field2 72 | * 73 | * @return string 74 | */ 75 | public function getField2() 76 | { 77 | return $this->field2; 78 | } 79 | 80 | /** 81 | * Set field3 82 | * 83 | * @param string $field3 84 | * 85 | * @return TestModelNoBypass 86 | */ 87 | public function setField3($field3) 88 | { 89 | $this->field3 = $field3; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Get field3 96 | * 97 | * @return string 98 | */ 99 | public function getField3() 100 | { 101 | return $this->field3; 102 | } 103 | 104 | /** 105 | * Set author 106 | * 107 | * @param \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface $author 108 | * 109 | * @return entity implementing ModelInterface 110 | */ 111 | public function setAuthor(UserInterface $author) 112 | { 113 | $this->author = $author; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get authorId 120 | * 121 | * @return \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface 122 | */ 123 | public function getAuthor(): ?UserInterface { 124 | return $this->author; 125 | } 126 | 127 | } 128 | 129 | -------------------------------------------------------------------------------- /Tests/Fixtures/Model/TestModelRoleAuthor.php: -------------------------------------------------------------------------------- 1 | id; 30 | } 31 | 32 | /** 33 | * Set field1 34 | * 35 | * @param string $field1 36 | * 37 | * @return TestModelRoleAuthor 38 | */ 39 | public function setField1($field1) 40 | { 41 | $this->field1 = $field1; 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * Get field1 48 | * 49 | * @return string 50 | */ 51 | public function getField1() 52 | { 53 | return $this->field1; 54 | } 55 | 56 | /** 57 | * Set field2 58 | * 59 | * @param string $field2 60 | * 61 | * @return TestModelRoleAuthor 62 | */ 63 | public function setField2($field2) 64 | { 65 | $this->field2 = $field2; 66 | 67 | return $this; 68 | } 69 | 70 | /** 71 | * Get field2 72 | * 73 | * @return string 74 | */ 75 | public function getField2() 76 | { 77 | return $this->field2; 78 | } 79 | 80 | /** 81 | * Set field3 82 | * 83 | * @param string $field3 84 | * 85 | * @return TestModelRoleAuthor 86 | */ 87 | public function setField3($field3) 88 | { 89 | $this->field3 = $field3; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Get field3 96 | * 97 | * @return string 98 | */ 99 | public function getField3() 100 | { 101 | return $this->field3; 102 | } 103 | 104 | /** 105 | * Set author 106 | * 107 | * @param \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface $author 108 | * 109 | * @return entity implementing ModelInterface 110 | */ 111 | public function setAuthor(UserInterface $author) 112 | { 113 | $this->author = $author; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get authorId 120 | * 121 | * @return \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface 122 | */ 123 | public function getAuthor(): ?UserInterface { 124 | return $this->author; 125 | } 126 | 127 | } 128 | 129 | -------------------------------------------------------------------------------- /Tests/Fixtures/Model/TestModelBoolean.php: -------------------------------------------------------------------------------- 1 | id; 32 | } 33 | 34 | /** 35 | * Set field1 36 | * 37 | * @param string $field1 38 | * 39 | * @return TestModelBoolean 40 | */ 41 | public function setField1($field1) 42 | { 43 | $this->field1 = $field1; 44 | 45 | return $this; 46 | } 47 | 48 | /** 49 | * Get field1 50 | * 51 | * @return string 52 | */ 53 | public function getField1() 54 | { 55 | return $this->field1; 56 | } 57 | 58 | /** 59 | * Set field2 60 | * 61 | * @param string $field2 62 | * 63 | * @return TestModelBoolean 64 | */ 65 | public function setField2($field2) 66 | { 67 | $this->field2 = $field2; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * Get field2 74 | * 75 | * @return string 76 | */ 77 | public function getField2() 78 | { 79 | return $this->field2; 80 | } 81 | 82 | /** 83 | * Set field3 84 | * 85 | * @param string $field3 86 | * 87 | * @return TestModelBoolean 88 | */ 89 | public function setField3($field3) 90 | { 91 | $this->field3 = $field3; 92 | 93 | return $this; 94 | } 95 | 96 | /** 97 | * Get field3 98 | * 99 | * @return string 100 | */ 101 | public function getField3() 102 | { 103 | return $this->field3; 104 | } 105 | 106 | /** 107 | * Set author 108 | * 109 | * @param \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface $author 110 | * 111 | * @return entity implementing ModelInterface 112 | */ 113 | public function setAuthor(UserInterface $author) 114 | { 115 | $this->author = $author; 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Get authorId 122 | * 123 | * @return \Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface 124 | */ 125 | public function getAuthor(): ?UserInterface { 126 | return $this->author; 127 | } 128 | 129 | } 130 | 131 | -------------------------------------------------------------------------------- /docs/source/overview.rst: -------------------------------------------------------------------------------- 1 | ======== 2 | Overview 3 | ======== 4 | 5 | Requirements 6 | ============ 7 | 8 | This bundle requires Symfony 4.1 or higher. 9 | 10 | .. _installation: 11 | 12 | Installation 13 | ============ 14 | 15 | Main bundle: 16 | 17 | .. code-block:: bash 18 | 19 | composer require ordermind/logical-authorization-bundle 20 | 21 | Support for Doctrine ORM: 22 | 23 | .. code-block:: bash 24 | 25 | composer require ordermind/logical-authorization-doctrine-orm-bundle 26 | 27 | Support for Doctrine MongoDB: 28 | 29 | .. code-block:: bash 30 | 31 | composer require ordermind/logical-authorization-doctrine-mongo-bundle 32 | 33 | License 34 | ======= 35 | 36 | Licensed using the `MIT license `_. 37 | 38 | Copyright (c) 2018 Kristofer Tengström 39 | 40 | Permission is hereby granted, free of charge, to any person obtaining a copy 41 | of this software and associated documentation files (the "Software"), to deal 42 | in the Software without restriction, including without limitation the rights 43 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 44 | copies of the Software, and to permit persons to whom the Software is 45 | furnished to do so, subject to the following conditions: 46 | 47 | The above copyright notice and this permission notice shall be included in 48 | all copies or substantial portions of the Software. 49 | 50 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 51 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 52 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 53 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 54 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 55 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 56 | THE SOFTWARE. 57 | 58 | Contribution Guidelines 59 | ======================= 60 | 61 | #. Make sure that your code is formatted according to the Symfony coding standards at https://symfony.com/doc/current/contributing/code/standards.html 62 | #. All pull requests must include unit tests to ensure the change works as 63 | expected and to prevent regressions. 64 | 65 | Reporting a security vulnerability 66 | ================================== 67 | 68 | If you've discovered a security vulnerability in Logical Authorization Bundle, we appreciate your help 69 | in disclosing it to us in a `responsible manner `_. 70 | 71 | Publicly disclosing a vulnerability can put the entire community at risk. If 72 | you've discovered a security concern, please email us at 73 | ordermind@gmail.com. We'll work with you to make sure that we understand the 74 | scope of the issue, and that we fully address your concern. 75 | 76 | After a security vulnerability has been corrected, a security hotfix release will 77 | be deployed as soon as possible. 78 | -------------------------------------------------------------------------------- /Services/LogicalAuthorizationModelInterface.php: -------------------------------------------------------------------------------- 1 | 'model_action1', 'model_action3' => 'model_action3', 'fields' => ['field_name1' => ['field_action1' => 'field_action1']]]. 16 | * 17 | * @param object|string $model A model object or class string. 18 | * @param array $modelActions A list of model actions that should be evaluated. 19 | * @param array $fieldActions A list of field actions that should be evaluated. 20 | * @param object|string $user (optional) Either a user object or a string to signify an anonymous user. If no user is supplied, the current user will be used. 21 | * 22 | * @return array A map of available actions 23 | */ 24 | public function getAvailableActions($model, array $modelActions, array $fieldActions, $user = null): array; 25 | 26 | /** 27 | * Checks access for an action on a model for a given user. 28 | * 29 | * If something goes wrong an error will be logged and the method will return FALSE. If no permissions are defined for this action on the provided model it will return TRUE. 30 | * 31 | * @param object|string $model A model object or class string. 32 | * @param string $action Examples of model actions are "create", "read", "update" and "delete". 33 | * @param object|string $user (optional) Either a user object or a string to signify an anonymous user. If no user is supplied, the current user will be used. 34 | * 35 | * @return bool TRUE if access is granted or FALSE if access is denied. 36 | */ 37 | public function checkModelAccess($model, string $action, $user = null): bool; 38 | 39 | /** 40 | * Checks access for an action on a specific field in a model for a given user. 41 | * 42 | * If something goes wrong an error will be logged and the method will return FALSE. If no permissions are defined for this action on the provided field and model it will return TRUE. 43 | * 44 | * @param object|string $model A model object or class string. 45 | * @param string $fieldName The name of the field. 46 | * @param string $action Examples of field actions are "get" and "set". 47 | * @param object|string $user (optional) Either a user object or a string to signify an anonymous user. If no user is supplied, the current user will be used. 48 | * 49 | * @return bool TRUE if access is granted or FALSE if access is denied. 50 | */ 51 | public function checkFieldAccess($model, string $fieldName, string $action, $user = null): bool; 52 | } 53 | -------------------------------------------------------------------------------- /Tests/Fixtures/Controller/DefaultController.php: -------------------------------------------------------------------------------- 1 | getName(); 30 | if (!is_string($name)) { 31 | throw new \InvalidArgumentException('The name of a flag must be a string.'); 32 | } 33 | if (!$name) { 34 | throw new \InvalidArgumentException('The name of a flag cannot be empty.'); 35 | } 36 | if ($this->flagExists($name)) { 37 | throw new \InvalidArgumentException("The flag \"$name\" already exists! If you want to change the class that handles a flag, you may do so by overriding the service definition for that flag."); 38 | } 39 | 40 | $flags = $this->getFlags(); 41 | $flags[$name] = $flag; 42 | $this->setFlags($flags); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | public function removeFlag(string $name) 49 | { 50 | if (!$name) { 51 | throw new \InvalidArgumentException('The name parameter cannot be empty.'); 52 | } 53 | if (!$this->flagExists($name)) { 54 | throw new FlagNotRegisteredException("The flag \"$name\" has not been registered. Please use the 'logauth.tag.permission_type.flag' service tag to register a flag."); 55 | } 56 | 57 | $flags = $this->getFlags(); 58 | unset($flags[$name]); 59 | $this->setFlags($flags); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getFlags(): array 66 | { 67 | return $this->flags; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function checkPermission($name, $context) 74 | { 75 | if (!$name) { 76 | throw new \InvalidArgumentException('The name parameter cannot be empty.'); 77 | } 78 | if (!$this->flagExists($name)) { 79 | throw new FlagNotRegisteredException("The flag \"$name\" has not been registered. Please use the 'logauth.tag.permission_type.flag' service tag to register a flag."); 80 | } 81 | 82 | $flags = $this->getFlags(); 83 | 84 | return $flags[$name]->checkFlag($context); 85 | } 86 | 87 | /** 88 | * @internal 89 | * 90 | * @param array $flags 91 | */ 92 | protected function setFlags(array $flags) 93 | { 94 | $this->flags = $flags; 95 | } 96 | 97 | /** 98 | * @internal 99 | * 100 | * @param string $name 101 | * 102 | * @return bool 103 | */ 104 | protected function flagExists(string $name): bool 105 | { 106 | $flags = $this->getFlags(); 107 | 108 | return isset($flags[$name]); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Services/LogicalPermissionsProxy.php: -------------------------------------------------------------------------------- 1 | accessChecker = new AccessChecker(); 28 | $this->permissionTypeCollection = $this->accessChecker->getPermissionTypeCollection(); 29 | } 30 | 31 | /** 32 | * {@inheritdoc} 33 | */ 34 | public function addType(PermissionTypeInterface $type) 35 | { 36 | try { 37 | $this->permissionTypeCollection->add($type); 38 | } catch (PermissionTypeAlreadyExistsException $e) { 39 | $class = get_class($e); 40 | $message = $e->getMessage(); 41 | $message .= ' If you want to change the class that handles a permission type, you may do so by overriding the service definition for that permission type.'; 42 | throw new $class($message); 43 | } 44 | } 45 | 46 | /** 47 | * {@inheritdoc} 48 | */ 49 | public function removeType(string $name) 50 | { 51 | $this->permissionTypeCollection->remove($name); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function typeExists(string $name): bool 58 | { 59 | return $this->permissionTypeCollection->has($name); 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | */ 65 | public function getTypes(): array 66 | { 67 | return $this->permissionTypeCollection->toArray(); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function setBypassAccessChecker(BypassAccessCheckerInterface $bypassAccessChecker) 74 | { 75 | $this->accessChecker->setBypassAccessChecker($bypassAccessChecker); 76 | } 77 | 78 | /** 79 | * {@inheritdoc} 80 | */ 81 | public function getBypassAccessChecker(): ?BypassAccessCheckerInterface 82 | { 83 | return $this->accessChecker->getBypassAccessChecker(); 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function getValidPermissionKeys(): array 90 | { 91 | return $this->accessChecker->getValidPermissionKeys(); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function checkAccess($permissions, array $context, bool $allowBypass = true): bool 98 | { 99 | try { 100 | return $this->accessChecker->checkAccess($permissions, $context, $allowBypass); 101 | } catch (PermissionTypeNotRegisteredException $e) { 102 | $class = get_class($e); 103 | $message = $e->getMessage(); 104 | $message .= ' Please use the \'logauth.tag.permission_type\' service tag to register a permission type.'; 105 | throw new $class($message); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Services/PermissionTreeBuilder.php: -------------------------------------------------------------------------------- 1 | dispatcher = $dispatcher; 36 | $this->permissionKeys = $lpProxy->getValidPermissionKeys(); 37 | $this->cache = $cache; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function getTree(bool $reset = false, bool $debug = false): array 44 | { 45 | if (!$reset && !is_null($this->tree)) { 46 | $tree = $this->tree; 47 | if ($debug) { 48 | $tree['fetch'] = 'static_cache'; 49 | } 50 | 51 | return $tree; 52 | } 53 | 54 | if (!$reset && !is_null($tree = $this->loadTreeFromCache())) { 55 | $this->tree = $tree; 56 | if ($debug) { 57 | $tree['fetch'] = 'cache'; 58 | } 59 | 60 | return $tree; 61 | } 62 | 63 | $tree = $this->loadTreeFromEvent(); 64 | ksort($tree); 65 | $this->saveTreeToCache($tree); 66 | $this->tree = $tree; 67 | 68 | if ($debug) { 69 | $tree['fetch'] = 'no_cache'; 70 | } 71 | 72 | return $tree; 73 | } 74 | 75 | /** 76 | * @internal 77 | * 78 | * @return ?array 79 | */ 80 | protected function loadTreeFromCache(): ?array 81 | { 82 | $cachedTree = $this->cache->getItem('ordermind.logauth.permissions'); 83 | if ($cachedTree->isHit()) { 84 | return $cachedTree->get(); 85 | } 86 | 87 | return null; 88 | } 89 | 90 | /** 91 | * @internal 92 | * 93 | * @param array $tree 94 | */ 95 | protected function saveTreeToCache(array $tree) 96 | { 97 | $cachedTree = $this->cache->getItem('ordermind.logauth.permissions'); 98 | $cachedTree->set($tree); 99 | $this->cache->save($cachedTree); 100 | } 101 | 102 | /** 103 | * @internal 104 | * 105 | * @return array 106 | */ 107 | protected function loadTreeFromEvent(): array 108 | { 109 | $event = new AddPermissionsEvent($this->permissionKeys); 110 | $this->dispatcher->dispatch('logauth.add_permissions', $event); 111 | 112 | return $event->getTree(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /PermissionTypes/Role/Role.php: -------------------------------------------------------------------------------- 1 | roleHierarchy = $roleHierarchy; 30 | } 31 | 32 | /** 33 | * {@inheritdoc} 34 | */ 35 | public static function getName(): string 36 | { 37 | return 'role'; 38 | } 39 | 40 | /** 41 | * Checks if a role is present on a user in a given context 42 | * 43 | * @param string $role The name of the role to evaluate 44 | * @param array $context The context for evaluating the role. The context must contain a 'user' key which references either a user string (to signify an anonymous user) or an object implementing Symfony\Component\Security\Core\User\UserInterface. You can get the current user by calling getCurrentUser() from the service 'logauth.service.helper'. 45 | * 46 | * @return bool TRUE if the role is present on the user or FALSE if it is not present 47 | */ 48 | public function checkPermission($role, $context) 49 | { 50 | if (!is_string($role)) { 51 | throw new \TypeError('The role parameter must be a string.'); 52 | } 53 | if (!$role) { 54 | throw new \InvalidArgumentException('The role parameter cannot be empty.'); 55 | } 56 | if (!is_array($context)) { 57 | throw new \TypeError('The context parameter must be an array.'); 58 | } 59 | if (!isset($context['user'])) { 60 | throw new \InvalidArgumentException(sprintf('The context parameter must contain a "user" key to be able to evaluate the %s flag.', $this->getName())); 61 | } 62 | 63 | $user = $context['user']; 64 | if (is_string($user)) { //Anonymous user 65 | return false; 66 | } 67 | 68 | if (!($user instanceof SecurityUserInterface)) { 69 | throw new \InvalidArgumentException('The user class must implement Symfony\Component\Security\Core\User\UserInterface to be able to evaluate the user role.'); 70 | } 71 | 72 | $roles = $user->getRoles(); 73 | 74 | // Use Symfony Security Role class to make roles compatible with RoleHierarchy::getReachableRoles(). 75 | foreach ($roles as $i => $thisRole) { 76 | if (is_string($thisRole)) { 77 | $roles[$i] = new SecurityRole($thisRole); 78 | } elseif (!($thisRole instanceof SecurityRole)) { 79 | throw new \InvalidArgumentException('One of the roles of this user is neither a string nor an instance of Symfony\Component\Security\Core\Role\Role.'); 80 | } 81 | } 82 | $roles = $this->roleHierarchy->getReachableRoles($roles); 83 | 84 | foreach ($roles as $thisRole) { 85 | $strRole = (string) $thisRole->getRole(); 86 | if ($role === $strRole) { 87 | return true; 88 | } 89 | } 90 | 91 | return false; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /PermissionTypes/Flag/Flags/UserIsAuthor.php: -------------------------------------------------------------------------------- 1 | getName())); 34 | } 35 | 36 | $user = $context['user']; 37 | if (is_string($user)) { //Anonymous user 38 | return false; 39 | } 40 | 41 | if (!($user instanceof UserInterface)) { 42 | throw new \InvalidArgumentException(sprintf('The user class must implement Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface to be able to evaluate the %s flag.', $this->getName())); 43 | } 44 | if (!isset($context['model'])) { 45 | throw new \InvalidArgumentException(sprintf('Missing key "model" in context parameter. We cannot evaluate the %s flag without a model.', $this->getName())); 46 | } 47 | 48 | $model = $context['model']; 49 | 50 | if (is_string($model) && class_exists($model)) { 51 | // A class string was passed which means that we don't have an actual object to evaluate. We interpret this as it not having an author which means that we return false. 52 | return false; 53 | } 54 | 55 | if ($model instanceof UserInterface) { 56 | return $user->getId() === $model->getId(); 57 | } 58 | 59 | if ($model instanceof ModelInterface) { 60 | $author = $model->getAuthor(); 61 | // If there is no author it probably means that the entity is not yet persisted. In that case it's reasonable to assume that the current user is the author. 62 | // If the lack of author is due to some other reason it's also reasonable to fall back to granting permission because the reason for this flag is to protect models that do have an author against other users. 63 | if (!$author) { 64 | return true; 65 | } 66 | if (!($author instanceof UserInterface)) { 67 | throw new \InvalidArgumentException(sprintf('The author of the model must implement Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface to be able to evaluate the %s flag.', $this->getName())); 68 | } 69 | 70 | return $user->getId() === $author->getId(); 71 | } 72 | 73 | throw new \InvalidArgumentException(sprintf('The model class must implement either Ordermind\LogicalAuthorizationBundle\Interfaces\ModelInterface or Ordermind\LogicalAuthorizationBundle\Interfaces\UserInterface to be able to evaluate the %s flag.', $this->getName())); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/Functional/Services/LogicalAuthorizationBase.php: -------------------------------------------------------------------------------- 1 | 'userpass', 17 | 'admin_user' => 'adminpass', 18 | 'superadmin_user' => 'superadminpass', 19 | ]; 20 | protected $load_services = array(); 21 | protected $client; 22 | protected $la; 23 | protected $helper; 24 | 25 | /** 26 | * This method is run before each public test method 27 | */ 28 | protected function setUp() 29 | { 30 | require_once __DIR__.'/../../AppKernel.php'; 31 | $kernel = new \AppKernel('test', true); 32 | $kernel->boot(); 33 | $container = $kernel->getContainer(); 34 | 35 | $this->client = static::createClient(); 36 | $this->la = $container->get('test.logauth.service.logauth'); 37 | $this->lpProxy = $container->get('test.logauth.service.logical_permissions_proxy'); 38 | $this->laModel = $container->get('test.logauth.service.logauth_model'); 39 | $this->laRoute = $container->get('test.logauth.service.logauth_route'); 40 | $this->helper = $container->get('test.logauth.service.helper'); 41 | $this->treeBuilder = $container->get('test.logauth.service.permission_tree_builder'); 42 | $this->twig = $container->get('twig'); 43 | $roleHierarchy = $container->getParameter('security.role_hierarchy.roles'); 44 | $this->roleHierarchy = new RoleHierarchy($roleHierarchy); 45 | 46 | $this->addUsers(); 47 | } 48 | 49 | /** 50 | * This method is run after each public test method 51 | * 52 | * It is important to reset all non-static properties to minimize memory leaks. 53 | */ 54 | protected function tearDown() 55 | { 56 | $this->client = null; 57 | 58 | parent::tearDown(); 59 | } 60 | 61 | protected function addUsers() 62 | { 63 | //Create new normal user 64 | if (!static::$authenticated_user) { 65 | static::$authenticated_user = new TestUser(); 66 | static::$authenticated_user->setUsername('authenticated_user'); 67 | static::$authenticated_user->setPassword($this->user_credentials['authenticated_user']); 68 | static::$authenticated_user->setEmail('user@email.com'); 69 | } 70 | 71 | //Create new admin user 72 | if (!static::$admin_user) { 73 | static::$admin_user = new TestUser(); 74 | static::$admin_user->setUsername('admin_user'); 75 | static::$admin_user->setPassword($this->user_credentials['admin_user']); 76 | static::$admin_user->setEmail('admin@email.com'); 77 | static::$admin_user->setRoles(['ROLE_ADMIN']); 78 | } 79 | 80 | //Create superadmin user 81 | if (!static::$superadmin_user) { 82 | static::$superadmin_user = new TestUser(); 83 | static::$superadmin_user->setUsername('superadmin_user'); 84 | static::$superadmin_user->setPassword($this->user_credentials['superadmin_user']); 85 | static::$superadmin_user->setEmail('superadmin@email.com'); 86 | static::$superadmin_user->setBypassAccess(true); 87 | } 88 | } 89 | 90 | protected function sendRequestAs($method = 'GET', $slug, array $params = array(), $user = null) 91 | { 92 | $headers = array(); 93 | if ($user) { 94 | $headers = array( 95 | 'PHP_AUTH_USER' => $user->getUsername(), 96 | 'PHP_AUTH_PW' => $this->user_credentials[$user->getUsername()], 97 | ); 98 | } 99 | $this->client->request($method, $slug, $params, array(), $headers); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Routing/Route.php: -------------------------------------------------------------------------------- 1 | setPath($path); 76 | $this->setDefaults($defaults); 77 | $this->setRequirements($requirements); 78 | $this->setOptions($options); 79 | $this->setHost($host); 80 | $this->setSchemes($schemes); 81 | $this->setMethods($methods); 82 | $this->setCondition($condition); 83 | $this->setPermissions($permissions); 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function serialize() 90 | { 91 | return serialize(array( 92 | 'path' => $this->path, 93 | 'host' => $this->host, 94 | 'defaults' => $this->defaults, 95 | 'requirements' => $this->requirements, 96 | 'options' => $this->options, 97 | 'schemes' => $this->schemes, 98 | 'methods' => $this->methods, 99 | 'condition' => $this->condition, 100 | 'compiled' => $this->compiled, 101 | 'permissions' => $this->permissions, 102 | )); 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | public function unserialize($serialized) 109 | { 110 | $data = unserialize($serialized); 111 | $this->path = $data['path']; 112 | $this->host = $data['host']; 113 | $this->defaults = $data['defaults']; 114 | $this->requirements = $data['requirements']; 115 | $this->options = $data['options']; 116 | $this->schemes = $data['schemes']; 117 | $this->methods = $data['methods']; 118 | 119 | if (isset($data['condition'])) { 120 | $this->condition = $data['condition']; 121 | } 122 | if (isset($data['compiled'])) { 123 | $this->compiled = $data['compiled']; 124 | } 125 | if (isset($data['permissions'])) { 126 | $this->permissions = $data['permissions']; 127 | } 128 | } 129 | 130 | /** 131 | * {@inheritdoc} 132 | */ 133 | public function setPermissions($permissions) 134 | { 135 | $this->permissions = $permissions; 136 | } 137 | 138 | /** 139 | * {@inheritdoc} 140 | */ 141 | public function getPermissions() 142 | { 143 | return $this->permissions; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /Twig/LogicalAuthorizationExtension.php: -------------------------------------------------------------------------------- 1 | laRoute = $laRoute; 33 | $this->laModel = $laModel; 34 | } 35 | 36 | /** 37 | * {@inheritdoc} 38 | */ 39 | public function getFunctions(): array 40 | { 41 | return array( 42 | new \Twig_SimpleFunction('logauth_check_route_access', array($this, 'checkRouteAccess')), 43 | new \Twig_SimpleFunction('logauth_check_model_access', array($this, 'checkModelAccess')), 44 | new \Twig_SimpleFunction('logauth_check_field_access', array($this, 'checkFieldAccess')), 45 | ); 46 | } 47 | 48 | /** 49 | * Twig extension callback for checking route access 50 | * 51 | * If something goes wrong an error will be logged and the method will return FALSE. If no permissions are defined for the provided route it will return TRUE. 52 | * 53 | * @param string $routeName The name of the route 54 | * @param object|string $user (optional) Either a user object or a string to signify an anonymous user. If no user is supplied, the current user will be used. 55 | * 56 | * @return bool TRUE if access is granted or FALSE if access is denied. 57 | */ 58 | public function checkRouteAccess(string $routeName, $user = null): bool 59 | { 60 | return $this->laRoute->checkRouteAccess($routeName, $user); 61 | } 62 | 63 | /** 64 | * Twig extension callback for checking model access 65 | * 66 | * If something goes wrong an error will be logged and the method will return FALSE. If no permissions are defined for this action on the provided model it will return TRUE. 67 | * 68 | * @param object|string $model A model object or class string. 69 | * @param string $action Examples of model actions are "create", "read", "update" and "delete". 70 | * @param object|string $user (optional) Either a user object or a string to signify an anonymous user. If no user is supplied, the current user will be used. 71 | * 72 | * @return bool TRUE if access is granted or FALSE if access is denied. 73 | */ 74 | public function checkModelAccess($model, string $action, $user = null): bool 75 | { 76 | return $this->laModel->checkModelAccess($model, $action, $user); 77 | } 78 | 79 | /** 80 | * Twig extension callback for checking field access 81 | * 82 | * If something goes wrong an error will be logged and the method will return FALSE. If no permissions are defined for this action on the provided field and model it will return TRUE. 83 | * 84 | * @param object|string $model A model object or class string. 85 | * @param string $fieldName The name of the field. 86 | * @param string $action Examples of field actions are "get" and "set". 87 | * @param object|string $user (optional) Either a user object or a string to signify an anonymous user. If no user is supplied, the current user will be used. 88 | * 89 | * @return bool TRUE if access is granted or FALSE if access is denied. 90 | */ 91 | public function checkFieldAccess($model, string $fieldName, string $action, $user = null): bool 92 | { 93 | return $this->laModel->checkFieldAccess($model, $fieldName, $action, $user); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/Fixtures/ControllerDir/DefaultController.php: -------------------------------------------------------------------------------- 1 | get('test.logauth.service.logauth_route'); 98 | $result = $laRoute->getAvailableRoutes(); 99 | if (empty($result['routes'])) { 100 | return new Response(0); 101 | } 102 | return new Response(count($result['routes'])); 103 | } 104 | 105 | /** 106 | * @Route("/count-available-route-patterns", name="count_available_route_patterns") 107 | * @Method({"GET"}) 108 | */ 109 | public function countAvailableRoutePatternsAction(Request $request) 110 | { 111 | $laRoute = $this->get('test.logauth.service.logauth_route'); 112 | $result = $laRoute->getAvailableRoutes(); 113 | if (empty($result['route_patterns'])) { 114 | return new Response(0); 115 | } 116 | return new Response(count($result['route_patterns'])); 117 | } 118 | 119 | /** 120 | * @Route("/get-current-username", name="get_current_username") 121 | * @Method({"GET"}) 122 | */ 123 | public function getCurrentUsernameAction(Request $request) 124 | { 125 | $user = $this->get('test.logauth.service.helper')->getCurrentUser(); 126 | if (is_null($user)) { 127 | return new Response($user); 128 | } 129 | if (is_string($user)) { 130 | return new Response($user); 131 | } 132 | return new Response($user->getUsername()); 133 | } 134 | 135 | /** 136 | * @Route("/count-forbidden-entities-lazy", name="test_count_forbidden_entities_lazy") 137 | * @Method({"GET"}) 138 | */ 139 | public function countForbiddenEntitiesLazyLoadAction(Request $request) 140 | { 141 | $operations = $this->get('test_model_operations'); 142 | $operations->setRepositoryDecorator($this->get('repository_decorator.forbidden_entity')); 143 | $collection = $operations->getLazyLoadedModelResult(); 144 | return new Response(count($collection)); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /Tests/config/config.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: security.yml } 3 | - { resource: services.yml } 4 | 5 | # Put parameters here that don't need to change on each machine where the app is deployed 6 | # http://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 7 | parameters: 8 | locale: en 9 | 10 | framework: 11 | secret: TestSecret 12 | test: ~ 13 | router: 14 | type: logauth_yml 15 | resource: "%kernel.root_dir%/config/routing.yml" 16 | strict_requirements: ~ 17 | default_locale: "%locale%" 18 | trusted_hosts: ~ 19 | session: 20 | # handler_id set to null will use default session handler from php.ini 21 | handler_id: ~ 22 | save_path: "%kernel.root_dir%/../var/sessions/%kernel.environment%" 23 | storage_id: session.storage.mock_file 24 | fragments: ~ 25 | http_method_override: true 26 | #cache: 27 | #app: cache.adapter.null 28 | 29 | # LogicalAuthorization Configuration 30 | logauth: 31 | permissions: 32 | routes: 33 | route_override_permissions: 34 | role: ROLE_ADMIN 35 | route_patterns: 36 | ^/test/route-: 37 | no_bypass: true 38 | 0: false 39 | ^/test/pattern-: true 40 | models: 41 | Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Model\TestModelBoolean: 42 | create: true 43 | read: false 44 | update: true 45 | delete: false 46 | fields: 47 | field1: 48 | get: true 49 | set: false 50 | Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Model\TestModelHasAccountNoInterface: 51 | create: 52 | flag: user_has_account 53 | read: 54 | flag: user_has_account 55 | update: 56 | flag: user_has_account 57 | delete: 58 | flag: user_has_account 59 | fields: 60 | field1: 61 | get: 62 | flag: user_has_account 63 | set: 64 | flag: user_has_account 65 | Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Model\TestModelNoBypass: 66 | create: 67 | no_bypass: true 68 | 0: false 69 | read: 70 | no_bypass: true 71 | 0: false 72 | update: 73 | no_bypass: true 74 | 0: false 75 | delete: 76 | no_bypass: true 77 | 0: false 78 | fields: 79 | field1: 80 | get: 81 | no_bypass: true 82 | 0: false 83 | 84 | set: 85 | no_bypass: true 86 | 0: false 87 | Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Model\TestModelRoleAuthor: 88 | create: 89 | role: ROLE_ADMIN 90 | read: 91 | OR: 92 | role: ROLE_ADMIN 93 | flag: user_is_author 94 | update: 95 | OR: 96 | role: ROLE_ADMIN 97 | flag: user_is_author 98 | delete: 99 | OR: 100 | role: ROLE_ADMIN 101 | flag: user_is_author 102 | fields: 103 | field1: 104 | get: 105 | role: ROLE_ADMIN 106 | flag: user_is_author 107 | set: 108 | role: ROLE_ADMIN 109 | flag: user_is_author 110 | Ordermind\LogicalAuthorizationBundle\Tests\Fixtures\Model\TestUser: 111 | create: 112 | role: ROLE_ADMIN 113 | read: 114 | OR: 115 | role: ROLE_ADMIN 116 | flag: user_is_author 117 | update: 118 | OR: 119 | role: ROLE_ADMIN 120 | flag: user_is_author 121 | delete: 122 | no_bypass: 123 | flag: user_is_author 124 | AND: 125 | role: ROLE_ADMIN 126 | flag: 127 | NOT: user_is_author 128 | -------------------------------------------------------------------------------- /Tests/Fixtures/Model/ErroneousUser.php: -------------------------------------------------------------------------------- 1 | setUsername($username); 35 | } 36 | if ($password) { 37 | $this->setPassword($password); 38 | } 39 | $this->setRoles($roles); 40 | if ($email) { 41 | $this->setEmail($email); 42 | } 43 | $this->setBypassAccess($bypassAccess); 44 | } 45 | 46 | 47 | /** 48 | * Get id 49 | * 50 | * @return int 51 | */ 52 | public function getId() 53 | { 54 | return $this->id; 55 | } 56 | 57 | /** 58 | * Set username 59 | * 60 | * @param string $username 61 | * 62 | * @return TestUser 63 | */ 64 | public function setUsername($username) 65 | { 66 | $this->username = $username; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Get username 73 | * 74 | * @return string 75 | */ 76 | public function getUsername() 77 | { 78 | return $this->username; 79 | } 80 | 81 | /** 82 | * Set password 83 | * 84 | * @param string $password 85 | * 86 | * @return TestUser 87 | */ 88 | public function setPassword($password) 89 | { 90 | $this->password = $password; 91 | 92 | return $this; 93 | } 94 | 95 | /** 96 | * Get password 97 | * 98 | * @return string 99 | */ 100 | public function getPassword() 101 | { 102 | return $this->password; 103 | } 104 | 105 | /** 106 | * Set old password 107 | * 108 | * @param string $oldPassword 109 | * 110 | * @return TestUser 111 | */ 112 | public function setOldPassword($password) 113 | { 114 | $encoder = new BCryptPasswordEncoder(static::bcryptStrength); 115 | $this->oldPassword = $encoder->encodePassword($password, $this->getSalt()); 116 | 117 | return $this; 118 | } 119 | 120 | /** 121 | * Get old password 122 | * 123 | * @return string 124 | */ 125 | public function getOldPassword() 126 | { 127 | return $this->oldPassword; 128 | } 129 | 130 | /** 131 | * Set roles 132 | * 133 | * @return array 134 | */ 135 | public function setRoles($roles) 136 | { 137 | if (array_search('ROLE_USER', $roles) === false) { 138 | array_unshift($roles, 'ROLE_USER'); 139 | } 140 | $this->roles = $roles; 141 | } 142 | 143 | /** 144 | * Get roles. Please use getFilteredRoles() instead. 145 | * 146 | * @return array 147 | */ 148 | public function getRoles() 149 | { 150 | return $this->roles; 151 | } 152 | 153 | /** 154 | * Get filtered roles. 155 | * 156 | * @return array 157 | */ 158 | public function getFilteredRoles() 159 | { 160 | $roles = $this->roles; 161 | if (($key = array_search('ROLE_USER', $roles)) !== false) { 162 | unset($roles[$key]); 163 | } 164 | return $roles; 165 | } 166 | 167 | /** 168 | * Set email 169 | * 170 | * @param string $email 171 | * 172 | * @return TestUser 173 | */ 174 | public function setEmail($email) 175 | { 176 | $this->email = $email; 177 | 178 | return $this; 179 | } 180 | 181 | /** 182 | * Get email 183 | * 184 | * @return string 185 | */ 186 | public function getEmail() 187 | { 188 | return $this->email; 189 | } 190 | 191 | /** 192 | * Set bypassAccess 193 | * 194 | * @param bool $bypassAccess 195 | * 196 | * @return TestUser 197 | */ 198 | public function setBypassAccess(bool $bypassAccess) 199 | { 200 | $this->bypassAccess = $bypassAccess; 201 | 202 | return $this; 203 | } 204 | 205 | /** 206 | * Get bypassAccess 207 | * 208 | * @return bool 209 | */ 210 | public function getBypassAccess(): bool 211 | { 212 | return (string) $this->bypassAccess; 213 | } 214 | 215 | public function getSalt() 216 | { 217 | return null; //bcrypt doesn't require a salt. 218 | } 219 | 220 | public function eraseCredentials() 221 | { 222 | } 223 | 224 | public function serialize() 225 | { 226 | return serialize(array( 227 | $this->id, 228 | $this->username, 229 | $this->password, 230 | )); 231 | } 232 | 233 | public function unserialize($serialized) 234 | { 235 | list( 236 | $this->id, 237 | $this->username, 238 | $this->password, 239 | ) = unserialize($serialized); 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /Tests/Fixtures/Model/TestUser.php: -------------------------------------------------------------------------------- 1 | setUsername($username); 29 | } 30 | if ($password) { 31 | $this->setPassword($password); 32 | } 33 | $this->setRoles($roles); 34 | if ($email) { 35 | $this->setEmail($email); 36 | } 37 | $this->setBypassAccess($bypassAccess); 38 | } 39 | 40 | public function setId($id) 41 | { 42 | $this->id = $id; 43 | } 44 | 45 | /** 46 | * Get id 47 | * 48 | * @return int 49 | */ 50 | public function getId() 51 | { 52 | return $this->id; 53 | } 54 | 55 | /** 56 | * Set username 57 | * 58 | * @param string $username 59 | * 60 | * @return TestUser 61 | */ 62 | public function setUsername($username) 63 | { 64 | $this->username = $username; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Get username 71 | * 72 | * @return string 73 | */ 74 | public function getUsername() 75 | { 76 | return $this->username; 77 | } 78 | 79 | /** 80 | * Set password 81 | * 82 | * @param string $password 83 | * 84 | * @return TestUser 85 | */ 86 | public function setPassword($password) 87 | { 88 | $this->password = $password; 89 | 90 | return $this; 91 | } 92 | 93 | /** 94 | * Get password 95 | * 96 | * @return string 97 | */ 98 | public function getPassword() 99 | { 100 | return $this->password; 101 | } 102 | 103 | /** 104 | * Set old password 105 | * 106 | * @param string $oldPassword 107 | * 108 | * @return TestUser 109 | */ 110 | public function setOldPassword($password) 111 | { 112 | $encoder = new BCryptPasswordEncoder(static::bcryptStrength); 113 | $this->oldPassword = $encoder->encodePassword($password, $this->getSalt()); 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get old password 120 | * 121 | * @return string 122 | */ 123 | public function getOldPassword() 124 | { 125 | return $this->oldPassword; 126 | } 127 | 128 | /** 129 | * Set roles 130 | * 131 | * @return array 132 | */ 133 | public function setRoles($roles) 134 | { 135 | if (array_search('ROLE_USER', $roles) === false) { 136 | array_unshift($roles, 'ROLE_USER'); 137 | } 138 | $this->roles = $roles; 139 | } 140 | 141 | /** 142 | * Get roles. Please use getFilteredRoles() instead. 143 | * 144 | * @return array 145 | */ 146 | public function getRoles() 147 | { 148 | return $this->roles; 149 | } 150 | 151 | /** 152 | * Get filtered roles. 153 | * 154 | * @return array 155 | */ 156 | public function getFilteredRoles() 157 | { 158 | $roles = $this->roles; 159 | if (($key = array_search('ROLE_USER', $roles)) !== false) { 160 | unset($roles[$key]); 161 | } 162 | return $roles; 163 | } 164 | 165 | /** 166 | * Set email 167 | * 168 | * @param string $email 169 | * 170 | * @return TestUser 171 | */ 172 | public function setEmail($email) 173 | { 174 | $this->email = $email; 175 | 176 | return $this; 177 | } 178 | 179 | /** 180 | * Get email 181 | * 182 | * @return string 183 | */ 184 | public function getEmail() 185 | { 186 | return $this->email; 187 | } 188 | 189 | /** 190 | * Set bypassAccess 191 | * 192 | * @param boolean $bypassAccess 193 | * 194 | * @return TestUser 195 | */ 196 | public function setBypassAccess(bool $bypassAccess) 197 | { 198 | $this->bypassAccess = $bypassAccess; 199 | 200 | return $this; 201 | } 202 | 203 | /** 204 | * Get bypassAccess 205 | * 206 | * @return bool 207 | */ 208 | public function getBypassAccess(): bool 209 | { 210 | return $this->bypassAccess; 211 | } 212 | 213 | public function getSalt() 214 | { 215 | return null; //bcrypt doesn't require a salt. 216 | } 217 | 218 | public function eraseCredentials() 219 | { 220 | } 221 | 222 | public function serialize() 223 | { 224 | return serialize(array( 225 | $this->id, 226 | $this->username, 227 | $this->password, 228 | )); 229 | } 230 | 231 | public function unserialize($serialized) 232 | { 233 | list( 234 | $this->id, 235 | $this->username, 236 | $this->password, 237 | ) = unserialize($serialized); 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /Routing/schema/routing/routing-1.0.xsd: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'Logical Authorization Bundle' 23 | copyright = u'2018, Kristofer Tengström' 24 | author = u'Kristofer Tengström' 25 | 26 | # The short X.Y version 27 | version = '' 28 | # The full version, including alpha/beta/rc tags 29 | release = '' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.todo', 45 | 'sphinx.ext.coverage', 46 | 'sphinx.ext.mathjax', 47 | 'sphinx.ext.ifconfig', 48 | 'sphinxcontrib.phpdomain', 49 | 'sphinx_tabs.tabs', 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = '.rst' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path . 74 | exclude_patterns = [] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = 'sphinx' 78 | 79 | 80 | # -- Options for HTML output ------------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = 'alabaster' 86 | 87 | # Theme options are theme-specific and customize the look and feel of a theme 88 | # further. For a list of options available for each theme, see the 89 | # documentation. 90 | # 91 | # html_theme_options = {} 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ['_static'] 97 | 98 | # Custom sidebar templates, must be a dictionary that maps document names 99 | # to template names. 100 | # 101 | # The default sidebars (for documents that don't match any pattern) are 102 | # defined by theme itself. Builtin themes are using these templates by 103 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 104 | # 'searchbox.html']``. 105 | # 106 | # html_sidebars = {} 107 | 108 | 109 | # -- Options for HTMLHelp output --------------------------------------------- 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = 'LogicalAuthorizationBundledoc' 113 | 114 | 115 | # -- Options for LaTeX output ------------------------------------------------ 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | (master_doc, 'LogicalAuthorizationBundle.tex', 'Logical Authorization Bundle Documentation', 140 | u'Kristofer Tengström', 'manual'), 141 | ] 142 | 143 | 144 | # -- Options for manual page output ------------------------------------------ 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [ 149 | (master_doc, 'logicalauthorizationbundle', 'Logical Authorization Bundle Documentation', 150 | [author], 1) 151 | ] 152 | 153 | 154 | # -- Options for Texinfo output ---------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | (master_doc, 'LogicalAuthorizationBundle', 'Logical Authorization Bundle Documentation', 161 | author, 'LogicalAuthorizationBundle', 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | # -- Extension configuration ------------------------------------------------- 167 | 168 | # -- Options for todo extension ---------------------------------------------- 169 | 170 | # If true, `todo` and `todoList` produce output, else they produce nothing. 171 | todo_include_todos = True 172 | 173 | 174 | import sphinx_rtd_theme 175 | html_theme = "sphinx_rtd_theme" 176 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 177 | 178 | # Set up PHP syntax highlights 179 | from sphinx.highlighting import lexers 180 | from pygments.lexers.web import PhpLexer 181 | lexers["php"] = PhpLexer(startinline=True, linenos=1) 182 | lexers["php-annotations"] = PhpLexer(startinline=True, linenos=1) 183 | primary_domain = "php" 184 | 185 | 186 | -------------------------------------------------------------------------------- /Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | # Core services 4 | 5 | logauth.cache_warmer.permissions: 6 | class: Ordermind\LogicalAuthorizationBundle\EventListener\PermissionsCacheWarmer 7 | arguments: ['@logauth.service.permission_tree_builder'] 8 | tags: 9 | - {name: kernel.cache_warmer} 10 | public: false 11 | 12 | logauth.security.expression_provider: 13 | class: Ordermind\LogicalAuthorizationBundle\Security\ExpressionProvider 14 | arguments: ['@logauth.service.logauth_route'] 15 | tags: 16 | - {name: security.expression_language_provider} 17 | public: false 18 | 19 | logauth.event_listener.add_route_permissions: 20 | class: Ordermind\LogicalAuthorizationBundle\EventListener\AddRoutePermissions 21 | arguments: ['@router'] 22 | tags: 23 | - {name: kernel.event_listener, event: logauth.add_permissions, method: onAddPermissions} 24 | public: false 25 | 26 | logauth.event_listener.add_app_config_permissions: 27 | class: Ordermind\LogicalAuthorizationBundle\EventListener\AddAppConfigPermissions 28 | arguments: ['%logauth.config%'] 29 | tags: 30 | - {name: kernel.event_listener, event: logauth.add_permissions, method: onAddPermissions, priority: -250} 31 | public: false 32 | 33 | logauth.twig.extension: 34 | class: Ordermind\LogicalAuthorizationBundle\Twig\LogicalAuthorizationExtension 35 | arguments: ['@logauth.service.logauth_route', '@logauth.service.logauth_model'] 36 | tags: 37 | - {name: twig.extension} 38 | public: false 39 | 40 | logauth.service.logical_permissions_proxy: 41 | class: Ordermind\LogicalAuthorizationBundle\Services\LogicalPermissionsProxy 42 | public: false 43 | 44 | logauth.service.helper: 45 | class: Ordermind\LogicalAuthorizationBundle\Services\Helper 46 | arguments: ['%kernel.environment%', '@security.token_storage', '@?logger'] 47 | public: false 48 | 49 | logauth.service.permission_tree_builder: 50 | class: Ordermind\LogicalAuthorizationBundle\Services\PermissionTreeBuilder 51 | arguments: ['@logauth.service.logical_permissions_proxy', '@event_dispatcher', '@cache.app'] 52 | public: false 53 | 54 | logauth.bypass_access_checker: 55 | class: Ordermind\LogicalAuthorizationBundle\BypassAccessChecker\BypassAccessChecker 56 | arguments: ['@logauth.service.logical_permissions_proxy'] 57 | public: false 58 | 59 | logauth.service.logauth_route: 60 | class: Ordermind\LogicalAuthorizationBundle\Services\LogicalAuthorizationRoute 61 | arguments: ['@logauth.service.logauth', '@logauth.service.permission_tree_builder', '@router', '@logauth.service.helper', '@?logauth.debug.collector'] 62 | public: false 63 | 64 | logauth.service.logauth_model: 65 | class: Ordermind\LogicalAuthorizationBundle\Services\LogicalAuthorizationModel 66 | arguments: ['@logauth.service.logauth', '@logauth.service.permission_tree_builder', '@logauth.service.helper', '@?logauth.debug.collector'] 67 | public: false 68 | 69 | logauth.service.logauth: 70 | class: Ordermind\LogicalAuthorizationBundle\Services\LogicalAuthorization 71 | arguments: ['@logauth.service.logical_permissions_proxy', '@logauth.service.helper', '@logauth.bypass_access_checker'] 72 | public: false 73 | 74 | logauth.routing.yml_file_loader: 75 | class: Ordermind\LogicalAuthorizationBundle\Routing\YamlLoader 76 | arguments: ['@file_locator'] 77 | tags: 78 | - {name: routing.loader} 79 | public: false 80 | 81 | logauth.routing.xml_file_loader: 82 | class: Ordermind\LogicalAuthorizationBundle\Routing\XmlLoader 83 | arguments: ['@file_locator'] 84 | tags: 85 | - {name: routing.loader} 86 | public: false 87 | 88 | logauth.routing.annotation_file_loader: 89 | class: Ordermind\LogicalAuthorizationBundle\Routing\AnnotationFileLoader 90 | arguments: ['@file_locator', '@logauth.routing.annotation_class_loader'] 91 | tags: 92 | - {name: routing.loader} 93 | public: false 94 | 95 | logauth.routing.annotation_dir_loader: 96 | class: Ordermind\LogicalAuthorizationBundle\Routing\AnnotationDirectoryLoader 97 | arguments: ['@file_locator', '@logauth.routing.annotation_class_loader'] 98 | tags: 99 | - {name: routing.loader} 100 | public: false 101 | 102 | logauth.routing.annotation_class_loader: 103 | class: Ordermind\LogicalAuthorizationBundle\Routing\AnnotationClassLoader 104 | arguments: ['@annotation_reader'] 105 | public: false 106 | 107 | # Permission type handlers 108 | 109 | logauth.permission_type.flag: 110 | class: Ordermind\LogicalAuthorizationBundle\PermissionTypes\Flag\FlagManager 111 | tags: 112 | - {name: logauth.tag.permission_type} 113 | public: false 114 | 115 | logauth.permission_type.flag.user_can_bypass_access: 116 | class: Ordermind\LogicalAuthorizationBundle\PermissionTypes\Flag\Flags\UserCanBypassAccess 117 | tags: 118 | - {name: logauth.tag.permission_type.flag} 119 | public: false 120 | 121 | logauth.permission_type.flag.user_has_account: 122 | class: Ordermind\LogicalAuthorizationBundle\PermissionTypes\Flag\Flags\UserHasAccount 123 | tags: 124 | - {name: logauth.tag.permission_type.flag} 125 | public: false 126 | 127 | logauth.permission_type.flag.user_is_author: 128 | class: Ordermind\LogicalAuthorizationBundle\PermissionTypes\Flag\Flags\UserIsAuthor 129 | tags: 130 | - {name: logauth.tag.permission_type.flag} 131 | public: false 132 | 133 | logauth.permission_type.role: 134 | class: Ordermind\LogicalAuthorizationBundle\PermissionTypes\Role\Role 135 | arguments: ['@security.role_hierarchy'] 136 | tags: 137 | - {name: logauth.tag.permission_type} 138 | public: false 139 | 140 | logauth.permission_type.host: 141 | class: Ordermind\LogicalAuthorizationBundle\PermissionTypes\Host\Host 142 | arguments: ['@request_stack'] 143 | tags: 144 | - {name: logauth.tag.permission_type} 145 | public: false 146 | 147 | logauth.permission_type.method: 148 | class: Ordermind\LogicalAuthorizationBundle\PermissionTypes\Method\Method 149 | arguments: ['@request_stack'] 150 | tags: 151 | - {name: logauth.tag.permission_type} 152 | public: false 153 | 154 | logauth.permission_type.ip: 155 | class: Ordermind\LogicalAuthorizationBundle\PermissionTypes\Ip\Ip 156 | arguments: ['@request_stack'] 157 | tags: 158 | - {name: logauth.tag.permission_type} 159 | public: false 160 | -------------------------------------------------------------------------------- /Services/LogicalAuthorizationRoute.php: -------------------------------------------------------------------------------- 1 | la = $la; 56 | $this->treeBuilder = $treeBuilder; 57 | $this->router = $router; 58 | $this->helper = $helper; 59 | $this->debugCollector = $debugCollector; 60 | } 61 | 62 | /** 63 | * {@inheritdoc} 64 | * 65 | * @return array 66 | */ 67 | public function getAvailableRoutes($user = null): array 68 | { 69 | if ($user instanceof ModelDecoratorInterface) { 70 | $user = $user->getModel(); 71 | } 72 | if (is_null($user)) { 73 | $user = $this->helper->getCurrentUser(); 74 | } 75 | 76 | $routes = []; 77 | foreach ($this->router->getRouteCollection()->getIterator() as $routeName => $route) { 78 | if (!$this->checkRouteAccess($routeName, $user)) { 79 | continue; 80 | } 81 | 82 | if (!isset($routes['routes'])) { 83 | $routes['routes'] = []; 84 | } 85 | $routes['routes'][$route->getPath()] = $route->getPath(); 86 | } 87 | 88 | $tree = $this->treeBuilder->getTree(); 89 | if (!empty($tree['route_patterns'])) { 90 | foreach ($tree['route_patterns'] as $pattern => $permissions) { 91 | if (!$this->la->checkAccess($permissions, ['user' => $user])) { 92 | continue; 93 | } 94 | 95 | if (!isset($routes['route_patterns'])) { 96 | $routes['route_patterns'] = []; 97 | } 98 | $routes['route_patterns'][$pattern] = $pattern; 99 | } 100 | } 101 | 102 | return $routes; 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | * 108 | * @return bool 109 | */ 110 | public function checkRouteAccess(string $routeName, $user = null): bool 111 | { 112 | if ($user instanceof ModelDecoratorInterface) { 113 | $user = $user->getModel(); 114 | } 115 | if (is_null($user)) { 116 | $user = $this->helper->getCurrentUser(); 117 | if (is_null($user)) { 118 | if (!is_null($this->debugCollector)) { 119 | $this->debugCollector->addPermissionCheck(true, 'route', $routeName, $user, [], [], 'No user was available during this permission check (not even an anonymous user). This usually happens during unit testing. Access was therefore automatically granted.'); 120 | } 121 | 122 | return true; 123 | } 124 | } 125 | 126 | if (!$routeName) { 127 | $this->helper->handleError('Error checking route access: the route_name parameter cannot be empty.', ['route' => $routeName, 'user' => $user]); 128 | if (!is_null($this->debugCollector)) { 129 | $this->debugCollector->addPermissionCheck(false, 'route', $routeName, $user, [], [], 'There was an error checking the route access and access was therefore automatically denied. Please refer to the error log for more information.'); 130 | } 131 | 132 | return false; 133 | } 134 | if (!is_string($user) && !is_object($user)) { 135 | $this->helper->handleError('Error checking route access: the user parameter must be either a string or an object.', ['route' => $routeName, 'user' => $user]); 136 | if (!is_null($this->debugCollector)) { 137 | $this->debugCollector->addPermissionCheck(false, 'route', $routeName, $user, [], [], 'There was an error checking the route access and access was therefore automatically denied. Please refer to the error log for more information.'); 138 | } 139 | 140 | return false; 141 | } 142 | 143 | $route = $this->router->getRouteCollection()->get($routeName); 144 | if (is_null($route)) { 145 | $this->helper->handleError('Error checking route access: the route could not be found.', ['route' => $routeName, 'user' => $user]); 146 | if (!is_null($this->debugCollector)) { 147 | $this->debugCollector->addPermissionCheck(false, 'route', $routeName, $user, [], [], 'There was an error checking the route access and access was therefore automatically denied. Please refer to the error log for more information.'); 148 | } 149 | 150 | return false; 151 | } 152 | 153 | $permissions = $this->getRoutePermissions($routeName); 154 | $context = ['route' => $routeName, 'user' => $user]; 155 | $access = $this->la->checkAccess($permissions, $context); 156 | 157 | if (!is_null($this->debugCollector)) { 158 | $this->debugCollector->addPermissionCheck($access, 'route', $routeName, $user, $permissions, $context); 159 | } 160 | 161 | return $access; 162 | } 163 | 164 | /** 165 | * @internal 166 | * 167 | * @param string $routeName 168 | * 169 | * @return array|string|bool 170 | */ 171 | protected function getRoutePermissions(string $routeName) 172 | { 173 | //If permissions are defined for an individual route, pattern permissions are completely ignored for that route. 174 | $tree = $this->treeBuilder->getTree(); 175 | 176 | //Check individual route permissions 177 | if (!empty($tree['routes']) && array_key_exists($routeName, $tree['routes'])) { 178 | return $tree['routes'][$routeName]; 179 | } 180 | 181 | //Check pattern permissions 182 | if (!empty($tree['route_patterns'])) { 183 | $route = $this->router->getRouteCollection()->get($routeName); 184 | if ($route) { 185 | $routePath = $route->getPath(); 186 | foreach ($tree['route_patterns'] as $pattern => $permissions) { 187 | if (preg_match("@$pattern@", $routePath)) { 188 | return $permissions; 189 | } 190 | } 191 | } 192 | } 193 | 194 | return []; 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /Tests/Functional/Services/LogicalAuthorizationModelTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($this->laModel->checkModelAccess(get_class($model), 'create', static::$admin_user)); 15 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'read', static::$admin_user)); 16 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'update', static::$admin_user)); 17 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'delete', static::$admin_user)); 18 | } 19 | 20 | public function testModelRoleDisallow() 21 | { 22 | $model = new TestModelRoleAuthor(); 23 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'create', static::$authenticated_user)); 24 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'read', static::$authenticated_user)); 25 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'update', static::$authenticated_user)); 26 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'delete', static::$authenticated_user)); 27 | } 28 | 29 | public function testModelFlagBypassAccessAllow() 30 | { 31 | $model = new TestModelRoleAuthor(); 32 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'create', static::$superadmin_user)); 33 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'read', static::$superadmin_user)); 34 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'update', static::$superadmin_user)); 35 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'delete', static::$superadmin_user)); 36 | } 37 | 38 | public function testModelFlagBypassAccessDisallow() 39 | { 40 | $model = new TestModelNoBypass(); 41 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'create', static::$superadmin_user)); 42 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'read', static::$superadmin_user)); 43 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'update', static::$superadmin_user)); 44 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'delete', static::$superadmin_user)); 45 | } 46 | 47 | public function testModelFlagHasAccountAllow() 48 | { 49 | $model = new TestModelHasAccountNoInterface(); 50 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'create', static::$authenticated_user)); 51 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'read', static::$authenticated_user)); 52 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'update', static::$authenticated_user)); 53 | $this->assertTrue($this->laModel->checkModelAccess(get_class($model), 'delete', static::$authenticated_user)); 54 | } 55 | 56 | public function testModelFlagHasAccountDisallow() 57 | { 58 | $model = new TestModelHasAccountNoInterface(); 59 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'create', 'anon.')); 60 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'read', 'anon.')); 61 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'update', 'anon.')); 62 | $this->assertFalse($this->laModel->checkModelAccess(get_class($model), 'delete', 'anon.')); 63 | } 64 | 65 | public function testModelFlagIsAuthorAllow() 66 | { 67 | static::$authenticated_user->setId(1); 68 | $model = new TestModelRoleAuthor(); 69 | $model->setAuthor(static::$authenticated_user); 70 | $this->assertTrue($this->laModel->checkModelAccess($model, 'read', static::$authenticated_user)); 71 | $this->assertTrue($this->laModel->checkModelAccess($model, 'update', static::$authenticated_user)); 72 | $this->assertTrue($this->laModel->checkModelAccess($model, 'delete', static::$authenticated_user)); 73 | } 74 | 75 | public function testModelFlagIsAuthorDisallow() 76 | { 77 | static::$authenticated_user->setId(1); 78 | static::$admin_user->setId(2); 79 | $model = new TestModelRoleAuthor(); 80 | $model->setAuthor(static::$admin_user); 81 | $this->assertFalse($this->laModel->checkModelAccess($model, 'read', static::$authenticated_user)); 82 | $this->assertFalse($this->laModel->checkModelAccess($model, 'update', static::$authenticated_user)); 83 | $this->assertFalse($this->laModel->checkModelAccess($model, 'delete', static::$authenticated_user)); 84 | } 85 | 86 | public function testUserFlagIsAuthor() 87 | { 88 | $this->assertTrue($this->laModel->checkModelAccess(static::$authenticated_user, 'read', static::$authenticated_user)); 89 | $this->assertTrue($this->laModel->checkModelAccess(static::$authenticated_user, 'update', static::$authenticated_user)); 90 | static::$authenticated_user->setBypassAccess(true); 91 | $this->assertFalse($this->laModel->checkModelAccess(static::$authenticated_user, 'delete', static::$authenticated_user)); 92 | $this->assertTrue($this->laModel->checkModelAccess(static::$authenticated_user, 'delete', static::$admin_user)); 93 | static::$authenticated_user->setBypassAccess(false); 94 | } 95 | 96 | public function testFieldRoleAllow() 97 | { 98 | $model = new TestModelRoleAuthor(); 99 | $this->assertTrue($this->laModel->checkFieldAccess(get_class($model), 'field1', 'set', static::$admin_user)); 100 | $this->assertTrue($this->laModel->checkFieldAccess(get_class($model), 'field1', 'get', static::$admin_user)); 101 | } 102 | 103 | public function testFieldRoleDisallow() 104 | { 105 | $model = new TestModelRoleAuthor(); 106 | $this->assertFalse($this->laModel->checkFieldAccess(get_class($model), 'field1', 'set', static::$authenticated_user)); 107 | $this->assertFalse($this->laModel->checkFieldAccess(get_class($model), 'field1', 'get', static::$authenticated_user)); 108 | } 109 | 110 | public function testFieldFlagBypassAccessAllow() 111 | { 112 | $model = new TestModelRoleAuthor(); 113 | $this->assertTrue($this->laModel->checkFieldAccess(get_class($model), 'field1', 'set', static::$superadmin_user)); 114 | $this->assertTrue($this->laModel->checkFieldAccess(get_class($model), 'field1', 'get', static::$superadmin_user)); 115 | } 116 | 117 | public function testFieldFlagBypassAccessDisallow() 118 | { 119 | $model = new TestModelNoBypass(); 120 | $this->assertFalse($this->laModel->checkFieldAccess(get_class($model), 'field1', 'set', static::$superadmin_user)); 121 | $this->assertFalse($this->laModel->checkFieldAccess(get_class($model), 'field1', 'get', static::$superadmin_user)); 122 | } 123 | 124 | public function testFieldFlagHasAccountAllow() 125 | { 126 | $model = new TestModelHasAccountNoInterface(); 127 | $this->assertTrue($this->laModel->checkFieldAccess(get_class($model), 'field1', 'set', static::$authenticated_user)); 128 | $this->assertTrue($this->laModel->checkFieldAccess(get_class($model), 'field1', 'get', static::$authenticated_user)); 129 | } 130 | 131 | public function testFieldFlagHasAccountDisallow() 132 | { 133 | $model = new TestModelHasAccountNoInterface(); 134 | $this->assertFalse($this->laModel->checkFieldAccess(get_class($model), 'field1', 'set', 'anon.')); 135 | $this->assertFalse($this->laModel->checkFieldAccess(get_class($model), 'field1', 'get', 'anon.')); 136 | } 137 | 138 | public function testFieldFlagIsAuthorAllow() 139 | { 140 | static::$authenticated_user->setId(1); 141 | $model = new TestModelRoleAuthor(); 142 | $model->setAuthor(static::$authenticated_user); 143 | $this->assertTrue($this->laModel->checkFieldAccess($model, 'field1', 'set', static::$authenticated_user)); 144 | $this->assertTrue($this->laModel->checkFieldAccess($model, 'field1', 'get', static::$authenticated_user)); 145 | } 146 | 147 | public function testFieldFlagIsAuthorDisallow() 148 | { 149 | static::$authenticated_user->setId(1); 150 | static::$admin_user->setId(2); 151 | $model = new TestModelRoleAuthor(); 152 | $model->setAuthor(static::$admin_user); 153 | $this->assertFalse($this->laModel->checkFieldAccess($model, 'field1', 'set', static::$authenticated_user)); 154 | $this->assertFalse($this->laModel->checkFieldAccess($model, 'field1', 'get', static::$authenticated_user)); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /DataCollector/Collector.php: -------------------------------------------------------------------------------- 1 | treeBuilder = $treeBuilder; 48 | $this->lpProxy = $lpProxy; 49 | $this->permissionLog = []; 50 | $this->data = []; 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | public function getName(): string 57 | { 58 | return 'logauth.collector'; 59 | } 60 | 61 | /** 62 | * {@inheritdoc} 63 | */ 64 | public function collect(Request $request, Response $response, \Exception $exception = null) 65 | { 66 | $log = $this->formatLog($this->permissionLog); 67 | $this->data = [ 68 | 'tree' => $this->treeBuilder->getTree(), 69 | 'log' => $log, 70 | ]; 71 | } 72 | 73 | /** 74 | * {@inheritdoc} 75 | */ 76 | public function lateCollect() 77 | { 78 | $this->data['tree'] = $this->cloneVar($this->data['tree']); 79 | foreach ($this->data['log'] as &$logItem) { 80 | if (!empty($logItem['item'])) { 81 | $logItem['item'] = $this->cloneVar($logItem['item']); 82 | } 83 | if (!empty($logItem['user']) && $logItem['user'] !== 'anon.') { 84 | $logItem['user'] = $this->cloneVar($logItem['user']); 85 | } 86 | if (!empty($logItem['backtrace'])) { 87 | $logItem['backtrace'] = $this->cloneVar($logItem['backtrace']); 88 | } 89 | } 90 | unset($logItem); 91 | } 92 | 93 | /** 94 | * {@inheritdoc} 95 | */ 96 | public function reset() 97 | { 98 | $this->data = []; 99 | } 100 | 101 | /** 102 | * {@inheritdoc} 103 | */ 104 | public function getPermissionTree(): Data 105 | { 106 | return $this->data['tree']; 107 | } 108 | 109 | /** 110 | * {@inheritdoc} 111 | */ 112 | public function getLog(): array 113 | { 114 | return $this->data['log']; 115 | } 116 | 117 | /** 118 | * {@inheritdoc} 119 | */ 120 | public function addPermissionCheck(bool $access, string $type, $item, $user, $permissions, array $context, string $message = '') 121 | { 122 | $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 11); 123 | array_shift($backtrace); 124 | $this->addPermissionLogItem(['access' => $access, 'type' => $type, 'item' => $item, 'user' => $user, 'permissions' => $permissions, 'context' => $context, 'message' => $message, 'backtrace' => $backtrace]); 125 | } 126 | 127 | /** 128 | * @internal 129 | * 130 | * @param array $logItem 131 | */ 132 | protected function addPermissionLogItem(array $logItem) 133 | { 134 | $this->permissionLog[] = $logItem; 135 | } 136 | 137 | /** 138 | * @internal 139 | * 140 | * @param array $log 141 | * 142 | * @return array 143 | */ 144 | protected function formatLog(array $log): array 145 | { 146 | foreach ($log as &$logItem) { 147 | if ($logItem['type'] === 'model' || $logItem['type'] === 'field') { 148 | $logItem['action'] = $logItem['item']['action']; 149 | } 150 | 151 | if ($logItem['type'] === 'field') { 152 | $logItem['field'] = $logItem['item']['field']; 153 | } 154 | 155 | $formattedItem = $this->formatItem($logItem['type'], $logItem['item']); 156 | unset($logItem['item']); 157 | $logItem += $formattedItem; 158 | 159 | if (!empty($logItem['message'])) { 160 | continue; 161 | } 162 | 163 | if (is_array($logItem['permissions']) && array_key_exists('no_bypass', $logItem['permissions'])) { 164 | $logItem['permissions']['NO_BYPASS'] = $logItem['permissions']['no_bypass']; 165 | unset($logItem['permissions']['no_bypass']); 166 | } 167 | 168 | $typeKeys = array_keys($this->lpProxy->getTypes()); 169 | 170 | $logItem['permission_no_bypass_checks'] = array_reverse($this->getPermissionNoBypassChecks($logItem['permissions'], $logItem['context'], $typeKeys)); 171 | if (count($logItem['permission_no_bypass_checks']) == 1 && !empty($logItem['permission_no_bypass_checks'][0]['error'])) { 172 | $logItem['message'] = $logItem['permission_no_bypass_checks'][0]['error']; 173 | } 174 | 175 | $logItem['bypassed_access'] = $this->getBypassedAccess($logItem['permissions'], $logItem['context']); 176 | 177 | $purePermissions = $logItem['permissions']; 178 | unset($purePermissions['NO_BYPASS']); 179 | 180 | $logItem['permission_checks'] = array_reverse($this->getPermissionChecks($purePermissions, $logItem['context'], $typeKeys)); 181 | if (count($logItem['permission_checks']) == 1 && !empty($logItem['permission_checks'][0]['error'])) { 182 | $logItem['message'] = $logItem['permission_checks'][0]['error']; 183 | } 184 | 185 | unset($logItem['context']); 186 | } 187 | unset($logItem); 188 | 189 | return $log; 190 | } 191 | 192 | /** 193 | * @internal 194 | * 195 | * @param string $type 196 | * @param string|array $item 197 | * 198 | * @return array 199 | */ 200 | protected function formatItem(string $type, $item): array 201 | { 202 | $formattedItem = []; 203 | 204 | if ('route' === $type) { 205 | return [ 206 | 'item_name' => $item, 207 | ]; 208 | } 209 | 210 | $model = $item['model']; 211 | $formattedItem['item_name'] = $model; 212 | if (is_object($model)) { 213 | $formattedItem['item'] = $model; 214 | $formattedItem['item_name'] = get_class($model); 215 | } 216 | if ('field' === $type) { 217 | $formattedItem['item_name'] .= ":{$item['field']}"; 218 | } 219 | 220 | return $formattedItem; 221 | } 222 | 223 | /** 224 | * @internal 225 | * 226 | * @param string|array|bool $permissions 227 | * @param array $context 228 | * @param array $typeKeys 229 | * 230 | * @return array 231 | */ 232 | protected function getPermissionChecks($permissions, array $context, array $typeKeys): array 233 | { 234 | // Extra permission check of the whole tree to catch errors 235 | try { 236 | $this->lpProxy->checkAccess($permissions, $context, false); 237 | } catch (\Exception $e) { 238 | return [[ 239 | 'permissions' => $permissions, 240 | 'resolve' => false, 241 | 'error' => $e->getMessage(), 242 | ], ]; 243 | } 244 | 245 | $checks = []; 246 | 247 | if (is_array($permissions)) { 248 | foreach ($permissions as $key => $value) { 249 | $checks = array_merge($checks, $this->getPermissionChecksRecursive([$key => $value], $context, $typeKeys)); 250 | } 251 | if (count($permissions) > 1) { 252 | $checks[] = [ 253 | 'permissions' => $permissions, 254 | 'resolve' => $this->lpProxy->checkAccess($permissions, $context, false), 255 | ]; 256 | } 257 | } else { 258 | $checks = array_merge($checks, $this->getPermissionChecksRecursive($permissions, $context, $typeKeys)); 259 | } 260 | 261 | return $checks; 262 | } 263 | 264 | /** 265 | * @internal 266 | * 267 | * @param string|array|bool $permissions 268 | * @param array $context 269 | * @param array $typeKeys 270 | * @param string|null $type 271 | * 272 | * @return array 273 | */ 274 | protected function getPermissionChecksRecursive($permissions, array $context, array $typeKeys, string $type = null): array { 275 | if (!is_array($permissions)) { 276 | $resolvePermissions = $permissions; 277 | if ($type) { 278 | $resolvePermissions = [$type => $permissions]; 279 | } 280 | 281 | return [ 282 | [ 283 | 'permissions' => $permissions, 284 | 'resolve' => $this->lpProxy->checkAccess($resolvePermissions, $context, false), 285 | ], 286 | ]; 287 | } 288 | 289 | reset($permissions); 290 | $key = key($permissions); 291 | $value = current($permissions); 292 | 293 | if (is_numeric($key)) { 294 | return $this->getPermissionChecksRecursive($value, $context, $typeKeys, $type); 295 | } 296 | 297 | if (in_array($key, $typeKeys, true)) { 298 | $type = $key; 299 | } 300 | 301 | if (is_array($value)) { 302 | $checks = []; 303 | foreach ($value as $key2 => $value2) { 304 | $checks = array_merge($checks, $this->getPermissionChecksRecursive([$key2 => $value2], $context, $typeKeys, $type)); 305 | } 306 | $resolvePermissions = $permissions; 307 | if ($type && $key !== $type) { 308 | $resolvePermissions = [$type => $permissions]; 309 | } 310 | $checks[] = [ 311 | 'permissions' => $permissions, 312 | 'resolve' => $this->lpProxy->checkAccess($resolvePermissions, $context, false), 313 | ]; 314 | 315 | return $checks; 316 | } 317 | 318 | if ($key === $type) { 319 | return [[ 320 | 'permissions' => $permissions, 321 | 'resolve' => $this->lpProxy->checkAccess($permissions, $context, false), 322 | ], ]; 323 | } 324 | 325 | $checks = []; 326 | $resolveValue = $value; 327 | if ($type) { 328 | $resolveValue = [$type => $resolveValue]; 329 | } 330 | $checks[] = [ 331 | 'permissions' => $value, 332 | 'resolve' => $this->lpProxy->checkAccess($resolveValue, $context, false), 333 | ]; 334 | 335 | $resolvePermissions = $permissions; 336 | if ($type) { 337 | $resolvePermissions = [$type => $resolvePermissions]; 338 | } 339 | $checks[] = [ 340 | 'permissions' => $permissions, 341 | 'resolve' => $this->lpProxy->checkAccess($resolvePermissions, $context, false), 342 | ]; 343 | 344 | return $checks; 345 | } 346 | 347 | /** 348 | * @internal 349 | * 350 | * @param string|array|bool $permissions 351 | * @param array $context 352 | * @param array $typeKeys 353 | * 354 | * @return array 355 | */ 356 | protected function getPermissionNoBypassChecks($permissions, array $context, array $typeKeys): array 357 | { 358 | if (is_array($permissions) && array_key_exists('NO_BYPASS', $permissions)) { 359 | return $this->getPermissionChecks($permissions['NO_BYPASS'], $context, $typeKeys); 360 | } 361 | 362 | return []; 363 | } 364 | 365 | /** 366 | * @internal 367 | * 368 | * @param string|array|bool $permissions 369 | * @param array $context 370 | * 371 | * @return bool 372 | */ 373 | protected function getBypassedAccess($permissions, array $context): bool 374 | { 375 | $newPermissions = [false]; 376 | if (is_array($permissions) && array_key_exists('NO_BYPASS', $permissions)) { 377 | $newPermissions['NO_BYPASS'] = $permissions['NO_BYPASS']; 378 | } 379 | 380 | try { 381 | return $this->lpProxy->checkAccess($newPermissions, $context); 382 | } catch (\Exception $e) { 383 | } 384 | 385 | return false; 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /Tests/Functional/Services/LogicalAuthorizationRouteTest.php: -------------------------------------------------------------------------------- 1 | sendRequestAs('GET', '/test/route-role', [], static::$admin_user); 10 | $response = $this->client->getResponse(); 11 | $this->assertEquals(200, $response->getStatusCode()); 12 | } 13 | 14 | public function testRouteRoleMultipleAllow() 15 | { 16 | $this->sendRequestAs('GET', '/test/route-role-multiple', [], static::$admin_user); 17 | $response = $this->client->getResponse(); 18 | $this->assertEquals(200, $response->getStatusCode()); 19 | } 20 | 21 | public function testRouteRoleDisallow() 22 | { 23 | $this->sendRequestAs('GET', '/test/route-role', [], static::$authenticated_user); 24 | $response = $this->client->getResponse(); 25 | $this->assertEquals(403, $response->getStatusCode()); 26 | } 27 | 28 | public function testRouteBypassActionAllow() 29 | { 30 | $this->sendRequestAs('GET', '/test/route-role', [], static::$superadmin_user); 31 | $response = $this->client->getResponse(); 32 | $this->assertEquals(200, $response->getStatusCode()); 33 | } 34 | 35 | public function testRouteBypassActionDisallow() 36 | { 37 | $this->sendRequestAs('GET', '/test/route-no-bypass', [], static::$superadmin_user); 38 | $response = $this->client->getResponse(); 39 | $this->assertEquals(403, $response->getStatusCode()); 40 | } 41 | 42 | public function testRouteHostAllow() 43 | { 44 | $client = static::createClient([], ['HTTP_HOST' => 'test.com']); 45 | $headers = [ 46 | 'PHP_AUTH_USER' => static::$authenticated_user->getUsername(), 47 | 'PHP_AUTH_PW' => $this->user_credentials[static::$authenticated_user->getUsername()], 48 | ]; 49 | $client->request('GET', '/test/route-host', [], [], $headers); 50 | $response = $client->getResponse(); 51 | $this->assertEquals(200, $response->getStatusCode()); 52 | } 53 | 54 | public function testRouteHostDisallow() 55 | { 56 | $client = static::createClient([], ['HTTP_HOST' => 'test.se']); 57 | $headers = [ 58 | 'PHP_AUTH_USER' => static::$authenticated_user->getUsername(), 59 | 'PHP_AUTH_PW' => $this->user_credentials[static::$authenticated_user->getUsername()], 60 | ]; 61 | $client->request('GET', '/test/route-host', [], [], $headers); 62 | $response = $client->getResponse(); 63 | $this->assertEquals(403, $response->getStatusCode()); 64 | } 65 | 66 | public function testRouteMethodAllow() 67 | { 68 | $this->sendRequestAs('GET', '/test/route-method', [], static::$admin_user); 69 | $response = $this->client->getResponse(); 70 | $this->assertEquals(200, $response->getStatusCode()); 71 | } 72 | 73 | public function testRouteMethodLowercaseAllow() 74 | { 75 | $this->sendRequestAs('GET', '/test/route-method-lowercase', [], static::$admin_user); 76 | $response = $this->client->getResponse(); 77 | $this->assertEquals(200, $response->getStatusCode()); 78 | } 79 | 80 | public function testRouteMethodDisallow() 81 | { 82 | $this->sendRequestAs('PUSH', '/test/route-method', [], static::$authenticated_user); 83 | $response = $this->client->getResponse(); 84 | $this->assertEquals(403, $response->getStatusCode()); 85 | } 86 | 87 | public function testRouteIpAllow() 88 | { 89 | $client = static::createClient([], ['REMOTE_ADDR' => '127.0.0.1']); 90 | $headers = [ 91 | 'PHP_AUTH_USER' => static::$authenticated_user->getUsername(), 92 | 'PHP_AUTH_PW' => $this->user_credentials[static::$authenticated_user->getUsername()], 93 | ]; 94 | $client->request('GET', '/test/route-ip', [], [], $headers); 95 | $response = $client->getResponse(); 96 | $this->assertEquals(200, $response->getStatusCode()); 97 | } 98 | 99 | public function testRouteIpDisallow() 100 | { 101 | $client = static::createClient([], ['REMOTE_ADDR' => '127.0.0.55']); 102 | $headers = [ 103 | 'PHP_AUTH_USER' => static::$authenticated_user->getUsername(), 104 | 'PHP_AUTH_PW' => $this->user_credentials[static::$authenticated_user->getUsername()], 105 | ]; 106 | $client->request('GET', '/test/route-ip', [], [], $headers); 107 | $response = $client->getResponse(); 108 | $this->assertEquals(403, $response->getStatusCode()); 109 | } 110 | 111 | public function testRouteHasAccountAllow() 112 | { 113 | $this->sendRequestAs('GET', '/test/route-has-account', [], static::$authenticated_user); 114 | $response = $this->client->getResponse(); 115 | $this->assertEquals(200, $response->getStatusCode()); 116 | } 117 | 118 | public function testRouteHasAccountDisallow() 119 | { 120 | $this->sendRequestAs('GET', '/test/route-has-account', []); 121 | $response = $this->client->getResponse(); 122 | $this->assertEquals(401, $response->getStatusCode()); 123 | } 124 | 125 | public function testMultipleRoute1Allow() 126 | { 127 | $this->sendRequestAs('GET', '/test/multiple-route-1', [], static::$admin_user); 128 | $response = $this->client->getResponse(); 129 | $this->assertEquals(200, $response->getStatusCode()); 130 | } 131 | 132 | public function testMultipleRoute2Allow() 133 | { 134 | $this->sendRequestAs('GET', '/test/multiple-route-2', [], static::$admin_user); 135 | $response = $this->client->getResponse(); 136 | $this->assertEquals(200, $response->getStatusCode()); 137 | } 138 | 139 | public function testMultipleRoute1Disallow() 140 | { 141 | $this->sendRequestAs('GET', '/test/multiple-route-1', [], static::$authenticated_user); 142 | $response = $this->client->getResponse(); 143 | $this->assertEquals(403, $response->getStatusCode()); 144 | } 145 | 146 | public function testMultipleRoute2Disallow() 147 | { 148 | $this->sendRequestAs('GET', '/test/multiple-route-2', [], static::$authenticated_user); 149 | $response = $this->client->getResponse(); 150 | $this->assertEquals(403, $response->getStatusCode()); 151 | } 152 | 153 | public function testYmlRouteAllow() 154 | { 155 | $this->sendRequestAs('GET', '/test/route-yml', [], static::$admin_user); 156 | $response = $this->client->getResponse(); 157 | $this->assertEquals(200, $response->getStatusCode()); 158 | } 159 | 160 | public function testYmlRouteDisallow() 161 | { 162 | $this->sendRequestAs('GET', '/test/route-yml', [], static::$authenticated_user); 163 | $response = $this->client->getResponse(); 164 | $this->assertEquals(403, $response->getStatusCode()); 165 | } 166 | 167 | public function testYmlRouteBoolAllow() 168 | { 169 | $this->sendRequestAs('GET', '/test/route-yml-allowed', []); 170 | $response = $this->client->getResponse(); 171 | $this->assertEquals(200, $response->getStatusCode()); 172 | } 173 | 174 | public function testYmlRouteBoolDisallow() 175 | { 176 | $this->sendRequestAs('GET', '/test/route-yml-denied', [], static::$admin_user); 177 | $response = $this->client->getResponse(); 178 | $this->assertEquals(403, $response->getStatusCode()); 179 | } 180 | 181 | public function testXmlRouteAllow() 182 | { 183 | $this->sendRequestAs('GET', '/test/route-xml', [], static::$admin_user); 184 | $response = $this->client->getResponse(); 185 | $this->assertEquals(200, $response->getStatusCode()); 186 | } 187 | 188 | public function testXmlRouteDisallow() 189 | { 190 | $this->sendRequestAs('GET', '/test/route-xml', [], static::$authenticated_user); 191 | $response = $this->client->getResponse(); 192 | $this->assertEquals(403, $response->getStatusCode()); 193 | } 194 | 195 | public function testXmlRouteBoolAllow() 196 | { 197 | $this->sendRequestAs('GET', '/test/route-xml-allowed', []); 198 | $response = $this->client->getResponse(); 199 | $this->assertEquals(200, $response->getStatusCode()); 200 | } 201 | 202 | public function testXmlRouteBoolDisallow() 203 | { 204 | $this->sendRequestAs('GET', '/test/route-xml-denied', [], static::$admin_user); 205 | $response = $this->client->getResponse(); 206 | $this->assertEquals(403, $response->getStatusCode()); 207 | } 208 | 209 | public function testRouteBoolAllow() 210 | { 211 | $this->sendRequestAs('GET', '/test/pattern-allowed', []); 212 | $response = $this->client->getResponse(); 213 | $this->assertEquals(200, $response->getStatusCode()); 214 | } 215 | 216 | public function testRouteBoolDeny() 217 | { 218 | $this->sendRequestAs('GET', '/test/route-denied', [], static::$admin_user); 219 | $response = $this->client->getResponse(); 220 | $this->assertEquals(403, $response->getStatusCode()); 221 | } 222 | 223 | public function testRouteComplexAllow() 224 | { 225 | $this->sendRequestAs('GET', '/test/route-complex', [], static::$admin_user); 226 | $response = $this->client->getResponse(); 227 | $this->assertEquals(200, $response->getStatusCode()); 228 | } 229 | 230 | public function testRouteComplexDeny() 231 | { 232 | $this->sendRequestAs('GET', '/test/route-complex', [], static::$authenticated_user); 233 | $response = $this->client->getResponse(); 234 | $this->assertEquals(403, $response->getStatusCode()); 235 | } 236 | 237 | public function testRoutePatternDenyAll() 238 | { 239 | $this->sendRequestAs('GET', '/test/route-forbidden', [], static::$superadmin_user); 240 | $response = $this->client->getResponse(); 241 | $this->assertEquals(403, $response->getStatusCode()); 242 | } 243 | 244 | public function testRoutePatternOverriddenAllow() 245 | { 246 | $this->sendRequestAs('GET', '/test/route-allowed', []); 247 | $response = $this->client->getResponse(); 248 | $this->assertEquals(200, $response->getStatusCode()); 249 | } 250 | 251 | public function testRoutePatternOverriddenDeny() 252 | { 253 | $this->sendRequestAs('GET', '/test/pattern-forbidden', [], static::$superadmin_user); 254 | $response = $this->client->getResponse(); 255 | $this->assertEquals(403, $response->getStatusCode()); 256 | } 257 | 258 | public function testAvailableRoutesAnonymous() 259 | { 260 | $this->sendRequestAs('GET', '/test/count-available-routes', []); 261 | $response = $this->client->getResponse(); 262 | $this->assertEquals(200, $response->getStatusCode()); 263 | $routes_count = $response->getContent(); 264 | $this->assertGreaterThan(3, $routes_count); 265 | } 266 | 267 | public function testAvailableRoutesAuthenticated() 268 | { 269 | $this->sendRequestAs('GET', '/test/count-available-routes', [], static::$authenticated_user); 270 | $response = $this->client->getResponse(); 271 | $this->assertEquals(200, $response->getStatusCode()); 272 | $routes_count = $response->getContent(); 273 | $this->assertGreaterThan(4, $routes_count); 274 | } 275 | 276 | public function testAvailableRoutesAdmin() 277 | { 278 | $this->sendRequestAs('GET', '/test/count-available-routes', [], static::$admin_user); 279 | $response = $this->client->getResponse(); 280 | $this->assertEquals(200, $response->getStatusCode()); 281 | $routes_count = $response->getContent(); 282 | $this->assertGreaterThan(5, $routes_count); 283 | } 284 | 285 | public function testAvailableRoutesSuperadmin() 286 | { 287 | $this->sendRequestAs('GET', '/test/count-available-routes', [], static::$superadmin_user); 288 | $response = $this->client->getResponse(); 289 | $this->assertEquals(200, $response->getStatusCode()); 290 | $routes_count = $response->getContent(); 291 | $this->assertGreaterThan(5, $routes_count); 292 | } 293 | 294 | public function testAvailableRoutePatternsAnonymous() 295 | { 296 | $this->sendRequestAs('GET', '/test/count-available-route-patterns', []); 297 | $response = $this->client->getResponse(); 298 | $this->assertEquals(200, $response->getStatusCode()); 299 | $routes_count = $response->getContent(); 300 | $this->assertEquals(1, $routes_count); 301 | } 302 | 303 | public function testAvailableRoutePatternsAuthenticated() 304 | { 305 | $this->sendRequestAs('GET', '/test/count-available-route-patterns', [], static::$authenticated_user); 306 | $response = $this->client->getResponse(); 307 | $this->assertEquals(200, $response->getStatusCode()); 308 | $routes_count = $response->getContent(); 309 | $this->assertEquals(1, $routes_count); 310 | } 311 | 312 | public function testAvailableRoutePatternsAdmin() 313 | { 314 | $this->sendRequestAs('GET', '/test/count-available-route-patterns', [], static::$admin_user); 315 | $response = $this->client->getResponse(); 316 | $this->assertEquals(200, $response->getStatusCode()); 317 | $routes_count = $response->getContent(); 318 | $this->assertEquals(1, $routes_count); 319 | } 320 | 321 | public function testAvailableRoutePatternsSuperadmin() 322 | { 323 | $this->sendRequestAs('GET', '/test/count-available-route-patterns', [], static::$superadmin_user); 324 | $response = $this->client->getResponse(); 325 | $this->assertEquals(200, $response->getStatusCode()); 326 | $routes_count = $response->getContent(); 327 | $this->assertEquals(1, $routes_count); 328 | } 329 | } 330 | --------------------------------------------------------------------------------