├── 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 | [](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 |
--------------------------------------------------------------------------------