├── data
└── cache
│ ├── text
│ └── .gitkeep
│ └── test
│ └── text
│ └── .gitkeep
├── src
├── Cache
│ ├── Exception
│ │ └── ExpiredCacheException.php
│ ├── Item
│ │ ├── TextCacheItem.php
│ │ └── MemoryCacheItem.php
│ └── Pool
│ │ ├── MemoryCacheItemPool.php
│ │ └── TextCacheItemPool.php
├── Configuration
│ ├── ConfigurationInterface.php
│ └── Configuration.php
├── Manager
│ ├── PolicyRuleManagerInterface.php
│ ├── AttributeManagerInterface.php
│ ├── ComparisonManagerInterface.php
│ ├── CacheManagerInterface.php
│ ├── CacheManager.php
│ ├── PolicyRuleManager.php
│ ├── AttributeManager.php
│ └── ComparisonManager.php
├── Comparison
│ ├── StringComparison.php
│ ├── AbstractComparison.php
│ ├── UserComparison.php
│ ├── BooleanComparison.php
│ ├── ObjectComparison.php
│ ├── NumericComparison.php
│ ├── DatetimeComparison.php
│ └── ArrayComparison.php
├── Model
│ ├── Attribute.php
│ ├── EnvironmentAttribute.php
│ ├── PolicyRule.php
│ ├── AbstractAttribute.php
│ └── PolicyRuleAttribute.php
├── Loader
│ ├── JsonLoader.php
│ └── YamlLoader.php
├── AbacFactory.php
└── Abac.php
├── .php_cs
├── tests
├── fixtures
│ ├── resources
│ │ ├── country.yml
│ │ └── visa.yml
│ ├── countries.php
│ ├── visas.php
│ ├── users
│ │ └── main_user.yml
│ ├── policy_rules_with_import.yml
│ ├── users.php
│ ├── policy_rules_with_array.yml
│ ├── vehicles.php
│ ├── policy_rules_with_getter_params.yml
│ ├── policy_rules_with_array.json
│ ├── policy_rules.yml
│ └── policy_rules.json
├── Configuration
│ └── ConfigurationTest.php
├── Model
│ ├── AttributeTest.php
│ ├── EnvironmentAttributeTest.php
│ ├── PolicyRuleAttributeTest.php
│ └── PolicyRuleTest.php
├── Comparison
│ ├── StringComparisonTest.php
│ ├── DatetimeComparisonTest.php
│ ├── BooleanComparisonTest.php
│ ├── NumericComparisonTest.php
│ ├── UserComparisonTest.php
│ ├── ObjectComparisonTest.php
│ └── ArrayComparisonTest.php
├── Loader
│ ├── JsonLoaderTest.php
│ └── YamlLoaderTest.php
├── Cache
│ ├── Item
│ │ ├── TextCacheItemTest.php
│ │ └── MemoryCacheItemTest.php
│ └── Pool
│ │ ├── TextCacheItemPoolTest.php
│ │ └── MemoryCacheItemPoolTest.php
├── Manager
│ ├── CacheManagerTest.php
│ ├── ComparisonManagerTest.php
│ ├── AttributeManagerTest.php
│ └── PolicyRuleManagerTest.php
└── AbacTest.php
├── .scrutinizer.yml
├── .travis.yml
├── .gitignore
├── UPGRADE-3.0.md
├── example
├── Country.php
├── Visa.php
├── Vehicle.php
└── User.php
├── doc
├── comparisons.md
├── caching.md
├── dependency-injection.md
├── access-control.md
└── configuration.md
├── phpunit.xml.dist
├── composer.json
├── LICENSE
├── example.php
├── CHANGELOG.md
└── README.md
/data/cache/text/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/cache/test/text/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/Cache/Exception/ExpiredCacheException.php:
--------------------------------------------------------------------------------
1 | exclude(array('data', 'doc', 'sql', 'vendor'))
5 | ->in(__DIR__)
6 | ;
7 | return PhpCsFixer\Config::create()
8 | ->setRules(array('@PSR2' => true))
9 | ->setFinder($finder)
10 | ;
--------------------------------------------------------------------------------
/tests/fixtures/resources/country.yml:
--------------------------------------------------------------------------------
1 | ---
2 | attributes:
3 | country:
4 | class: PhpAbac\Example\Country
5 | type: resource
6 | fields:
7 | name:
8 | name: Nom du pays
9 | code:
10 | name: Code international
--------------------------------------------------------------------------------
/tests/fixtures/resources/visa.yml:
--------------------------------------------------------------------------------
1 | ---
2 | attributes:
3 | visa:
4 | class: PhpAbac\Example\Visa
5 | type: resource
6 | fields:
7 | country.code:
8 | name: Code Pays
9 | lastRenewal:
10 | name: Dernier renouvellement
--------------------------------------------------------------------------------
/src/Manager/PolicyRuleManagerInterface.php:
--------------------------------------------------------------------------------
1 | setName('France')
8 | ->setCode('FR'),
9 | (new Country())
10 | ->setName('United Kingdoms')
11 | ->setCode('UK'),
12 | (new Country())
13 | ->setName('United States')
14 | ->setCode('US'),
15 | ];
16 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | imports:
2 | - php
3 |
4 | tools:
5 | external_code_coverage:
6 | timeout: 600
7 | php_mess_detector: true
8 | php_cpd: true
9 | php_code_sniffer:
10 | config:
11 | standard: PSR2
12 | php_pdepend: true
13 | php_analyzer: true
14 | sensiolabs_security_checker: true
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/Manager/AttributeManagerInterface.php:
--------------------------------------------------------------------------------
1 | isEqual($expected, $value);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Manager/CacheManagerInterface.php:
--------------------------------------------------------------------------------
1 | property = $property;
13 |
14 | return $this;
15 | }
16 |
17 | public function getProperty(): string
18 | {
19 | return $this->property;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Loader/JsonLoader.php:
--------------------------------------------------------------------------------
1 | comparisonManager = $comparisonManager;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: false
4 |
5 | php:
6 | - 7.0
7 | - 7.1
8 | - 7.2
9 |
10 | before_script:
11 | - curl -s http://getcomposer.org/installer | php
12 | - php composer.phar install --dev --no-interaction
13 |
14 | script:
15 | - mkdir -p build/logs
16 | - phpunit --coverage-text --coverage-clover build/logs/clover.xml
17 |
18 | after_script:
19 | - wget https://scrutinizer-ci.com/ocular.phar
20 | - php ocular.phar code-coverage:upload --format=php-clover build/logs/clover.xml
21 |
--------------------------------------------------------------------------------
/tests/Configuration/ConfigurationTest.php:
--------------------------------------------------------------------------------
1 | assertCount(5, $configuration->getAttributes());
14 | $this->assertCount(4, $configuration->getRules());
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Model/EnvironmentAttribute.php:
--------------------------------------------------------------------------------
1 | variableName = $variableName;
13 |
14 | return $this;
15 | }
16 |
17 | public function getVariableName(): string
18 | {
19 | return $this->variableName;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/Loader/YamlLoader.php:
--------------------------------------------------------------------------------
1 | setId(1)
8 | ->setCountry($countries[0])
9 | ->setLastRenewal(new \DateTime())
10 | ->setCreatedAt(new \DateTime()),
11 | (new Visa())
12 | ->setId(2)
13 | ->setCountry($countries[1])
14 | ->setLastRenewal(new \DateTime())
15 | ->setCreatedAt(new \DateTime()),
16 | (new Visa())
17 | ->setId(3)
18 | ->setCountry($countries[2])
19 | ->setLastRenewal(new \DateTime())
20 | ->setCreatedAt(new \DateTime()),
21 | ];
22 |
--------------------------------------------------------------------------------
/src/Comparison/UserComparison.php:
--------------------------------------------------------------------------------
1 | comparisonManager->getAttributeManager();
10 | // Create an attribute out of the extra data we have and compare its retrieved value to the expected one
11 | return $attributeManager->retrieveAttribute(
12 | $attributeManager->getAttribute($attributeId),
13 | $extraData['user']
14 | ) === $value;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/Comparison/BooleanComparison.php:
--------------------------------------------------------------------------------
1 | comparisonManager->getAttributeManager();
10 | // Create an attribute out of the extra data we have and compare its retrieved value to the expected one
11 | return $attributeManager->retrieveAttribute(
12 | $attributeManager->getAttribute($attributeId),
13 | null,
14 | $extraData['resource']
15 | ) === $value;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /nbproject/
2 | code-coverage
3 | build
4 | phpunit.xml
5 | data/cache/text/*
6 | !data/cache/text/.gitkeep
7 | data/cache/test/text/*
8 | !data/cache/test/text/.gitkeep
9 | .php_cs.cache
10 | # Composer
11 | vendor
12 | composer.phar
13 | composer.lock
14 |
15 | # Exclude PHPStorm IDE File
16 | .idea/
17 | .idea/*
18 |
19 | # Exclude IntelliJ
20 | out/
21 |
22 | ## OSX
23 | .DS_Store
24 | .AppleDouble
25 | .LSOverride
26 |
27 | # Thumbnails
28 | ._*
29 |
30 | # Files that might appear on external disk
31 | .Spotlight-V100
32 | .Trashes
33 |
34 | # Directories potentially created on remote AFP share
35 | .AppleDB
36 | .AppleDesktop
37 | Network Trash Folder
38 | Temporary Items
39 | .apdisk
--------------------------------------------------------------------------------
/tests/Model/AttributeTest.php:
--------------------------------------------------------------------------------
1 | setName('test-attribute')
14 | ->setProperty('userAttributes')
15 | ->setType('resource')
16 | ->setValue([])
17 | ;
18 | $this->assertEquals('test-attribute', $attribute->getName());
19 | $this->assertEquals('userAttributes', $attribute->getProperty());
20 | $this->assertEquals('resource', $attribute->getType());
21 | $this->assertEquals([], $attribute->getValue());
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/tests/Model/EnvironmentAttributeTest.php:
--------------------------------------------------------------------------------
1 | setType('environment')
14 | ->setName('test-attribute')
15 | ->setVariableName('service-state')
16 | ->setValue(3)
17 | ;
18 | $this->assertEquals('environment', $attribute->getType());
19 | $this->assertEquals('test-attribute', $attribute->getName());
20 | $this->assertEquals('service-state', $attribute->getVariableName());
21 | $this->assertEquals(3, $attribute->getValue());
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Comparison/NumericComparison.php:
--------------------------------------------------------------------------------
1 | $value;
15 | }
16 |
17 | public function isLesserThanOrEqual(int $expected, int $value): bool
18 | {
19 | return $expected >= $value;
20 | }
21 |
22 | public function isGreaterThan(int $expected, int $value): bool
23 | {
24 | return $expected < $value;
25 | }
26 |
27 | public function isGreaterThanOrEqual(int $expected, int $value): bool
28 | {
29 | return $expected <= $value;
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/Model/PolicyRuleAttributeTest.php:
--------------------------------------------------------------------------------
1 | setAttribute(new Attribute())
17 | ->setComparisonType('String')
18 | ->setComparison('isEqual')
19 | ->setValue(true)
20 | ;
21 | $this->assertTrue($policyRuleAttribute->getValue());
22 | $this->assertInstanceof('PhpAbac\Model\Attribute', $policyRuleAttribute->getAttribute());
23 | $this->assertEquals('String', $policyRuleAttribute->getComparisonType());
24 | $this->assertEquals('isEqual', $policyRuleAttribute->getComparison());
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/fixtures/policy_rules_with_import.yml:
--------------------------------------------------------------------------------
1 | ---
2 | '@import':
3 | - users/main_user.yml
4 | - resources/visa.yml
5 | - resources/country.yml
6 |
7 | rules:
8 | travel-to-foreign-country:
9 | attributes:
10 | main_user.age:
11 | comparison_type: numeric
12 | comparison: isGreaterThan
13 | value: 18
14 | main_user.visa:
15 | comparison_type: array
16 | comparison: contains
17 | getter_params:
18 | visa:
19 | -
20 | param_name: '@country_code'
21 | param_value: country.code
22 | with:
23 | visa.lastRenewal:
24 | comparison_type: datetime
25 | comparison: isMoreRecentThan
26 | value: -1Y
27 |
--------------------------------------------------------------------------------
/UPGRADE-3.0.md:
--------------------------------------------------------------------------------
1 | Upgrade from 2.x to 3.0
2 | =======================
3 |
4 | PHP version
5 | ---------------
6 |
7 | The minimal supported PHP version is 7.0.
8 |
9 | Create Abac instance
10 | --------------------
11 |
12 | In 2.x, you were able to create an Abac instance the following way:
13 |
14 | ```php
15 | name = $name;
19 |
20 | return $this;
21 | }
22 |
23 | /**
24 | * @return string
25 | */
26 | public function getName()
27 | {
28 | return $this->name;
29 | }
30 |
31 | /**
32 | * @param string $code
33 | * @return \PhpAbac\Example\Country
34 | */
35 | public function setCode($code)
36 | {
37 | $this->code = $code;
38 |
39 | return $this;
40 | }
41 |
42 | /**
43 | * @return string
44 | */
45 | public function getCode()
46 | {
47 | return $this->code;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Model/PolicyRuleTest.php:
--------------------------------------------------------------------------------
1 | setName('citizenship')
22 | ->addPolicyRuleAttribute($pra1)
23 | ->addPolicyRuleAttribute($pra2)
24 | ->addPolicyRuleAttribute($pra3)
25 | ->addPolicyRuleAttribute($pra4)
26 | ->removePolicyRuleAttribute($pra4)
27 | ;
28 | $this->assertEquals('citizenship', $policyRule->getName());
29 | $this->assertCount(3, $policyRule->getPolicyRuleAttributes());
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/doc/comparisons.md:
--------------------------------------------------------------------------------
1 | Comparisons
2 | ==========
3 |
4 | A policy rule specifies expected values for each attribute.
5 |
6 | An attribute is associated to a comparison.
7 |
8 | With this comparison, the library will be able to determine if the attribute matches the expected value.
9 |
10 | This is the list of the available comparisons. Feel free to suggest new ones.
11 |
12 | Numeric
13 | -------
14 |
15 | * ### isEqual
16 | * ### greaterThan
17 | * ### lesserThan
18 |
19 | String
20 | ------
21 |
22 | * ### isEqual
23 | * ### isNotEqual
24 |
25 | Date
26 | ----
27 |
28 | * ### isBetween
29 | * ### isMoreRecentThan
30 | * ### IsLessRecentThan
31 |
32 | Array
33 | -----
34 |
35 | * ### isIn
36 | * ### isNotIn
37 | * ### intersect
38 | * ### doNotIntersect
39 | * ### contains
40 |
41 | Boolean
42 | ------
43 |
44 | * ### boolAnd
45 | * ### boolOr
46 | * ### isNull
47 | * ### isNotNull
48 |
49 | Object
50 | -------
51 |
52 | * ### isFieldEqual
53 |
54 | User
55 | -------
56 |
57 | * ### isFieldEqual
58 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
13 |
14 |
15 |
16 | ./tests
17 |
18 |
19 |
20 |
21 |
22 | ./
23 |
24 | ./tests
25 | ./vendor
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "craftcamp/php-abac",
3 | "description": "Library used to implement Attribute-Based Access Control in a PHP application",
4 | "type": "library",
5 | "keywords": ["access-control", "attributes", "security"],
6 | "license": "MIT",
7 | "minimum-stability": "stable",
8 | "authors": [
9 | {
10 | "name": "Axel Venet",
11 | "email": "kern046@gmail.com",
12 | "role": "Developer"
13 | }
14 | ],
15 | "require": {
16 | "php": ">=7.0",
17 | "psr/cache": "~1.0",
18 | "symfony/config": "~3.0|^4.0",
19 | "symfony/yaml" : "~3.0|^4.0"
20 | },
21 | "support": {
22 | "email": "kern046@gmail.com"
23 | },
24 | "autoload" : {
25 | "psr-4": {
26 | "PhpAbac\\": "src/",
27 | "PhpAbac\\Example\\": "example/",
28 | "PhpAbac\\Test\\": "tests/"
29 | }
30 | },
31 | "require-dev": {
32 | "friendsofphp/php-cs-fixer": "^2.12",
33 | "phpunit/phpunit": "^6.5"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2018 Axel Venet
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.
--------------------------------------------------------------------------------
/tests/fixtures/users.php:
--------------------------------------------------------------------------------
1 | setId(1)
8 | ->setName('John Doe')
9 | ->setAge(36)
10 | ->setParentNationality('FR')
11 | ->addVisa($visas[0])
12 | ->addVisa($visas[1])
13 | ->setHasDoneJapd(false)
14 | ->setHasDrivingLicense(true)
15 | ->setCountry('FR'),
16 | (new User())
17 | ->setId(2)
18 | ->setName('Thierry')
19 | ->setAge(24)
20 | ->addVisa($visas[2])
21 | ->setParentNationality('FR')
22 | ->setHasDoneJapd(false)
23 | ->setHasDrivingLicense(false),
24 | (new User())
25 | ->setId(3)
26 | ->setName('Jason')
27 | ->setAge(17)
28 | ->setParentNationality('FR')
29 | ->setHasDoneJapd(true)
30 | ->setHasDrivingLicense(true)
31 | ->setCountry('FR'),
32 | (new User())
33 | ->setId(4)
34 | ->setName('Bouddha')
35 | ->setAge(556)
36 | ->setParentNationality('FR')
37 | ->setHasDoneJapd(true)
38 | ->setHasDrivingLicense(false),
39 | (new User())
40 | ->setId(5)
41 | ->setName('Mickey')
42 | ->setAge(22)
43 | ->setParentNationality('FR')
44 | ->setHasDoneJapd(true)
45 | ->setHasDrivingLicense(false)
46 | ->setCountry('US')
47 | ];
48 |
--------------------------------------------------------------------------------
/tests/Comparison/StringComparisonTest.php:
--------------------------------------------------------------------------------
1 | comparison = new StringComparison($this->getComparisonManagerMock());
17 | }
18 |
19 | public function testIsEqual()
20 | {
21 | $this->assertTrue($this->comparison->isEqual('john-doe', 'john-doe'));
22 | $this->assertFalse($this->comparison->isEqual('john-doe', 'john-DOE'));
23 | }
24 |
25 | public function testIsNotEqual()
26 | {
27 | $this->assertTrue($this->comparison->isNotEqual('john-doe', 'john-DOE'));
28 | $this->assertFalse($this->comparison->isNotEqual('john-doe', 'john-doe'));
29 | }
30 |
31 | public function getComparisonManagerMock()
32 | {
33 | $comparisonManagerMock = $this
34 | ->getMockBuilder(ComparisonManager::class)
35 | ->disableOriginalConstructor()
36 | ->getMock()
37 | ;
38 | return $comparisonManagerMock;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Loader/JsonLoaderTest.php:
--------------------------------------------------------------------------------
1 | setCurrentDir(__DIR__.'/../fixtures/bad');
17 | $loader->import('unexisting_policy_rules.json');
18 | }
19 |
20 | public function testLoaderJsonValidJsonFile()
21 | {
22 | $loader = new JsonLoader(new FileLocator(__DIR__.'/../fixtures'));
23 | $loader->setCurrentDir(__DIR__.'/../fixtures');
24 | $this->assertThat($loader->import('policy_rules.json'), $this->isType('array'));
25 | }
26 |
27 | public function testSupports()
28 | {
29 | $loader = new JsonLoader(new FileLocator(__DIR__.'/../fixtures'));
30 | $loader->setCurrentDir(__DIR__.'/../fixtures');
31 | $this->assertTrue($loader->supports('json.json'));
32 | $this->assertFalse($loader->supports('json.yaml'));
33 | $this->assertFalse($loader->supports('json.xml'));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/Loader/YamlLoaderTest.php:
--------------------------------------------------------------------------------
1 | setCurrentDir(__DIR__.'/../fixtures/bad');
17 | $loader->import('unexisting_policy_rules.yml');
18 | }
19 |
20 | public function testLoaderYamlValidYamlFile()
21 | {
22 | $loader = new YamlLoader(new FileLocator(__DIR__.'/../fixtures'));
23 | $loader->setCurrentDir(__DIR__.'/../fixtures');
24 | $this->assertThat($loader->import('policy_rules.yml'), $this->isType('array'));
25 | }
26 |
27 | public function testSupports()
28 | {
29 | $loader = new YamlLoader(new FileLocator(__DIR__.'/../fixtures'));
30 | $loader->setCurrentDir(__DIR__.'/../fixtures');
31 | $this->assertTrue($loader->supports('yaml.yaml'));
32 | $this->assertFalse($loader->supports('yaml.json'));
33 | $this->assertFalse($loader->supports('yaml.xml'));
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/Model/PolicyRule.php:
--------------------------------------------------------------------------------
1 | **/
10 | protected $policyRuleAttributes;
11 |
12 | public function __construct()
13 | {
14 | $this->policyRuleAttributes = [];
15 | }
16 |
17 | public function setName(string $name): PolicyRule
18 | {
19 | $this->name = $name;
20 |
21 | return $this;
22 | }
23 |
24 | public function getName(): string
25 | {
26 | return $this->name;
27 | }
28 |
29 | public function addPolicyRuleAttribute(PolicyRuleAttribute $pra): PolicyRule
30 | {
31 | if (!in_array($pra, $this->policyRuleAttributes, true)) {
32 | $this->policyRuleAttributes[] = $pra;
33 | }
34 | return $this;
35 | }
36 |
37 | public function removePolicyRuleAttribute(PolicyRuleAttribute $pra): PolicyRule
38 | {
39 | if (($key = array_search($pra, $this->policyRuleAttributes)) !== false) {
40 | unset($this->policyRuleAttributes[$key]);
41 | }
42 |
43 | return $this;
44 | }
45 |
46 | public function getPolicyRuleAttributes(): array
47 | {
48 | return $this->policyRuleAttributes;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/Model/AbstractAttribute.php:
--------------------------------------------------------------------------------
1 | name = $name;
19 |
20 | return $this;
21 | }
22 |
23 | public function getName(): string
24 | {
25 | return $this->name;
26 | }
27 |
28 | public function setType(string $type): AbstractAttribute
29 | {
30 | $this->type = $type;
31 |
32 | return $this;
33 | }
34 |
35 | public function getType(): string
36 | {
37 | return $this->type;
38 | }
39 |
40 | public function setSlug(string $slug): AbstractAttribute
41 | {
42 | $this->slug = $slug;
43 |
44 | return $this;
45 | }
46 |
47 | public function getSlug(): string
48 | {
49 | return $this->slug;
50 | }
51 |
52 | public function setValue($value): AbstractAttribute
53 | {
54 | $this->value = $value;
55 |
56 | return $this;
57 | }
58 |
59 | public function getValue()
60 | {
61 | return $this->value;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/tests/fixtures/policy_rules_with_array.yml:
--------------------------------------------------------------------------------
1 | ---
2 | attributes:
3 | main_user:
4 | class: PhpAbac\Example\User
5 | type: user
6 | fields:
7 | id:
8 | name: ID
9 | age:
10 | name: Age
11 | parentNationality:
12 | name: Nationalité des parents
13 | hasDoneJapd:
14 | name: JAPD
15 | hasDrivingLicense:
16 | name: Permis de conduire
17 | visas:
18 | name: Visas
19 | country:
20 | name: Code ISO du pays
21 | rules:
22 | gunlaw:
23 | -
24 | attributes:
25 | main_user.age:
26 | comparison_type: numeric
27 | comparison: isGreaterThan
28 | value: 18
29 | main_user.country:
30 | comparison_type: string
31 | comparison: isEqual
32 | value: FR
33 | -
34 | attributes:
35 | main_user.age:
36 | comparison_type: numeric
37 | comparison: isGreaterThan
38 | value: 21
39 | main_user.country:
40 | comparison_type: string
41 | comparison: isNotEqual
42 | value: FR
43 |
--------------------------------------------------------------------------------
/doc/caching.md:
--------------------------------------------------------------------------------
1 | Caching
2 | =======
3 |
4 | To cache access control policy rule result, you can use several PSR-6 compliant drivers.
5 |
6 | The default one is ``memory``, which will set the results in RAM.
7 |
8 | The list of implemented drivers are here, do not hesitate to implement your own and make a Pull Request !
9 |
10 | Usage
11 | -------
12 |
13 | Some of the caching options are arguments of the ```enforce``` method.
14 |
15 | New options can be configured in the Abac ```__construct``` method as well, like the filesystem root for your cache files.
16 |
17 | **WARNING:** For now, do not set an ending slash in the cache_folder value.
18 |
19 | ```php
20 | use PhpAbac\AbacFactory;
21 |
22 | $abac = AbacFactory::getAbac(['policy_rules.yml'], null, [], [
23 | 'cache_folder' => __DIR__ . '/cache'
24 | ]);
25 | $abac->enforce('my_rule', $user, $resource, [
26 | 'cache_result' => true,
27 | 'cache_ttl' => 3600,
28 | 'cache_driver' => 'text'
29 | ])
30 | ```
31 |
32 | In the next versions, some of these options will be put in the ``Abac`` constructor, the ``enforce`` method will be allowed to do an override of these values.
33 |
34 | Drivers
35 | -------
36 |
37 | ### Memory
38 |
39 | This is the default one. It will store the results of the ```enforce``` method in RAM.
40 |
41 | ### Text
42 |
43 | This is a basic filesystem caching driver, which will put the results and the expiration date in a .txt file.
44 |
--------------------------------------------------------------------------------
/tests/fixtures/vehicles.php:
--------------------------------------------------------------------------------
1 | setId(1)
8 | ->setOwner($users[0])
9 | ->setBrand('Renault')
10 | ->setModel('Mégane')
11 | ->setLastTechnicalReviewDate(new \DateTime('-1 year'))
12 | ->setManufactureDate(new \DateTime('-3 years'))
13 | ->setOrigin('FR')
14 | ->setEngineType('diesel')
15 | ->setEcoClass('C'),
16 | (new Vehicle())
17 | ->setId(2)
18 | ->setOwner($users[2])
19 | ->setBrand('Fiat')
20 | ->setModel('Stilo')
21 | ->setLastTechnicalReviewDate(new \DateTime('-7 years'))
22 | ->setManufactureDate(new \DateTime('-14 years'))
23 | ->setOrigin('IT')
24 | ->setEngineType('diesel')
25 | ->setEcoClass('C'),
26 | (new Vehicle())
27 | ->setId(3)
28 | ->setOwner($users[0])
29 | ->setBrand('Alpha Roméo')
30 | ->setModel('Mito')
31 | ->setLastTechnicalReviewDate(new \DateTime('-2 years'))
32 | ->setManufactureDate(new \DateTime('-4 years'))
33 | ->setOrigin('FR')
34 | ->setEngineType('gasoline')
35 | ->setEcoClass('D'),
36 | (new Vehicle())
37 | ->setId(4)
38 | ->setOwner($users[3])
39 | ->setBrand('Fiat')
40 | ->setModel('Punto')
41 | ->setLastTechnicalReviewDate(new \DateTime('-1 year'))
42 | ->setManufactureDate(new \DateTime('-6 years'))
43 | ->setOrigin('FR')
44 | ->setEngineType('diesel')
45 | ->setEcoClass('B'),
46 | ];
47 |
--------------------------------------------------------------------------------
/doc/dependency-injection.md:
--------------------------------------------------------------------------------
1 | DependencyInjection
2 | ===================
3 |
4 | In order to allow you to extend the library, a proper dependency injection was set.
5 |
6 | You're able to replace each component of the Abac library.
7 |
8 | The normal way to get the library and enforce your rules is:
9 |
10 | ```php
11 | options = $options;
22 | }
23 |
24 | public function save(CacheItemInterface $item)
25 | {
26 | $this->getItemPool($item->getDriver())->save($item);
27 | }
28 |
29 | public function getItem(string $key, string $driver = null, int $ttl = null): CacheItemInterface
30 | {
31 | $finalDriver = ($driver !== null) ? $driver : $this->defaultDriver;
32 |
33 | $pool = $this->getItemPool($finalDriver);
34 | $item = $pool->getItem($key);
35 |
36 | // In this case, the pool returned a new CacheItem
37 | if ($item->get() === null) {
38 | $item->expiresAfter($ttl);
39 | }
40 | return $item;
41 | }
42 |
43 | public function getItemPool(string $driver): CacheItemPoolInterface
44 | {
45 | if (!isset($this->pools[$driver])) {
46 | $poolClass = 'PhpAbac\\Cache\\Pool\\' . ucfirst($driver) . 'CacheItemPool';
47 | $this->pools[$driver] = new $poolClass($this->options);
48 | }
49 | return $this->pools[$driver];
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/Comparison/DatetimeComparison.php:
--------------------------------------------------------------------------------
1 | = $datetime;
10 | }
11 |
12 | public function isMoreRecentThan(string $format, \DateTime $datetime): bool
13 | {
14 | return $this->getDatetimeFromFormat($format) <= $datetime;
15 | }
16 |
17 | public function isLessRecentThan(string $format, \DateTime $datetime): bool
18 | {
19 | return $this->getDatetimeFromFormat($format) >= $datetime;
20 | }
21 |
22 | public function getDatetimeFromFormat(string $format): \DateTime
23 | {
24 | $formats = [
25 | 'Y' => 31104000,
26 | 'M' => 2592000,
27 | 'D' => 86400,
28 | 'H' => 3600,
29 | 'm' => 60,
30 | 's' => 1,
31 | ];
32 | $operator = $format{0};
33 | $format = substr($format, 1);
34 | $time = 0;
35 |
36 | foreach ($formats as $scale => $seconds) {
37 | $data = explode($scale, $format);
38 |
39 | if (strlen($format) === strlen($data[0])) {
40 | continue;
41 | }
42 | $time += $data[0] * $seconds;
43 | // Remaining format string
44 | $format = $data[1];
45 | }
46 |
47 | return
48 | ($operator === '+')
49 | ? (new \DateTime())->setTimestamp((time() + $time))
50 | : (new \DateTime())->setTimestamp((time() - $time))
51 | ;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/Cache/Item/TextCacheItemTest.php:
--------------------------------------------------------------------------------
1 | item = new TextCacheItem('php_abac.test');
15 | }
16 |
17 | public function testSet()
18 | {
19 | $this->item->set('test');
20 |
21 | $this->assertEquals('test', $this->item->get());
22 | }
23 |
24 | public function testIsHit()
25 | {
26 | $this->assertTrue($this->item->isHit());
27 | }
28 |
29 | public function testIsHitWithMissItem()
30 | {
31 | $this->item->expiresAt((new \DateTime())->setTimestamp(time() - 100));
32 |
33 | $this->assertFalse($this->item->isHit());
34 | }
35 |
36 | public function testGetKey()
37 | {
38 | $this->assertEquals('php_abac.test', $this->item->getKey());
39 | }
40 |
41 | public function testGet()
42 | {
43 | $this->item->set('test');
44 |
45 | $this->assertEquals('test', $this->item->get());
46 | }
47 |
48 | public function testExpiresAt()
49 | {
50 | $time = time();
51 |
52 | $this->item->expiresAt((new \DateTime())->setTimestamp($time));
53 |
54 | $this->assertEquals($time, $this->item->getExpirationDate()->getTimestamp());
55 | }
56 |
57 | public function testExpiresAfter()
58 | {
59 | $time = time() + 1500;
60 |
61 | $this->item->expiresAfter(1500);
62 |
63 | $this->assertEquals($time, $this->item->getExpirationDate()->getTimestamp());
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Comparison/DatetimeComparisonTest.php:
--------------------------------------------------------------------------------
1 | comparison = new DatetimeComparison($this->getComparisonManagerMock());
17 | }
18 |
19 | public function testIsBetween()
20 | {
21 | $start = new \DateTime('2015-08-01');
22 | $end = new \DateTime('2015-08-16');
23 |
24 | $this->assertTrue($this->comparison->isBetween($start, $end, new \DateTime('2015-08-05')));
25 | $this->assertFalse($this->comparison->isBetween($start, $end, new \DateTime('2015-07-18')));
26 | }
27 |
28 | public function testIsMoreRecentThan()
29 | {
30 | $this->assertTrue($this->comparison->isMoreRecentThan('-2Y', new \DateTime()));
31 | $this->assertFalse($this->comparison->isMoreRecentThan('-2Y', new \DateTime('2010-01-02')));
32 | }
33 |
34 | public function testIsLessRecentThan()
35 | {
36 | $this->assertTrue($this->comparison->isLessRecentThan('-2Y', new \DateTime('2010-01-02')));
37 | $this->assertFalse($this->comparison->isLessRecentThan('-2Y', new \DateTime()));
38 | }
39 |
40 | public function getComparisonManagerMock()
41 | {
42 | $comparisonManagerMock = $this
43 | ->getMockBuilder(ComparisonManager::class)
44 | ->disableOriginalConstructor()
45 | ->getMock()
46 | ;
47 | return $comparisonManagerMock;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Comparison/BooleanComparisonTest.php:
--------------------------------------------------------------------------------
1 | comparison = new BooleanComparison($this->getComparisonManagerMock());
17 | }
18 |
19 | public function testBoolAnd()
20 | {
21 | $this->assertTrue($this->comparison->boolAnd(true, true));
22 | $this->assertFalse($this->comparison->boolAnd(true, false));
23 | $this->assertFalse($this->comparison->boolAnd(false, false));
24 | }
25 |
26 | public function testBoolOr()
27 | {
28 | $this->assertTrue($this->comparison->boolOr(true, true));
29 | $this->assertTrue($this->comparison->boolOr(true, false));
30 | $this->assertFalse($this->comparison->boolOr(false, false));
31 | }
32 |
33 | public function testIsNull()
34 | {
35 | $this->assertTrue($this->comparison->isNull(true, null));
36 | $this->assertFalse($this->comparison->isNull(true, true));
37 | }
38 |
39 | public function testIsNotNull()
40 | {
41 | $this->assertTrue($this->comparison->isNotNull(true, true));
42 | $this->assertFalse($this->comparison->isNotNull(true, null));
43 | }
44 |
45 | public function getComparisonManagerMock()
46 | {
47 | $comparisonManagerMock = $this
48 | ->getMockBuilder(ComparisonManager::class)
49 | ->disableOriginalConstructor()
50 | ->getMock()
51 | ;
52 | return $comparisonManagerMock;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/Cache/Item/MemoryCacheItemTest.php:
--------------------------------------------------------------------------------
1 | item = new MemoryCacheItem('php_abac.test');
15 | }
16 |
17 | public function testSet()
18 | {
19 | $this->item->set('test');
20 |
21 | $this->assertEquals('test', $this->item->get());
22 | }
23 |
24 | public function testIsHit()
25 | {
26 | $this->assertTrue($this->item->isHit());
27 | }
28 |
29 | public function testIsHitWithMissItem()
30 | {
31 | $this->item->expiresAt((new \DateTime())->setTimestamp(time() - 100));
32 |
33 | $this->assertFalse($this->item->isHit());
34 | }
35 |
36 | public function testGetKey()
37 | {
38 | $this->assertEquals('php_abac.test', $this->item->getKey());
39 | }
40 |
41 | public function testGet()
42 | {
43 | $this->item->set('test');
44 |
45 | $this->assertEquals('test', $this->item->get());
46 | }
47 |
48 | public function testExpiresAt()
49 | {
50 | $time = time();
51 |
52 | $this->item->expiresAt((new \DateTime())->setTimestamp($time));
53 |
54 | $this->assertEquals($time, $this->item->getExpirationDate()->getTimestamp());
55 | }
56 |
57 | public function testExpiresAfter()
58 | {
59 | $time = time() + 1500;
60 |
61 | $this->item->expiresAfter(1500);
62 |
63 | $this->assertEquals($time, $this->item->getExpirationDate()->getTimestamp());
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Manager/CacheManagerTest.php:
--------------------------------------------------------------------------------
1 | cacheManager = new CacheManager();
20 | }
21 |
22 | public function testSave()
23 | {
24 | $item = $this->cacheManager->getItemPool('memory')->getItem('php_abac.test');
25 |
26 | $item->set('Test Value');
27 |
28 | $this->cacheManager->save($item);
29 |
30 | $savedItem = $this->cacheManager->getItemPool('memory')->getItem('php_abac.test');
31 |
32 | $this->assertEquals('Test Value', $savedItem->get());
33 | }
34 |
35 | public function testGetItem()
36 | {
37 | $item = $this->cacheManager->getItemPool('memory')->getItem('php_abac.test');
38 |
39 | $this->assertInstanceOf(CacheItemInterface::class, $item);
40 | $this->assertEquals('php_abac.test', $item->getKey());
41 | $this->assertNull($item->get());
42 | }
43 |
44 | public function testGetItemPool()
45 | {
46 | $pool = $this->cacheManager->getItemPool('memory');
47 | $item = $pool->getItem('php_abac.test');
48 | $this->cacheManager->save($item);
49 | $items = $pool->getItems(['php_abac.test']);
50 |
51 | $this->assertInstanceOf(CacheItemPoolInterface::class, $pool);
52 | $this->assertCount(1, $items);
53 | $this->assertarrayHasKey('php_abac.test', $items);
54 | $this->assertEquals($item, $items['php_abac.test']);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/Comparison/ArrayComparison.php:
--------------------------------------------------------------------------------
1 | isIn($haystack, $needle);
15 | }
16 |
17 | public function intersect(array $array1, array $array2): bool
18 | {
19 | return count(array_intersect($array1, $array2)) > 0;
20 | }
21 |
22 | public function doNotIntersect(array $array1, array $array2): bool
23 | {
24 | return !$this->intersect($array1, $array2);
25 | }
26 |
27 | public function contains(array $policyRuleAttributes, array $attributes, array $extraData = []): bool
28 | {
29 | foreach ($extraData['attribute']->getValue() as $attribute) {
30 | $result = true;
31 | // For each attribute, we check the whole rules set
32 | foreach ($policyRuleAttributes as $pra) {
33 | $attributeData = $pra->getAttribute();
34 | $attributeData->setValue(
35 | $this->comparisonManager->getAttributeManager()->retrieveAttribute($attributeData, $extraData['user'], $attribute)
36 | );
37 | // If one field is not matched, the whole attribute is rejected
38 | if (!$this->comparisonManager->compare($pra, true)) {
39 | $result = false;
40 | break;
41 | }
42 | }
43 | // If the result is still true at the end of the attribute check, the rule is enforced
44 | if ($result === true) {
45 | return true;
46 | }
47 | }
48 | return false;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/fixtures/policy_rules_with_getter_params.yml:
--------------------------------------------------------------------------------
1 | ---
2 | attributes:
3 | main_user:
4 | class: PhpAbac\Example\User
5 | type: user
6 | fields:
7 | id:
8 | name: ID
9 | age:
10 | name: Age
11 | parentNationality:
12 | name: Nationalité des parents
13 | hasDoneJapd:
14 | name: JAPD
15 | hasDrivingLicense:
16 | name: Permis de conduire
17 | visa:
18 | name: Visa specific
19 | visas:
20 | name: Visas
21 | country:
22 | name: Code ISO du pays
23 | visa:
24 | class: PhpAbac\Example\Visa
25 | type: resource
26 | fields:
27 | country.code:
28 | name: Code Pays
29 | lastRenewal:
30 | name: Dernier renouvellement
31 | country:
32 | class: PhpAbac\Example\Country
33 | type: resource
34 | fields:
35 | name:
36 | name: Nom du pays
37 | code:
38 | name: Code international
39 | rules:
40 | travel-to-foreign-country:
41 | attributes:
42 | main_user.age:
43 | comparison_type: numeric
44 | comparison: isGreaterThan
45 | value: 18
46 | main_user.visa:
47 | comparison_type: array
48 | comparison: contains
49 | getter_params:
50 | visa:
51 | -
52 | param_name: '@country_code'
53 | param_value: country.code
54 | with:
55 | visa.lastRenewal:
56 | comparison_type: datetime
57 | comparison: isMoreRecentThan
58 | value: -1Y
59 |
--------------------------------------------------------------------------------
/example/Visa.php:
--------------------------------------------------------------------------------
1 | id = $id;
23 |
24 | return $this;
25 | }
26 |
27 | /**
28 | * @return int
29 | */
30 | public function getId()
31 | {
32 | return $this->id;
33 | }
34 |
35 | /**
36 | * @param Country $country
37 | * @return \PhpAbac\Example\Visa
38 | */
39 | public function setCountry(Country $country)
40 | {
41 | $this->country = $country;
42 |
43 | return $this;
44 | }
45 |
46 | /**
47 | * @return Country
48 | */
49 | public function getCountry()
50 | {
51 | return $this->country;
52 | }
53 |
54 | /**
55 | * @param \DateTime $createdAt
56 | * @return \PhpAbac\Example\Visa
57 | */
58 | public function setCreatedAt(\DateTime $createdAt)
59 | {
60 | $this->createdAt = $createdAt;
61 |
62 | return $this;
63 | }
64 |
65 | /**
66 | * @return \DateTime
67 | */
68 | public function getCreatedAt()
69 | {
70 | return $this->createdAt;
71 | }
72 |
73 | /**
74 | * @param \DateTime $lastRenewal
75 | * @return \PhpAbac\Example\Visa
76 | */
77 | public function setLastRenewal(\DateTime $lastRenewal)
78 | {
79 | $this->lastRenewal = $lastRenewal;
80 |
81 | return $this;
82 | }
83 |
84 | /**
85 | * @return \DateTime
86 | */
87 | public function getLastRenewal()
88 | {
89 | return $this->lastRenewal;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/tests/Comparison/NumericComparisonTest.php:
--------------------------------------------------------------------------------
1 | comparison = new NumericComparison($this->getComparisonManagerMock());
17 | }
18 |
19 | public function testIsEqual()
20 | {
21 | $this->assertTrue($this->comparison->isEqual(4, 4));
22 | $this->assertFalse($this->comparison->isEqual(4, 5));
23 | }
24 |
25 | public function testIsLesserThan()
26 | {
27 | $this->assertTrue($this->comparison->isLesserThan(21, 18));
28 | $this->assertFalse($this->comparison->isLesserThan(21, 22));
29 | $this->assertFalse($this->comparison->isLesserThan(21, 21));
30 | }
31 |
32 | public function testIsLesserThanOrEqual()
33 | {
34 | $this->assertTrue($this->comparison->isLesserThanOrEqual(18, 18));
35 | $this->assertFalse($this->comparison->isLesserThanOrEqual(21, 22));
36 | }
37 |
38 | public function testIsGreaterThan()
39 | {
40 | $this->assertTrue($this->comparison->isGreaterThan(18, 21));
41 | $this->assertFalse($this->comparison->isGreaterThan(18, 18));
42 | $this->assertFalse($this->comparison->isGreaterThan(18, 14));
43 | }
44 |
45 | public function testIsGreaterThanOrEqual()
46 | {
47 | $this->assertTrue($this->comparison->isGreaterThanOrEqual(18, 18));
48 | $this->assertFalse($this->comparison->isGreaterThanOrEqual(21, 18));
49 | }
50 |
51 | public function getComparisonManagerMock()
52 | {
53 | $comparisonManagerMock = $this
54 | ->getMockBuilder(ComparisonManager::class)
55 | ->disableOriginalConstructor()
56 | ->getMock()
57 | ;
58 | return $comparisonManagerMock;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/Cache/Item/TextCacheItem.php:
--------------------------------------------------------------------------------
1 | key = $key;
25 | $this->expiresAfter($ttl);
26 | }
27 |
28 | public function set($value): TextCacheItem
29 | {
30 | $this->value = $value;
31 |
32 | return $this;
33 | }
34 |
35 | public function isHit(): bool
36 | {
37 | return $this->expiresAt >= new \DateTime();
38 | }
39 |
40 | public function getKey(): string
41 | {
42 | return $this->key;
43 | }
44 |
45 | public function get()
46 | {
47 | if (!$this->isHit()) {
48 | throw new ExpiredCacheException('Cache item is expired');
49 | }
50 | return $this->value;
51 | }
52 |
53 | public function expiresAfter($time): TextCacheItem
54 | {
55 | $lifetime = ($time !== null) ? $time : $this->defaultLifetime;
56 |
57 | $this->expiresAt = (new \DateTime())->setTimestamp(time() + $lifetime);
58 |
59 | return $this;
60 | }
61 |
62 | public function expiresAt($expiration): TextCacheItem
63 | {
64 | $this->expiresAt =
65 | ($expiration === null)
66 | ? (new \DateTime())->setTimestamp(time() + $this->defaultLifetime)
67 | : $expiration
68 | ;
69 | return $this;
70 | }
71 |
72 | public function getExpirationDate(): \DateTime
73 | {
74 | return $this->expiresAt;
75 | }
76 |
77 | public function getDriver(): string
78 | {
79 | return $this->driver;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/Cache/Item/MemoryCacheItem.php:
--------------------------------------------------------------------------------
1 | key = $key;
25 | $this->expiresAfter($ttl);
26 | }
27 |
28 | public function set($value): MemoryCacheItem
29 | {
30 | $this->value = $value;
31 |
32 | return $this;
33 | }
34 |
35 | public function isHit(): bool
36 | {
37 | return $this->expiresAt >= new \DateTime();
38 | }
39 |
40 | public function getKey(): string
41 | {
42 | return $this->key;
43 | }
44 |
45 | public function get()
46 | {
47 | if (!$this->isHit()) {
48 | throw new ExpiredCacheException('Cache item is expired');
49 | }
50 | return $this->value;
51 | }
52 |
53 | public function expiresAfter($time): MemoryCacheItem
54 | {
55 | $lifetime = ($time !== null) ? $time : $this->defaultLifetime;
56 |
57 | $this->expiresAt = (new \DateTime())->setTimestamp(time() + $lifetime);
58 |
59 | return $this;
60 | }
61 |
62 | public function expiresAt($expiration): MemoryCacheItem
63 | {
64 | $this->expiresAt =
65 | ($expiration === null)
66 | ? (new \DateTime())->setTimestamp(time() + $this->defaultLifetime)
67 | : $expiration
68 | ;
69 | return $this;
70 | }
71 |
72 | public function getExpirationDate(): \DateTime
73 | {
74 | return $this->expiresAt;
75 | }
76 |
77 | public function getDriver(): string
78 | {
79 | return $this->driver;
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/tests/fixtures/policy_rules_with_array.json:
--------------------------------------------------------------------------------
1 | {
2 | "attributes": {
3 | "main_user": {
4 | "class": "PhpAbac\\Example\\User",
5 | "type": "user",
6 | "fields": {
7 | "id": {
8 | "name": "ID"
9 | },
10 | "age": {
11 | "name": "Age"
12 | },
13 | "parentNationality": {
14 | "name": "Nationalité des parents"
15 | },
16 | "hasDoneJapd": {
17 | "name": "JAPD"
18 | },
19 | "hasDrivingLicense": {
20 | "name": "Permis de conduire"
21 | },
22 | "visas": {
23 | "name": "Visas"
24 | },
25 | "country": {
26 | "name": "Code ISO du pays"
27 | }
28 | }
29 | }
30 | },
31 | "rules": {
32 | "gunlaw": [
33 | {
34 | "attributes": {
35 | "main_user.age": {
36 | "comparison_type": "numeric",
37 | "comparison": "isGreaterThan",
38 | "value": 18
39 | },
40 | "main_user.country": {
41 | "comparison_type": "string",
42 | "comparison": "isEqual",
43 | "value": "FR"
44 | }
45 | }
46 | },
47 | {
48 | "attributes": {
49 | "main_user.age": {
50 | "comparison_type": "numeric",
51 | "comparison": "isGreaterThan",
52 | "value": 21
53 | },
54 | "main_user.country": {
55 | "comparison_type": "string",
56 | "comparison": "isNotEqual",
57 | "value": "FR"
58 | }
59 | }
60 | }
61 | ]
62 | }
63 | }
--------------------------------------------------------------------------------
/tests/Comparison/UserComparisonTest.php:
--------------------------------------------------------------------------------
1 | comparison = new UserComparison($this->getComparisonManagerMock());
23 | }
24 |
25 | public function testIsFieldEqual()
26 | {
27 | $extraData = [
28 | 'user' =>
29 | (new User())
30 | ->setId(1)
31 | ->setParentNationality('UK')
32 | ];
33 | $this->assertFalse($this->comparison->isFieldEqual('main_user.parentNationality', 'FR', $extraData));
34 | $this->assertTrue($this->comparison->isFieldEqual('main_user.parentNationality', 'UK', $extraData));
35 | }
36 |
37 | public function getComparisonManagerMock()
38 | {
39 | $comparisonManagerMock = $this
40 | ->getMockBuilder(ComparisonManager::class)
41 | ->disableOriginalConstructor()
42 | ->getMock()
43 | ;
44 | $comparisonManagerMock
45 | ->expects($this->any())
46 | ->method('getAttributeManager')
47 | ->willReturnCallback([$this, 'getAttributeManagerMock'])
48 | ;
49 | return $comparisonManagerMock;
50 | }
51 |
52 | public function getAttributeManagerMock()
53 | {
54 | $attributeManagerMock = $this
55 | ->getMockBuilder(AttributeManager::class)
56 | ->disableOriginalConstructor()
57 | ->getMock()
58 | ;
59 | $attributeManagerMock
60 | ->expects($this->any())
61 | ->method('getAttribute')
62 | ->willReturn((new Attribute()))
63 | ;
64 | $attributeManagerMock
65 | ->expects($this->any())
66 | ->method('retrieveAttribute')
67 | ->willReturn('UK')
68 | ;
69 | return $attributeManagerMock;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/Comparison/ObjectComparisonTest.php:
--------------------------------------------------------------------------------
1 | comparison = new ObjectComparison($this->getComparisonManagerMock());
25 | }
26 |
27 | public function testIsFieldEqual()
28 | {
29 | $extraData = [
30 | 'resource' =>
31 | (new Vehicle())
32 | ->setId(1)
33 | ->setOwner((new User())->setId(1))
34 | ];
35 | $this->assertFalse($this->comparison->isFieldEqual('vehicle.owner.id', 2, $extraData));
36 | $this->assertTrue($this->comparison->isFieldEqual('vehicle.owner.id', 1, $extraData));
37 | }
38 |
39 | public function getComparisonManagerMock()
40 | {
41 | $comparisonManagerMock = $this
42 | ->getMockBuilder(ComparisonManager::class)
43 | ->disableOriginalConstructor()
44 | ->getMock()
45 | ;
46 | $comparisonManagerMock
47 | ->expects($this->any())
48 | ->method('getAttributeManager')
49 | ->willReturnCallback([$this, 'getAttributeManagerMock'])
50 | ;
51 | return $comparisonManagerMock;
52 | }
53 |
54 | public function getAttributeManagerMock()
55 | {
56 | $attributeManagerMock = $this
57 | ->getMockBuilder(AttributeManager::class)
58 | ->disableOriginalConstructor()
59 | ->getMock()
60 | ;
61 | $attributeManagerMock
62 | ->expects($this->any())
63 | ->method('getAttribute')
64 | ->willReturn(new Attribute())
65 | ;
66 | $attributeManagerMock
67 | ->expects($this->any())
68 | ->method('retrieveAttribute')
69 | ->willReturn(1)
70 | ;
71 | return $attributeManagerMock;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Model/PolicyRuleAttribute.php:
--------------------------------------------------------------------------------
1 | attribute = $attribute;
23 |
24 | return $this;
25 | }
26 |
27 | public function getAttribute(): AbstractAttribute
28 | {
29 | return $this->attribute;
30 | }
31 |
32 | public function setComparisonType(string $comparisonType): PolicyRuleAttribute
33 | {
34 | $this->comparisonType = $comparisonType;
35 |
36 | return $this;
37 | }
38 |
39 | public function getComparisonType(): string
40 | {
41 | return $this->comparisonType;
42 | }
43 |
44 | public function setComparison(string $comparison): PolicyRuleAttribute
45 | {
46 | $this->comparison = $comparison;
47 |
48 | return $this;
49 | }
50 |
51 | public function getComparison(): string
52 | {
53 | return $this->comparison;
54 | }
55 |
56 | public function setValue($value): PolicyRuleAttribute
57 | {
58 | $this->value = $value;
59 |
60 | return $this;
61 | }
62 |
63 | public function getValue()
64 | {
65 | return $this->value;
66 | }
67 |
68 | public function setExtraData(array $extraData): PolicyRuleAttribute
69 | {
70 | $this->extraData = $extraData;
71 |
72 | return $this;
73 | }
74 |
75 | public function addExtraData(string $key, $value): PolicyRuleAttribute
76 | {
77 | $this->extraData[$key] = $value;
78 |
79 | return $this;
80 | }
81 |
82 | public function removeExtraData(string $key): PolicyRuleAttribute
83 | {
84 | if (isset($this->extraData[$key])) {
85 | unset($this->extraData[$key]);
86 | }
87 | return $this;
88 | }
89 |
90 | public function getExtraData(): array
91 | {
92 | return $this->extraData;
93 | }
94 |
95 | public function setGetterParams(array $value): PolicyRuleAttribute
96 | {
97 | $this->getter_params_a = $value;
98 |
99 | return $this;
100 | }
101 |
102 | public function getGetterParams(): array
103 | {
104 | return $this->getter_params_a;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/AbacFactory.php:
--------------------------------------------------------------------------------
1 | initLoaders($configDir);
30 | $this->parseFiles($configurationFiles);
31 | }
32 |
33 | protected function initLoaders(string $configDir = null)
34 | {
35 | $locator = new FileLocator($configDir);
36 | foreach (self::LOADERS as $loaderClass) {
37 | $loader = new $loaderClass($locator);
38 | $loader->setCurrentDir($configDir);
39 | $this->loaders[] = $loader;
40 | }
41 | }
42 |
43 | protected function parseFiles(array $configurationFiles)
44 | {
45 | foreach ($configurationFiles as $configurationFile) {
46 | $config = $this->getLoader($configurationFile)->import($configurationFile, pathinfo($configurationFile, PATHINFO_EXTENSION));
47 |
48 | if (in_array($configurationFile, $this->loadedFiles)) {
49 | continue;
50 | }
51 |
52 | $this->loadedFiles[] = $configurationFile;
53 |
54 | if (isset($config['@import'])) {
55 | $this->parseFiles($config['@import']);
56 | unset($config['@import']);
57 | }
58 |
59 | if (isset($config['attributes'])) {
60 | $this->attributes = array_merge($this->attributes, $config['attributes']);
61 | }
62 | if (isset($config['rules'])) {
63 | $this->rules = array_merge($this->rules, $config['rules']);
64 | }
65 | }
66 | }
67 |
68 | protected function getLoader(string $configurationFile): LoaderInterface
69 | {
70 | foreach ($this->loaders as $abacLoader) {
71 | if ($abacLoader->supports($configurationFile)) {
72 | return $abacLoader;
73 | }
74 | }
75 | throw new \Exception('Loader not found for the file ' . $configurationFile);
76 | }
77 |
78 | public function getAttributes(): array
79 | {
80 | return $this->attributes;
81 | }
82 |
83 | public function getRules(): array
84 | {
85 | return $this->rules;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Cache/Pool/MemoryCacheItemPool.php:
--------------------------------------------------------------------------------
1 | items[$key])) {
22 | unset($this->items[$key]);
23 | }
24 | if (isset($this->deferredItems[$key])) {
25 | unset($this->deferredItems[$key]);
26 | }
27 | return true;
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | public function deleteItems(array $keys)
34 | {
35 | foreach ($keys as $key) {
36 | if (!$this->deleteItem($key)) {
37 | return false;
38 | }
39 | }
40 | return true;
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function save(CacheItemInterface $item)
47 | {
48 | $this->items[$item->getKey()] = $item;
49 |
50 | return true;
51 | }
52 |
53 | /**
54 | * {@inheritdoc}
55 | */
56 | public function saveDeferred(CacheItemInterface $item)
57 | {
58 | $this->deferredItems[$item->getKey()] = $item;
59 |
60 | return true;
61 | }
62 |
63 | /**
64 | * {@inheritdoc}
65 | */
66 | public function commit()
67 | {
68 | foreach ($this->deferredItems as $key => $item) {
69 | $this->items[$key] = $item;
70 | unset($this->deferredItems[$key]);
71 | }
72 | return true;
73 | }
74 |
75 | /**
76 | * {@inheritdoc}
77 | */
78 | public function hasItem($key)
79 | {
80 | return isset($this->items[$key]);
81 | }
82 |
83 | /**
84 | * {@inheritdoc}
85 | */
86 | public function getItem($key)
87 | {
88 | if (!$this->hasItem($key)) {
89 | return new MemoryCacheItem($key);
90 | }
91 | return $this->items[$key];
92 | }
93 |
94 | /**
95 | * {@inheritdoc}
96 | */
97 | public function getItems(array $keys = array())
98 | {
99 | $items = [];
100 | foreach ($keys as $key) {
101 | if ($this->hasItem($key)) {
102 | $items[$key] = $this->getItem($key);
103 | }
104 | }
105 | return $items;
106 | }
107 |
108 | /**
109 | * {@inheritdoc}
110 | */
111 | public function clear()
112 | {
113 | $this->items = [];
114 | $this->deferredItems = [];
115 | return true;
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/example.php:
--------------------------------------------------------------------------------
1 | enforce('nationality-access', $users[3], null, [
17 | 'cache_result' => true,
18 | 'cache_lifetime' => 100,
19 | 'cache_driver' => 'memory'
20 | ]);
21 |
22 | if ($user1Nationality === true) {
23 | echo("GRANTED : The user 1 is able to be nationalized\n");
24 | } else {
25 | echo("FAIL : The system didn't grant access\n");
26 | }
27 |
28 | $user2Nationality = $abac->enforce('nationality-access', $users[0]);
29 | if ($user2Nationality !== true) {
30 | echo("DENIED : The user 2 is not able to be nationalized because he hasn't done his JAPD\n");
31 | } else {
32 | echo("FAIL : The system didn't deny access\n");
33 | }
34 |
35 | $user1Vehicle = $abac->enforce('vehicle-homologation', $users[0], $vehicles[0], [
36 | 'dynamic_attributes' => ['proprietaire' => 1]
37 | ]);
38 | if ($user1Vehicle === true) {
39 | echo("GRANTED : The vehicle 1 is able to be approved for the user 1\n");
40 | } else {
41 | echo("FAIL : The system didn't grant access\n");
42 | }
43 | $user3Vehicle = $abac->enforce('vehicle-homologation', $users[2], $vehicles[1], [
44 | 'dynamic_attributes' => ['proprietaire' => 3]
45 | ]);
46 | if ($user3Vehicle !== true) {
47 | echo("DENIED : The vehicle 2 is not approved for the user 3 because its last technical review is too old\n");
48 | } else {
49 | echo("FAIL : The system didn't deny access\n");
50 | }
51 | $user4Vehicle = $abac->enforce('vehicle-homologation', $users[3], $vehicles[3], [
52 | 'dynamic_attributes' => ['proprietaire' => 4]
53 | ]);
54 | if ($user4Vehicle !== true) {
55 | echo("DENIED : The vehicle 4 is not able to be approved for the user 4 because he has no driving license\n");
56 | } else {
57 | echo("FAIL : The system didn't deny access\n");
58 | }
59 | $user5Vehicle = $abac->enforce('vehicle-homologation', $users[3], $vehicles[3], [
60 | 'dynamic_attributes' => ['proprietaire' => 1]
61 | ]);
62 | if ($user5Vehicle !== true) {
63 | echo("DENIED : The vehicle 4 is not able to be approved for the user 2 because he doesn't own the vehicle\n");
64 | } else {
65 | echo("FAIL : The system didn't deny access\n");
66 | }
67 | $userTravel1 = $abac->enforce('travel-to-foreign-country', $users[0], null, [
68 | 'dynamic_attributes' => [
69 | 'code-pays' => 'US'
70 | ]
71 | ]);
72 | if ($userTravel1 !== true) {
73 | echo("DENIED: The user 1 is not allowed to travel to the USA because he doesn't have an US visa\n");
74 | } else {
75 | echo('FAIL: The system didn\'t deny access');
76 | }
77 | $userTravel2 = $abac->enforce('travel-to-foreign-country', $users[1], null, [
78 | 'dynamic_attributes' => [
79 | 'code-pays' => 'US'
80 | ]
81 | ]);
82 | if ($userTravel2 === true) {
83 | echo("GRANTED: The user 2 is allowed to travel to the USA\n");
84 | } else {
85 | echo('FAIL: The system didn\'t grant access');
86 | }
87 |
--------------------------------------------------------------------------------
/src/Manager/PolicyRuleManager.php:
--------------------------------------------------------------------------------
1 | attributeManager = $attributeManager;
22 | $this->rules = $configuration->getRules();
23 | }
24 |
25 | public function getRule(string $ruleName, $user, $resource): array
26 | {
27 | if (!isset($this->rules[$ruleName])) {
28 | throw new \InvalidArgumentException('The given rule "' . $ruleName . '" is not configured');
29 | }
30 |
31 | // TODO check if this is really useful
32 | // force to treat always arrays
33 | if (array_key_exists('attributes', $this->rules[$ruleName])) {
34 | $this->rules[$ruleName] = [$this->rules[$ruleName]];
35 | }
36 |
37 | $rules = [];
38 | foreach ($this->rules[$ruleName] as $rule) {
39 | $policyRule = (new PolicyRule())->setName($ruleName);
40 | // For each policy rule attribute, the data is formatted
41 | foreach ($this->processRuleAttributes($rule['attributes'], $user, $resource) as $pra) {
42 | $policyRule->addPolicyRuleAttribute($pra);
43 | }
44 | $rules[] = $policyRule;
45 | }
46 | return $rules;
47 | }
48 |
49 | /**
50 | * This method is meant to convert attribute data from array to formatted policy rule attribute
51 | */
52 | public function processRuleAttributes(array $attributes, $user, $resource)
53 | {
54 | foreach ($attributes as $attributeName => $attribute) {
55 | $pra = (new PolicyRuleAttribute())
56 | ->setAttribute($this->attributeManager->getAttribute($attributeName))
57 | ->setComparison($attribute['comparison'])
58 | ->setComparisonType($attribute['comparison_type'])
59 | ->setValue((isset($attribute['value'])) ? $attribute['value'] : null)
60 | ->setGetterParams(isset($attribute[ 'getter_params' ]) ? $attribute[ 'getter_params' ] : []);
61 | $this->processRuleAttributeComparisonType($pra, $user, $resource);
62 | // In the case the user configured more keys than the basic ones
63 | // it will be stored as extra data
64 | foreach ($attribute as $key => $value) {
65 | if (!in_array($key, ['comparison', 'comparison_type', 'value','getter_params'])) {
66 | $pra->addExtraData($key, $value);
67 | }
68 | }
69 | // This generator avoid useless memory consumption instead of returning a whole array
70 | yield $pra;
71 | }
72 | }
73 |
74 | /**
75 | * This method is meant to set appropriated extra data to $pra depending on comparison type
76 | */
77 | protected function processRuleAttributeComparisonType(PolicyRuleAttribute $pra, $user, $resource)
78 | {
79 | switch ($pra->getComparisonType()) {
80 | case 'user':
81 | $pra->setExtraData(['user' => $user]);
82 | break;
83 | case 'object':
84 | $pra->setExtraData(['resource' => $resource]);
85 | break;
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Cache/Pool/TextCacheItemPool.php:
--------------------------------------------------------------------------------
1 | configure($options);
23 | }
24 |
25 | /**
26 | * @param array $options
27 | */
28 | protected function configure($options)
29 | {
30 | $this->cacheFolder =
31 | (isset($options['cache_folder']))
32 | ? "{$options['cache_folder']}/text"
33 | : __DIR__ . '/../../../data/cache/text'
34 | ;
35 | }
36 |
37 | /**
38 | * {@inheritdoc}
39 | */
40 | public function deleteItem($key)
41 | {
42 | if (is_file("{$this->cacheFolder}/$key.txt")) {
43 | unlink("{$this->cacheFolder}/$key.txt");
44 | }
45 | if (isset($this->deferredItems[$key])) {
46 | unset($this->deferredItems[$key]);
47 | }
48 | return true;
49 | }
50 |
51 | /**
52 | * {@inheritdoc}
53 | */
54 | public function deleteItems(array $keys)
55 | {
56 | foreach ($keys as $key) {
57 | if (!$this->deleteItem($key)) {
58 | return false;
59 | }
60 | }
61 | return true;
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function save(CacheItemInterface $item)
68 | {
69 | $data = "{$item->get()};{$item->getExpirationDate()->format('Y-m-d H:i:s')}";
70 |
71 | file_put_contents("{$this->cacheFolder}/{$item->getKey()}.txt", $data);
72 |
73 | return true;
74 | }
75 |
76 | /**
77 | * {@inheritdoc}
78 | */
79 | public function saveDeferred(CacheItemInterface $item)
80 | {
81 | $this->deferredItems[$item->getKey()] = $item;
82 |
83 | return true;
84 | }
85 |
86 | /**
87 | * {@inheritdoc}
88 | */
89 | public function commit()
90 | {
91 | foreach ($this->deferredItems as $key => $item) {
92 | $this->save($item);
93 | unset($this->deferredItems[$key]);
94 | }
95 | return true;
96 | }
97 |
98 | /**
99 | * {@inheritdoc}
100 | */
101 | public function hasItem($key)
102 | {
103 | return is_file("{$this->cacheFolder}/{$key}.txt");
104 | }
105 |
106 | /**
107 | * {@inheritdoc}
108 | */
109 | public function getItem($key)
110 | {
111 | $item = new TextCacheItem($key);
112 | if (!$this->hasItem($key)) {
113 | return $item;
114 | }
115 | $data = explode(';', file_get_contents("{$this->cacheFolder}/{$key}.txt"));
116 | return $item
117 | ->set($data[0])
118 | ->expiresAt((new \DateTime($data[1])))
119 | ;
120 | }
121 |
122 | /**
123 | * {@inheritdoc}
124 | */
125 | public function getItems(array $keys = array())
126 | {
127 | $items = [];
128 | foreach ($keys as $key) {
129 | if ($this->hasItem($key)) {
130 | $items[$key] = $this->getItem($key);
131 | }
132 | }
133 | return $items;
134 | }
135 |
136 | /**
137 | * {@inheritdoc}
138 | */
139 | public function clear()
140 | {
141 | $items = glob("{$this->cacheFolder}/*.txt"); // get all file names
142 | foreach ($items as $item) { // iterate files
143 | if (is_file($item)) {
144 | unlink($item);
145 | } // delete file
146 | }
147 | $this->deferredItems = [];
148 | return true;
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/tests/Manager/ComparisonManagerTest.php:
--------------------------------------------------------------------------------
1 | manager = new ComparisonManager($this->getAttributeManagerMock());
22 | }
23 |
24 | public function testCompare()
25 | {
26 | $this->assertTrue($this->manager->compare(
27 | (new PolicyRuleAttribute())
28 | ->setAttribute(
29 | (new Attribute())
30 | ->setName('Test')
31 | ->setSlug('test')
32 | ->setValue('Value')
33 | )
34 | ->setComparisonType('string')
35 | ->setComparison('isEqual')
36 | ->setValue('Value')
37 | ));
38 | $this->assertEquals([], $this->manager->getResult());
39 | }
40 |
41 | public function testCompareWithInvalidAttribute()
42 | {
43 | $this->assertFalse($this->manager->compare(
44 | (new PolicyRuleAttribute())
45 | ->setAttribute(
46 | (new Attribute())
47 | ->setName('Test')
48 | ->setSlug('test')
49 | ->setValue('Wrong value')
50 | )
51 | ->setComparisonType('string')
52 | ->setComparison('isEqual')
53 | ->setValue('Value')
54 | ));
55 | $this->assertEquals(['test'], $this->manager->getResult());
56 | }
57 |
58 | /**
59 | * @expectedException \InvalidArgumentException
60 | * @expectedExceptionMessage The requested comparison class does not exist
61 | */
62 | public function testCompareWithInvalidType()
63 | {
64 | $this->manager->compare(
65 | (new PolicyRuleAttribute())
66 | ->setAttribute(
67 | (new Attribute())
68 | ->setName('Test')
69 | ->setSlug('test')
70 | ->setValue('Value')
71 | )
72 | ->setComparisonType('unknownType')
73 | ->setComparison('isEqual')
74 | ->setValue('Value')
75 | );
76 | }
77 |
78 | /**
79 | * @expectedException \InvalidArgumentException
80 | * @expectedExceptionMessage The requested comparison method does not exist
81 | */
82 | public function testCompareWithInvalidMethod()
83 | {
84 | $this->manager->compare(
85 | (new PolicyRuleAttribute())
86 | ->setAttribute(
87 | (new Attribute())
88 | ->setName('Test')
89 | ->setSlug('test')
90 | ->setValue('Value')
91 | )
92 | ->setComparisonType('string')
93 | ->setComparison('equal')
94 | ->setValue('Value')
95 | );
96 | }
97 |
98 | public function testDynamicAttributes()
99 | {
100 | $this->manager->setDynamicAttributes(['owner-id' => 13]);
101 | $this->assertEquals(13, $this->manager->getDynamicAttribute('owner-id'));
102 | }
103 |
104 | /**
105 | * @expectedException \InvalidArgumentException
106 | * @expectedExceptionMessage The dynamic value for attribute owner-id was not given
107 | */
108 | public function testGetMissingDynamicAttribute()
109 | {
110 | $this->manager->getDynamicAttribute('owner-id');
111 | }
112 |
113 | public function getAttributeManagerMock()
114 | {
115 | $managerMock = $this
116 | ->getMockBuilder(AttributeManager::class)
117 | ->disableOriginalConstructor()
118 | ->getMock()
119 | ;
120 | return $managerMock;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 | All notable changes to this project will be documented in this file.
3 | This project adheres to [Semantic Versioning](http://semver.org/).
4 |
5 | ## [3.0.0] - 2018-10-13
6 | ### Added
7 | - Abac class dependency injection
8 | - Abac factory
9 | - Support of PHP 7.x
10 |
11 | ### Changed
12 | - Repository ownership
13 |
14 | ### Removed
15 | - Support of PHP 5.x
16 |
17 | ## [2.1.2] - 2018-01-31
18 | ### Added
19 | - Support of Symfony 4 components
20 |
21 | ### Fixed
22 | - Code style (PSR-2 compliant)
23 |
24 | ## [2.1.1] - 2016-12-12
25 | ### Added
26 | - JSON configuration file compatibility
27 | - Using Symfony Locator for configuration file
28 | - Multiple Set of Rules for an unique Rule name
29 | - Allow simple configuration by include file ( via @import attribute )
30 | - Allow to specify getter prefix (default = get ) and method to apply on getter name method instead of ucfist(default)
31 | - Allow addional parameters for getters in config file.
32 |
33 | ## [2.1.0] - 2016-10-09
34 | ### Added
35 | - Text cache driver
36 | - Cache folder configuration for cache files
37 | - Resource field comparison refering to user attribute
38 | - User field comparison refering to resource attribute
39 |
40 | ### Changed
41 | - Auxiliary comparisons made by a comparison class does not generate rejected attributes anymore
42 |
43 | ## [2.0.3] - 2016-06-04
44 | ### Changed
45 | - The comparison manager handles dynamic attributes instead of ABAC class
46 | - The comparison manager handles rejected attributes and result instead of ABAC class
47 |
48 | ### Fixed
49 | - Chained attributes can return null in case of unset object in the chain
50 | - Dynamic attributes for contained attributes
51 |
52 | ## [2.0.2] - 2016-06-03
53 | ### Added
54 | - Null comparison
55 | - Not null comparison
56 | - Chained attributes example and documentation
57 |
58 | ### Fixed
59 | - Code example in documentation
60 |
61 | ## [2.0.1] - 2016-06-02
62 | ### Added
63 | - Containing comparison for arrays
64 | - Extra data for policy rule attributes
65 |
66 | ## [2.0.0] - 2016-05-26
67 | ### Added
68 | - Configuration manager
69 | - YAML Loader for configuration files
70 | - Multiple configuration files loading
71 | - Example classes for example script
72 |
73 | ### Changed
74 | - Rules and attributes are now defined with configuration file instead of database
75 | - The enforce method accepts objects instead of numeric IDs
76 | - Attributes are now accessed from the objects instead of the database
77 |
78 | ### Removed
79 | - Rules and attributes creation by manager
80 |
81 | ## [1.2.0] - 2016-04-20
82 | ### Added
83 | - PSR-6 compliant cache implementation
84 | - Memory cache driver
85 |
86 | ### Changed
87 | - Dynamic attributes are now an enforce method option
88 |
89 | ### Fixed
90 | - Example script database connection
91 | - Support lowercase for comparison type values
92 |
93 | ## [1.1] - 2015-11-17
94 | ### Added
95 | - Scrutinizer CI
96 | - Travis CI
97 | - SQLite for unit tests
98 |
99 | ### Changed
100 | - Perform PHP-CS-Fixer to apply PSR-2
101 |
102 | ### Fixed
103 | - PHP 5.4 support
104 |
105 | ## [1.0] - 2015-11-16
106 | ### Added
107 | * POC file example.php
108 | * Environment Attributes
109 | * Dynamic Attributes
110 |
111 | ### Changed
112 | * enforce() method to accept dynamic and environment attributes
113 | * Tables structure (optimized with foreign keys)
114 |
115 | ### Fixed
116 | * Policy Rule creation
117 | * Attribute creation
118 |
119 | ## [0.3] - 2015-08-25
120 | ### Added
121 | * Comparison classes
122 | * enforce() method to take access-control decisions
123 |
124 | ### Changed
125 | * Attributes model to implement comparison
126 |
127 | ### Removed
128 | * Abac resetSchema method (replaced by fixtures)
129 |
130 | ## [0.2] - 2015-08-05
131 | ### Added
132 | * Policy Rule creation
133 | * Policy Rules manager
134 | * Policy Rules repository
135 | * Policy Rules model
136 | * Attributes manager
137 | * Attributes repository
138 | * Attributes model
139 | * SQL schema dump
140 |
--------------------------------------------------------------------------------
/example/Vehicle.php:
--------------------------------------------------------------------------------
1 | id = $id;
33 |
34 | return $this;
35 | }
36 |
37 | /**
38 | * @return int
39 | */
40 | public function getId()
41 | {
42 | return $this->id;
43 | }
44 |
45 | /**
46 | * @param \PhpAbac\Example\User $owner
47 | * @return \PhpAbac\Example\Vehicle
48 | */
49 | public function setOwner(User $owner)
50 | {
51 | $this->owner = $owner;
52 |
53 | return $this;
54 | }
55 |
56 | /**
57 | * @return \PhpAbac\Example\User
58 | */
59 | public function getOwner()
60 | {
61 | return $this->owner;
62 | }
63 |
64 | /**
65 | * @param string $brand
66 | * @return \PhpAbac\Example\Vehicle
67 | */
68 | public function setBrand($brand)
69 | {
70 | $this->brand = $brand;
71 |
72 | return $this;
73 | }
74 |
75 | /**
76 | * @return string
77 | */
78 | public function getBrand()
79 | {
80 | return $this->brand;
81 | }
82 |
83 | /**
84 | *
85 | * @param string $model
86 | * @return \PhpAbac\Example\Vehicle
87 | */
88 | public function setModel($model)
89 | {
90 | $this->model = $model;
91 |
92 | return $this;
93 | }
94 |
95 | /**
96 | * @return string
97 | */
98 | public function getModel()
99 | {
100 | return $this->model;
101 | }
102 |
103 | /**
104 | * @param \DateTime $lastTechnicalReviewDate
105 | * @return \PhpAbac\Example\Vehicle
106 | */
107 | public function setLastTechnicalReviewDate(\DateTime $lastTechnicalReviewDate)
108 | {
109 | $this->lastTechnicalReviewDate = $lastTechnicalReviewDate;
110 |
111 | return $this;
112 | }
113 |
114 | /**
115 | * @return \DateTime
116 | */
117 | public function getLastTechnicalReviewDate()
118 | {
119 | return $this->lastTechnicalReviewDate;
120 | }
121 |
122 | /**
123 | * @param \DateTime $manufactureDate
124 | * @return \PhpAbac\Example\Vehicle
125 | */
126 | public function setManufactureDate(\DateTime $manufactureDate)
127 | {
128 | $this->manufactureDate = $manufactureDate;
129 |
130 | return $this;
131 | }
132 |
133 | public function getManufactureDate()
134 | {
135 | return $this->manufactureDate;
136 | }
137 |
138 | public function setOrigin($origin)
139 | {
140 | $this->origin = $origin;
141 |
142 | return $this;
143 | }
144 |
145 | public function getOrigin()
146 | {
147 | return $this->origin;
148 | }
149 |
150 | public function setEngineType($engineType)
151 | {
152 | $this->engineType = $engineType;
153 |
154 | return $this;
155 | }
156 |
157 | public function getEngineType()
158 | {
159 | return $this->engineType;
160 | }
161 |
162 | public function setEcoClass($ecoClass)
163 | {
164 | $this->ecoClass = $ecoClass;
165 |
166 | return $this;
167 | }
168 |
169 | public function getEcoClass()
170 | {
171 | return $this->ecoClass;
172 | }
173 | }
174 |
--------------------------------------------------------------------------------
/tests/fixtures/policy_rules.yml:
--------------------------------------------------------------------------------
1 | ---
2 | attributes:
3 | main_user:
4 | class: PhpAbac\Example\User
5 | type: user
6 | fields:
7 | id:
8 | name: ID
9 | age:
10 | name: Age
11 | parentNationality:
12 | name: Nationalité des parents
13 | hasDoneJapd:
14 | name: JAPD
15 | hasDrivingLicense:
16 | name: Permis de conduire
17 | visas:
18 | name: Visas
19 |
20 | vehicle:
21 | class: PhpAbac\Example\Vehicle
22 | type: resource
23 | fields:
24 | origin:
25 | name: Origine
26 | owner.id:
27 | name: Propriétaire
28 | manufactureDate:
29 | name: Date de sortie d'usine
30 | lastTechnicalReviewDate:
31 | name: Dernière révision technique
32 |
33 | country:
34 | class: PhpAbac\Example\Country
35 | type: resource
36 | fields:
37 | name:
38 | name: Nom du pays
39 | code:
40 | name: Code international
41 |
42 | visa:
43 | class: PhpAbac\Example\Visa
44 | type: resource
45 | fields:
46 | country.code:
47 | name: Code Pays
48 | lastRenewal:
49 | name: Dernier renouvellement
50 |
51 | environment:
52 | service_state:
53 | name: Statut du service
54 | variable_name: SERVICE_STATE
55 |
56 | rules:
57 | nationality-access:
58 | attributes:
59 | main_user.age:
60 | comparison_type: numeric
61 | comparison: isGreaterThan
62 | value: 18
63 | main_user.parentNationality:
64 | comparison_type: string
65 | comparison: isEqual
66 | value: FR
67 | main_user.hasDoneJapd:
68 | comparison_type: boolean
69 | comparison: boolAnd
70 | value: true
71 | vehicle-homologation:
72 | attributes:
73 | main_user.hasDrivingLicense:
74 | comparison_type: boolean
75 | comparison: boolAnd
76 | value: true
77 | vehicle.lastTechnicalReviewDate:
78 | comparison_type: datetime
79 | comparison: isMoreRecentThan
80 | value: -2Y
81 | vehicle.manufactureDate:
82 | comparison_type: datetime
83 | comparison: isMoreRecentThan
84 | value: -25Y
85 | vehicle.owner.id:
86 | comparison_type: user
87 | comparison: isFieldEqual
88 | value: main_user.id
89 | vehicle.origin:
90 | comparison_type: array
91 | comparison: isIn
92 | value: ["FR", "DE", "IT", "L", "GB", "P", "ES", "NL", "B"]
93 | environment.service_state:
94 | comparison_type: string
95 | comparison: isEqual
96 | value: OPEN
97 | gunlaw:
98 | attributes:
99 | main_user.age:
100 | comparison_type: numeric
101 | comparison: isGreaterThan
102 | value: 21
103 | travel-to-foreign-country:
104 | attributes:
105 | main_user.age:
106 | comparison_type: numeric
107 | comparison: isGreaterThan
108 | value: 18
109 | main_user.visas:
110 | comparison_type: array
111 | comparison: contains
112 | with:
113 | visa.country.code:
114 | comparison_type: string
115 | comparison: isEqual
116 | value: dynamic
117 | visa.lastRenewal:
118 | comparison_type: datetime
119 | comparison: isMoreRecentThan
120 | value: -1Y
121 |
--------------------------------------------------------------------------------
/tests/Cache/Pool/TextCacheItemPoolTest.php:
--------------------------------------------------------------------------------
1 | pool = new TextCacheItemPool([
15 | 'cache_folder' => __DIR__ . '/../../../data/cache/test'
16 | ]);
17 | }
18 |
19 | public function tearDown()
20 | {
21 | $this->pool->clear();
22 | }
23 |
24 | public function testGetItem()
25 | {
26 | $this->pool->save((new TextCacheItem('php_abac.test'))->set('test'));
27 |
28 | $item = $this->pool->getItem('php_abac.test');
29 |
30 | $this->assertInstanceOf(TextCacheItem::class, $item);
31 | $this->assertEquals('test', $item->get());
32 | }
33 |
34 | public function testGetUnknownItem()
35 | {
36 | $item = $this->pool->getItem('php_abac.test');
37 |
38 | $this->assertInstanceOf(TextCacheItem::class, $item);
39 | $this->assertEquals('php_abac.test', $item->getKey());
40 | $this->assertNull($item->get());
41 | }
42 |
43 | public function testGetItems()
44 | {
45 | $this->pool->save((new TextCacheItem('php_abac.test1'))->set('test 1'));
46 | $this->pool->save((new TextCacheItem('php_abac.test2'))->set('test 2'));
47 | $this->pool->save((new TextCacheItem('php_abac.test3'))->set('test 3'));
48 |
49 | $items = $this->pool->getItems([
50 | 'php_abac.test2',
51 | 'php_abac.test3'
52 | ]);
53 | $this->assertCount(2, $items);
54 | $this->assertArrayHasKey('php_abac.test2', $items);
55 | $this->assertInstanceOf(TextCacheItem::class, $items['php_abac.test2']);
56 | $this->assertEquals('test 2', $items['php_abac.test2']->get());
57 | }
58 |
59 | public function testHasItem()
60 | {
61 | $this->pool->save(new TextCacheItem('php_abac.test'));
62 |
63 | $this->assertFalse($this->pool->hasItem('php_abac.unknown_value'));
64 | $this->assertTrue($this->pool->hasItem('php_abac.test'));
65 | }
66 |
67 | public function testSave()
68 | {
69 | $this->pool->save((new TextCacheItem('php_abac.test'))->set('test'));
70 |
71 | $item = $this->pool->getItem('php_abac.test');
72 |
73 | $this->assertInstanceOf(TextCacheItem::class, $item);
74 | $this->assertEquals('test', $item->get());
75 | }
76 |
77 | public function testSaveDeferred()
78 | {
79 | $this->pool->saveDeferred(new TextCacheItem('php_abac.test'));
80 |
81 | $this->assertFalse($this->pool->hasItem('php_abac.test'));
82 |
83 | $this->pool->commit();
84 |
85 | $this->assertTrue($this->pool->hasItem('php_abac.test'));
86 | }
87 |
88 | public function testCommit()
89 | {
90 | $key = 'php_abac.test_deferred';
91 | $value = 'Cached value';
92 |
93 | $this->pool->saveDeferred((new TextCacheItem($key))->set($value));
94 |
95 | $this->pool->commit();
96 |
97 | $this->assertTrue($this->pool->hasItem($key));
98 | $this->assertInstanceOf(TextCacheItem::class, $this->pool->getItem($key));
99 | $this->assertEquals($value, $this->pool->getItem($key)->get());
100 | }
101 |
102 | public function testClear()
103 | {
104 | $this->pool->save(new TextCacheItem('php_abac.test'));
105 |
106 | $this->assertTrue($this->pool->clear());
107 | $this->assertFalse($this->pool->hasItem('php_abac.test'));
108 | }
109 |
110 | public function testDeleteItem()
111 | {
112 | $this->pool->save(new TextCacheItem('php_abac.test1'));
113 |
114 | $this->pool->deleteItem('php_abac.test1');
115 |
116 | $this->assertFalse($this->pool->hasItem('php_abac.test1'));
117 | }
118 |
119 | public function testDeleteItems()
120 | {
121 | $this->pool->save((new TextCacheItem('php_abac.test1'))->set('test 1'));
122 | $this->pool->save((new TextCacheItem('php_abac.test2'))->set('test 2'));
123 | $this->pool->save((new TextCacheItem('php_abac.test3'))->set('test 3'));
124 | $this->pool->deleteItems([
125 | 'php_abac.test2',
126 | 'php_abac.test3'
127 | ]);
128 |
129 | $items = $this->pool->getItems([
130 | 'php_abac.test1',
131 | 'php_abac.test2',
132 | 'php_abac.test3'
133 | ]);
134 | $this->assertCount(1, $items);
135 | $this->assertArrayHasKey('php_abac.test1', $items);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/tests/Cache/Pool/MemoryCacheItemPoolTest.php:
--------------------------------------------------------------------------------
1 | pool = new MemoryCacheItemPool();
15 | }
16 |
17 | public function testGetItem()
18 | {
19 | $this->pool->save((new MemoryCacheItem('php_abac.test'))->set('test'));
20 |
21 | $item = $this->pool->getItem('php_abac.test');
22 |
23 | $this->assertInstanceOf(MemoryCacheItem::class, $item);
24 | $this->assertEquals('test', $item->get());
25 | }
26 |
27 | public function testGetUnknownItem()
28 | {
29 | $item = $this->pool->getItem('php_abac.test');
30 |
31 | $this->assertInstanceOf(MemoryCacheItem::class, $item);
32 | $this->assertEquals('php_abac.test', $item->getKey());
33 | $this->assertNull($item->get());
34 | }
35 |
36 | public function testGetItems()
37 | {
38 | $this->pool->save((new MemoryCacheItem('php_abac.test1'))->set('test 1'));
39 | $this->pool->save((new MemoryCacheItem('php_abac.test2'))->set('test 2'));
40 | $this->pool->save((new MemoryCacheItem('php_abac.test3'))->set('test 3'));
41 |
42 | $items = $this->pool->getItems([
43 | 'php_abac.test2',
44 | 'php_abac.test3'
45 | ]);
46 | $this->assertCount(2, $items);
47 | $this->assertArrayHasKey('php_abac.test2', $items);
48 | $this->assertInstanceOf(MemoryCacheItem::class, $items['php_abac.test2']);
49 | $this->assertEquals('test 2', $items['php_abac.test2']->get());
50 | }
51 |
52 | public function testHasItem()
53 | {
54 | $this->pool->save(new MemoryCacheItem('php_abac.test'));
55 |
56 | $this->assertFalse($this->pool->hasItem('php_abac.unknown_value'));
57 | $this->assertTrue($this->pool->hasItem('php_abac.test'));
58 | }
59 |
60 | public function testSave()
61 | {
62 | $this->pool->save((new MemoryCacheItem('php_abac.test'))->set('test'));
63 |
64 | $item = $this->pool->getItem('php_abac.test');
65 |
66 | $this->assertInstanceOf(MemoryCacheItem::class, $item);
67 | $this->assertEquals('test', $item->get());
68 | }
69 |
70 | public function testSaveDeferred()
71 | {
72 | $this->pool->saveDeferred(new MemoryCacheItem('php_abac.test'));
73 |
74 | $this->assertFalse($this->pool->hasItem('php_abac.test'));
75 |
76 | $this->pool->commit();
77 |
78 | $this->assertTrue($this->pool->hasItem('php_abac.test'));
79 | }
80 |
81 | public function testCommit()
82 | {
83 | $key = 'php_abac.test_deferred';
84 | $value = 'Cached value';
85 |
86 | $this->pool->saveDeferred((new MemoryCacheItem($key))->set($value));
87 |
88 | $this->pool->commit();
89 |
90 | $this->assertTrue($this->pool->hasItem($key));
91 | $this->assertInstanceOf(MemoryCacheItem::class, $this->pool->getItem($key));
92 | $this->assertEquals($value, $this->pool->getItem($key)->get());
93 | }
94 |
95 | public function testClear()
96 | {
97 | $this->pool->save(new MemoryCacheItem('php_abac.test'));
98 |
99 | $this->assertTrue($this->pool->clear());
100 | $this->assertFalse($this->pool->hasItem('php_abac.test'));
101 | }
102 |
103 | public function testDeleteItem()
104 | {
105 | $this->pool->save(new MemoryCacheItem('php_abac.test1'));
106 |
107 | $this->pool->deleteItem('php_abac.test1');
108 |
109 | $this->assertFalse($this->pool->hasItem('php_abac.test1'));
110 | }
111 |
112 | public function testDeleteItems()
113 | {
114 | $this->pool->save((new MemoryCacheItem('php_abac.test1'))->set('test 1'));
115 | $this->pool->save((new MemoryCacheItem('php_abac.test2'))->set('test 2'));
116 | $this->pool->save((new MemoryCacheItem('php_abac.test3'))->set('test 3'));
117 | $this->pool->deleteItems([
118 | 'php_abac.test2',
119 | 'php_abac.test3'
120 | ]);
121 |
122 | $items = $this->pool->getItems([
123 | 'php_abac.test1',
124 | 'php_abac.test2',
125 | 'php_abac.test3'
126 | ]);
127 | $this->assertCount(1, $items);
128 | $this->assertArrayHasKey('php_abac.test1', $items);
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/example/User.php:
--------------------------------------------------------------------------------
1 | id = $id;
31 |
32 | return $this;
33 | }
34 |
35 | /**
36 | * @return int
37 | */
38 | public function getId()
39 | {
40 | return $this->id;
41 | }
42 |
43 | /**
44 | * @param string $name
45 | * @return \PhpAbac\Example\User
46 | */
47 | public function setName($name)
48 | {
49 | $this->name = $name;
50 |
51 | return $this;
52 | }
53 |
54 | /**
55 | * @return string
56 | */
57 | public function getName()
58 | {
59 | return $this->name;
60 | }
61 |
62 | /**
63 | * @param int $age
64 | * @return \PhpAbac\Example\User
65 | */
66 | public function setAge($age)
67 | {
68 | $this->age = $age;
69 |
70 | return $this;
71 | }
72 |
73 | /**
74 | * @return int
75 | */
76 | public function getAge()
77 | {
78 | return $this->age;
79 | }
80 |
81 | /**
82 | * @param string $parentNationality
83 | * @return \PhpAbac\Example\User
84 | */
85 | public function setParentNationality($parentNationality)
86 | {
87 | $this->parentNationality = $parentNationality;
88 |
89 | return $this;
90 | }
91 |
92 | /**
93 | * @return bool
94 | */
95 | public function getParentNationality()
96 | {
97 | return $this->parentNationality;
98 | }
99 |
100 | /**
101 | * @param \PhpAbac\Example\Visa $visa
102 | * @return \PhpAbac\Example\User
103 | */
104 | public function addVisa(Visa $visa)
105 | {
106 | $this->visas[$visa->getId()] = $visa;
107 |
108 | return $this;
109 | }
110 |
111 | /**
112 | * @param \PhpAbac\Example\Visa $visa
113 | * @return \PhpAbac\Example\User
114 | */
115 | public function removeVisa(Visa $visa)
116 | {
117 | if (isset($this->visas[$visa->getId()])) {
118 | unset($this->visas[$visa->getId()]);
119 | }
120 | return $this;
121 | }
122 |
123 | /**
124 | * @return array
125 | */
126 | public function getVisas()
127 | {
128 | return $this->visas;
129 | }
130 |
131 | /**
132 | * Return a specific visa
133 | *
134 | * @param Visa $visa
135 | *
136 | * @return mixed|null
137 | */
138 | public function getVisa($country_code)
139 | {
140 | /** @var Visa $visa */
141 | $visas = [];
142 | foreach ($this->visas as $visa) {
143 | if ($visa->getCountry()->getCode() == $country_code) {
144 | $visas[] = $visa;
145 | }
146 | }
147 | return $visas;
148 | }
149 |
150 | /**
151 | * @param bool $hasDoneJapd
152 | * @return \PhpAbac\Example\User
153 | */
154 | public function setHasDoneJapd($hasDoneJapd)
155 | {
156 | $this->hasDoneJapd = $hasDoneJapd;
157 |
158 | return $this;
159 | }
160 |
161 | /**
162 | * @return bool
163 | */
164 | public function getHasDoneJapd()
165 | {
166 | return $this->hasDoneJapd;
167 | }
168 |
169 | /**
170 | * @param bool $hasDrivingLicense
171 | * @return \PhpAbac\Example\User
172 | */
173 | public function setHasDrivingLicense($hasDrivingLicense)
174 | {
175 | $this->hasDrivingLicense = $hasDrivingLicense;
176 |
177 | return $this;
178 | }
179 |
180 | /**
181 | * @return bool
182 | */
183 | public function getHasDrivingLicense()
184 | {
185 | return $this->hasDrivingLicense;
186 | }
187 |
188 |
189 | /**
190 | * Function to set the iso code of the user country
191 | *
192 | * @param $country
193 | */
194 | public function setCountry($country)
195 | {
196 | $this->country = $country;
197 |
198 | return $this;
199 | }
200 |
201 | /**
202 | * @return string Iso code of the user country
203 | */
204 | public function getCountry()
205 | {
206 | return $this->country;
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/tests/AbacTest.php:
--------------------------------------------------------------------------------
1 | basicSet = [
20 | AbacFactory::getAbac([__DIR__ . '/fixtures/policy_rules.yml']),
21 | AbacFactory::getAbac([__DIR__ . '/fixtures/policy_rules.json']),
22 | ];
23 | $this->multipleRulesetSet = [
24 | AbacFactory::getAbac([__DIR__ . '/fixtures/policy_rules_with_array.yml']),
25 | AbacFactory::getAbac([__DIR__ . '/fixtures/policy_rules_with_array.json']),
26 | AbacFactory::getAbac(['policy_rules_with_array.yml'], __DIR__.'/fixtures/'),
27 | AbacFactory::getAbac(['policy_rules_with_array.json'], __DIR__.'/fixtures/'),
28 | ];
29 | $this->getterParamsSet = [
30 | AbacFactory::getAbac(['policy_rules_with_getter_params.yml'], __DIR__.'/fixtures/'),
31 | ];
32 | $this->importSet = [
33 | AbacFactory::getAbac(['policy_rules_with_import.yml'], __DIR__.'/fixtures/'),
34 | ];
35 | }
36 |
37 | public function testEnforce()
38 | {
39 | $countries = include('tests/fixtures/countries.php');
40 | $visas = include('tests/fixtures/visas.php');
41 | $users = include('tests/fixtures/users.php');
42 | $vehicles = include('tests/fixtures/vehicles.php');
43 |
44 | foreach ($this->basicSet as $abac) {
45 | $this->assertTrue($abac->enforce('nationality-access', $users[3]));
46 | $this->assertFalse($abac->enforce('nationality-access', $users[1]));
47 | $this->assertEquals(['japd'], $abac->getErrors());
48 |
49 | // getenv() don't work in CLI scripts without putenv()
50 | putenv('SERVICE_STATE=OPEN');
51 |
52 | $this->assertTrue($abac->enforce('vehicle-homologation', $users[0], $vehicles[0]));
53 | $this->assertFalse($abac->enforce('vehicle-homologation', $users[2], $vehicles[1]));
54 | $this->assertEquals(['derniere-revision-technique'], $abac->getErrors());
55 | $this->assertFalse($abac->enforce('vehicle-homologation', $users[3], $vehicles[3]));
56 | $this->assertEquals(['permis-de-conduire'], $abac->getErrors());
57 | $this->assertFalse($abac->enforce('travel-to-foreign-country', $users[0], null, [
58 | 'dynamic_attributes' => ['code-pays' => 'US']
59 | ]));
60 | $this->assertEquals(['visas'], $abac->getErrors());
61 | $this->assertTrue($abac->enforce('travel-to-foreign-country', $users[1], null, [
62 | 'dynamic_attributes' => ['code-pays' => 'US']
63 | ]));
64 | }
65 | }
66 |
67 |
68 | public function testEnforceWithMultipleRulesets()
69 | {
70 | $countries = include('tests/fixtures/countries.php');
71 | $visas = include('tests/fixtures/visas.php');
72 | $users = include('tests/fixtures/users.php');
73 |
74 | foreach ($this->multipleRulesetSet as $abac) {
75 | // for this test, the attribute in error are the tested attributes of the last rule of the ruleset
76 | $this->assertFalse($abac->enforce('gunlaw', $users[2]));
77 | $this->assertEquals(['age','code-iso-du-pays'], $abac->getErrors());
78 |
79 | $this->assertTrue($abac->enforce('gunlaw', $users[4]));
80 | $this->assertTrue($abac->enforce('gunlaw', $users[0]));
81 | $this->assertTrue($abac->enforce('gunlaw', $users[1]));
82 | }
83 | }
84 |
85 |
86 | public function testEnforceWithGetterParams()
87 | {
88 | $countries = include('tests/fixtures/countries.php');
89 | $visas = include('tests/fixtures/visas.php');
90 | $users = include('tests/fixtures/users.php');
91 |
92 | foreach ($this->getterParamsSet as $abac) {
93 | $this->assertFalse($abac->enforce('travel-to-foreign-country', $users[0], $countries[2]));
94 | $this->assertEquals(['visa-specific'], $abac->getErrors());
95 | $this->assertTrue($abac->enforce('travel-to-foreign-country', $users[1], $countries[2]));
96 | }
97 | }
98 |
99 |
100 | public function testEnforceWithImport()
101 | {
102 | $countries = include('tests/fixtures/countries.php');
103 | $visas = include('tests/fixtures/visas.php');
104 | $users = include('tests/fixtures/users.php');
105 | foreach ($this->importSet as $abac) {
106 | $this->assertFalse($abac->enforce('travel-to-foreign-country', $users[0], $countries[2]));
107 | $this->assertEquals(['visa-specific'], $abac->getErrors());
108 | $this->assertTrue($abac->enforce('travel-to-foreign-country', $users[1], $countries[2]));
109 | }
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/doc/access-control.md:
--------------------------------------------------------------------------------
1 | Access-Control
2 | ==============
3 |
4 | Introduction
5 | ------------
6 |
7 | This library is meant to perform access control with precise logic.
8 |
9 | Using policy rules, we can analyze users, resources and environment attributes to determine if an user is able to perform an action.
10 |
11 | Once our policy rules are defined, we can simply check if a policy rule is enforced in a given case.
12 |
13 | Usage
14 | ---
15 |
16 | ```php
17 | use PhpAbac\AbacFactory;
18 |
19 | $abac = AbacFactory::getAbac(/** ... **/);
20 |
21 | $check = $abac->enforce('medical-reports-access', $user, $report);
22 | ```
23 |
24 | ```$check``` have two possible values :
25 |
26 | * ```true```, meaning that all the policy rules required attributes are matched for the given user and resource.
27 | * An array of slugs, associated to the attributes which did not match.
28 |
29 | Dynamic Attributes
30 | ------------------
31 |
32 | In some cases, an attribute won't be expected to match a static value, but a dynamic value depending on the case.
33 |
34 | In these cases, the library allows you to give an array as fourth argument of the enforce() method.
35 |
36 | This associative array will contain the targetted attribute's slug as key and the dynamic value.
37 |
38 | For example, it can be useful to check the ownership of a resource :
39 |
40 | ```php
41 | use PhpAbac\AbacFactory;
42 |
43 | $abac = AbacFactory::getAbac();
44 |
45 | $check = $abac->enforce('medical-reports-access', $user, $report, [
46 | 'dynamic-attributes' => [
47 | 'report-author' => $user->getId()
48 | ]
49 | ]);
50 | ```
51 |
52 | Be careful, the key of your dynamic attribute is the **slug** of the attribute's name, not its configuration ID.
53 |
54 | To define an attribute as dynamic, we can write the following code in the configuration file :
55 |
56 | ```yaml
57 | attributes:
58 | medical_report:
59 | class: MySuperVeterinary\Model\MedicalReport
60 | type: resource
61 | fields:
62 | author.id:
63 | name: Report Author
64 | rules:
65 | medical-reports-access:
66 | attributes:
67 | medical_report.author.id:
68 | comparison_type: numeric
69 | comparison: isEqual
70 | value: dynamic
71 | ```
72 |
73 | Comparison by reference
74 | -----------------------
75 |
76 | In some cases, you shall want to compare users and resources between themselves.
77 |
78 | This is possible with the proper configuration to check if a resource field is equal to an user's.
79 |
80 | Let's refactor the previous example.
81 |
82 | ```yaml
83 | attributes:
84 | main_user:
85 | class: MySuperVeterinary\Model\User
86 | type: user
87 | fields:
88 | id:
89 | name: User ID
90 | medical_report:
91 | class: MySuperVeterinary\Model\MedicalReport
92 | type: resource
93 | fields:
94 | author.id:
95 | name: Report Author
96 | rules:
97 | medical-reports-access:
98 | attributes:
99 | medical_report.author.id:
100 | comparison_type: user
101 | comparison: isFieldEqual
102 | value: main_user.id
103 | ```
104 |
105 | This way, you can call the ```enforce``` method without dynamic attributes, the ```User``` and the ```MedicalReport``` fields will be compared one another.
106 |
107 | The same can be done with resources :
108 |
109 | ```yaml
110 | attributes:
111 | main_user:
112 | class: MyTravelApp\Model\User
113 | type: user
114 | fields:
115 | id:
116 | name: User ID
117 | nationality:
118 | name: Nationality
119 | country:
120 | class: MyTravelApp\Model\MedicalReport
121 | type: resource
122 | fields:
123 | name:
124 | name: Country name
125 | code:
126 | name: Country code
127 |
128 | rules:
129 | nationality-access:
130 | attributes:
131 | main_user.nationality:
132 | comparison_type: object
133 | comparison: isFieldEqual
134 | value: country.code
135 | ```
136 |
137 | You just have to call :
138 |
139 | ```php
140 | $isAllowed = $abac->enforce('nationality-access', $person, $country);
141 | ```
142 |
143 | Cache
144 | -----------------
145 |
146 | This library implements cache using PSR-6 specification.
147 |
148 | To enable cache for a specific call of the enforce method, add the following options :
149 |
150 | ```php
151 | $check = $abac->enforce('medical-reports-access', $user, $report, [
152 | 'dynamic-attributes' => [
153 | 'report-author' => $user->getId()
154 | ],
155 | 'cache_result' => true, // enable cache
156 | 'cache_ttl' => 60, // Time to live in seconds, default is one hour
157 | 'cache_driver' => 'memory' // Default is memory
158 | ]);
159 | ```
160 |
161 | With this, if you call this method again with the same $user and $report, the previous result will be returned.
162 |
163 | Available cache drivers :
164 |
165 | * ``memory`` : This cache is stored in the library RAM. It will be erased after the script execution.
166 |
--------------------------------------------------------------------------------
/src/Manager/AttributeManager.php:
--------------------------------------------------------------------------------
1 | Prefix to add before getter name (default)'get'
26 | * 'getter_name_transformation_function' => Function to apply on the getter name ( before adding prefix ) (default)'ucfirst'
27 | */
28 | public function __construct(Configuration $configuration, array $options = [])
29 | {
30 | $this->attributes = $configuration->getAttributes();
31 |
32 | $options = array_intersect_key($options, array_flip([
33 | 'getter_prefix',
34 | 'getter_name_transformation_function',
35 | ]));
36 |
37 | foreach ($options as $name => $value) {
38 | $this->$name = $value;
39 | }
40 | }
41 |
42 | public function getAttribute(string $attributeId): AbstractAttribute
43 | {
44 | $attributeKeys = explode('.', $attributeId);
45 | // The first element will be the attribute ID, then the field ID
46 | $attributeId = array_shift($attributeKeys);
47 | $attributeName = implode('.', $attributeKeys);
48 | // The field ID is also the attribute object property
49 | $attributeData = $this->attributes[$attributeId];
50 | return
51 | ($attributeId === 'environment')
52 | ? $this->getEnvironmentAttribute($attributeData, $attributeName)
53 | : $this->getClassicAttribute($attributeData, $attributeName)
54 | ;
55 | }
56 |
57 | private function getClassicAttribute(array $attributeData, string $property): Attribute
58 | {
59 | return
60 | (new Attribute())
61 | ->setName($attributeData['fields'][$property]['name'])
62 | ->setType($attributeData['type'])
63 | ->setProperty($property)
64 | ->setSlug($this->slugify($attributeData['fields'][$property]['name']))
65 | ;
66 | }
67 |
68 | private function getEnvironmentAttribute(array $attributeData, string $key): EnvironmentAttribute
69 | {
70 | return
71 | (new EnvironmentAttribute())
72 | ->setName($attributeData[$key]['name'])
73 | ->setType('environment')
74 | ->setVariableName($attributeData[$key]['variable_name'])
75 | ->setSlug($this->slugify($attributeData[$key]['name']))
76 | ;
77 | }
78 |
79 | public function retrieveAttribute(AbstractAttribute $attribute, $user = null, $object = null, array $getter_params = [])
80 | {
81 | switch ($attribute->getType()) {
82 | case 'user':
83 | return $this->retrieveClassicAttribute($attribute, $user, $getter_params);
84 | case 'resource':
85 | return $this->retrieveClassicAttribute($attribute, $object);
86 | case 'environment':
87 | return $this->retrieveEnvironmentAttribute($attribute);
88 | }
89 | }
90 |
91 | private function retrieveClassicAttribute(Attribute $attribute, $object, array $getter_params = [])
92 | {
93 | $propertyPath = explode('.', $attribute->getProperty());
94 | $propertyValue = $object;
95 | foreach ($propertyPath as $property) {
96 | $getter = $this->getter_prefix.call_user_func($this->getter_name_transformation_function, $property);
97 | // Use is_callable, instead of method_exists, to deal with __call magic method
98 | if (!is_callable([$propertyValue,$getter])) {
99 | throw new \InvalidArgumentException('There is no getter for the "'.$attribute->getProperty().'" attribute for object "'.get_class($propertyValue).'" with getter "'.$getter.'"');
100 | }
101 | if (($propertyValue = call_user_func_array([
102 | $propertyValue,
103 | $getter,
104 | ], isset($getter_params[ $property ]) ? $getter_params[ $property ] : [])) === null
105 | ) {
106 | return null;
107 | }
108 | }
109 | return $propertyValue;
110 | }
111 |
112 | private function retrieveEnvironmentAttribute(EnvironmentAttribute $attribute)
113 | {
114 | return getenv($attribute->getVariableName());
115 | }
116 |
117 | public function slugify(string $name): string
118 | {
119 | // replace non letter or digits by -
120 | $name = trim(preg_replace('~[^\\pL\d]+~u', '-', $name), '-');
121 | // transliterate
122 | if (function_exists('iconv')) {
123 | $name = iconv('utf-8', 'us-ascii//TRANSLIT', $name);
124 | }
125 | // remove unwanted characters
126 | $name = preg_replace('~[^-\w]+~', '', strtolower($name));
127 | if (empty($name)) {
128 | return 'n-a';
129 | }
130 | return $name;
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/Manager/ComparisonManager.php:
--------------------------------------------------------------------------------
1 | ArrayComparison::class,
24 | 'boolean' => BooleanComparison::class,
25 | 'datetime' => DatetimeComparison::class,
26 | 'numeric' => NumericComparison::class,
27 | 'object' => ObjectComparison::class,
28 | 'user' => UserComparison::class,
29 | 'string' => StringComparison::class,
30 | ];
31 | /** @var array **/
32 | protected $rejectedAttributes = [];
33 |
34 | public function __construct(AttributeManager $manager)
35 | {
36 | $this->attributeManager = $manager;
37 | }
38 |
39 | /**
40 | * This method retrieve the comparison class, instanciate it,
41 | * and then perform the configured comparison
42 | * It does return a control value for special operations,
43 | * but the real check is at the end of the enforce() method,
44 | * when the rejected attributes are counted.
45 | *
46 | * If the second parameter is set to true, compare will not report errors.
47 | * This is used to test a bunch of comparisons expecting not all of them true to return a granted access.
48 | * In fact, this parameter is used in comparisons which need to perform comparisons on their own.
49 | */
50 | public function compare(PolicyRuleAttribute $pra, bool $subComparing = false): bool
51 | {
52 | $attribute = $pra->getAttribute();
53 | // The expected value can be set in the configuration as dynamic
54 | // In this case, we retrieve the expected value in the passed options
55 | $praValue =
56 | ($pra->getValue() === 'dynamic')
57 | ? $this->getDynamicAttribute($attribute->getSlug())
58 | : $pra->getValue()
59 | ;
60 | // Checking that the configured comparison type is available
61 | if (!isset($this->comparisons[$pra->getComparisonType()])) {
62 | throw new \InvalidArgumentException('The requested comparison class does not exist');
63 | }
64 | // The comparison class will perform the attribute check with the configured method
65 | // For more complex comparisons, the comparison manager is injected
66 | $comparison = new $this->comparisons[$pra->getComparisonType()]($this);
67 | if (!method_exists($comparison, $pra->getComparison())) {
68 | throw new \InvalidArgumentException('The requested comparison method does not exist');
69 | }
70 | // Then the comparison is performed with needed
71 | $result = $comparison->{$pra->getComparison()}($praValue, $attribute->getValue(), $pra->getExtraData());
72 | // If the checked attribute is not valid, the attribute slug is marked as rejected
73 | // The rejected attributes will be returned instead of the expected true boolean
74 | if ($result !== true) {
75 | // In case of sub comparing, the error reporting is disabled
76 | if (!in_array($attribute->getSlug(), $this->rejectedAttributes) && $subComparing === false) {
77 | $this->rejectedAttributes[] = $attribute->getSlug();
78 | }
79 | return false;
80 | }
81 | return true;
82 | }
83 |
84 | public function setDynamicAttributes(array $dynamicAttributes)
85 | {
86 | $this->dynamicAttributes = $dynamicAttributes;
87 | }
88 |
89 | /**
90 | * A dynamic attribute is a value given by the user code as an option
91 | * If a policy rule attribute is dynamic,
92 | * we check that the developer has given a dynamic value in the options
93 | *
94 | * Dynamic attributes are given with slugs as key
95 | *
96 | * @param string $attributeSlug
97 | * @return mixed
98 | * @throws \InvalidArgumentException
99 | */
100 | public function getDynamicAttribute(string $attributeSlug)
101 | {
102 | if (!isset($this->dynamicAttributes[$attributeSlug])) {
103 | throw new \InvalidArgumentException("The dynamic value for attribute $attributeSlug was not given");
104 | }
105 | return $this->dynamicAttributes[$attributeSlug];
106 | }
107 |
108 | public function addComparison(string $type, string $class)
109 | {
110 | $this->comparisons[$type] = $class;
111 | }
112 |
113 | public function getAttributeManager(): AttributeManager
114 | {
115 | return $this->attributeManager;
116 | }
117 |
118 | /**
119 | * This method is called when all the policy rule attributes are checked
120 | * All along the comparisons, the failing attributes slugs are stored
121 | * If the rejected attributes array is not empty, it means that the rule is not enforced
122 | */
123 | public function getResult(): array
124 | {
125 | $result =
126 | (count($this->rejectedAttributes) > 0)
127 | ? $this->rejectedAttributes
128 | : []
129 | ;
130 | $this->rejectedAttributes = [];
131 | return $result;
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/tests/Manager/AttributeManagerTest.php:
--------------------------------------------------------------------------------
1 | manager = new AttributeManager($this->getConfigurationMock());
22 | }
23 |
24 | public function testGetClassicAttribute()
25 | {
26 | $attribute = $this->manager->getAttribute('main_user.age');
27 |
28 | $this->assertInstanceOf(Attribute::class, $attribute);
29 | $this->assertEquals('user', $attribute->getType());
30 | $this->assertEquals('Age', $attribute->getName());
31 | $this->assertEquals('age', $attribute->getSlug());
32 | $this->assertEquals('age', $attribute->getProperty());
33 | $this->assertNull($attribute->getValue());
34 | }
35 |
36 | public function testGetEnvironmentAttribute()
37 | {
38 | $attribute = $this->manager->getAttribute('environment.service_state');
39 |
40 | $this->assertInstanceOf(EnvironmentAttribute::class, $attribute);
41 | $this->assertEquals('environment', $attribute->getType());
42 | $this->assertEquals('SERVICE_STATE', $attribute->getVariableName());
43 | $this->assertEquals('Statut du service', $attribute->getName());
44 | $this->assertEquals('statut-du-service', $attribute->getSlug());
45 | $this->assertNull($attribute->getValue());
46 | }
47 |
48 | public function testRetrieveClassicAttribute()
49 | {
50 | $this->assertEquals(18, $this->manager->retrieveAttribute(
51 | $this->manager->getAttribute('main_user.age'),
52 | (new User())->setAge(18)
53 | ));
54 | }
55 |
56 | public function testRetrieveEnvironmentAttribute()
57 | {
58 | putenv('SERVICE_STATE=OPEN');
59 | $this->assertEquals('OPEN', $this->manager->retrieveAttribute(
60 | $this->manager->getAttribute('environment.service_state'),
61 | (new User())->setAge(18)
62 | ));
63 | }
64 |
65 | public function getConfigurationMock()
66 | {
67 | $configurationMock = $this
68 | ->getMockBuilder(Configuration::class)
69 | ->disableOriginalConstructor()
70 | ->getMock()
71 | ;
72 | $configurationMock
73 | ->expects($this->any())
74 | ->method('getAttributes')
75 | ->willReturnCallback([$this, 'getAttributesMock'])
76 | ;
77 | return $configurationMock;
78 | }
79 |
80 | public function getAttributesMock()
81 | {
82 | return [
83 | 'main_user' => [
84 | 'class' => 'PhpAbac\Example\User',
85 | 'type' => 'user',
86 | 'fields' => [
87 | 'id' => [
88 | 'name' => 'ID'
89 | ],
90 | 'age' => [
91 | 'name' => 'Age'
92 | ],
93 | 'parentNationality' => [
94 | 'name' => 'Nationalité des parents'
95 | ],
96 | 'hasDoneJapd' => [
97 | 'name' => 'JAPD'
98 | ],
99 | 'hasDrivingLicense' => [
100 | 'name' => 'Permis de conduire'
101 | ],
102 | 'visas' => [
103 | 'name' => 'Visas'
104 | ]
105 | ]
106 | ],
107 |
108 | 'vehicle' => [
109 | 'class' => 'PhpAbac\Example\Vehicle',
110 | 'type' => 'resource',
111 | 'fields' => [
112 | 'origin' => [
113 | 'name' => 'Origine'
114 | ],
115 | 'owner.id' => [
116 | 'name' => 'Propriétaire'
117 | ],
118 | 'manufactureDate' => [
119 | 'name' => "Date de sortie d'usine"
120 | ],
121 | 'lastTechnicalReviewDate' => [
122 | 'name' => 'Dernière révision technique'
123 | ]
124 | ],
125 | ],
126 | 'country' => [
127 | 'class' => 'PhpAbac\Example\Country',
128 | 'type' => 'resource',
129 | 'fields' => [
130 | 'name' => [
131 | 'name' => 'Nom du pays'
132 | ],
133 | 'code' => [
134 | 'name' => 'Code international'
135 | ]
136 | ]
137 | ],
138 | 'visa' => [
139 | 'class' => 'PhpAbac\Example\Visa',
140 | 'type' => 'resource',
141 | 'fields' => [
142 | 'country.code' => [
143 | 'name' => 'Code Pays'
144 | ],
145 | 'lastRenewal' => [
146 | 'name' => 'Dernier renouvellement'
147 | ]
148 | ]
149 | ],
150 | 'environment' => [
151 | 'service_state' => [
152 | 'name' => 'Statut du service',
153 | 'variable_name' => 'SERVICE_STATE'
154 | ]
155 | ]
156 | ];
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/tests/Comparison/ArrayComparisonTest.php:
--------------------------------------------------------------------------------
1 | comparison = new ArrayComparison($this->getComparisonManagerMock());
24 | }
25 |
26 | public function testIsIn()
27 | {
28 | $this->assertTrue($this->comparison->isIn([
29 | 'value',
30 | 'expected_value',
31 | 'another_value',
32 | ], 'expected_value'));
33 | $this->assertFalse($this->comparison->isIn([
34 | 'value',
35 | 'another_value',
36 | ], 'expected_value'));
37 | }
38 |
39 | public function testIsNotIn()
40 | {
41 | $this->assertTrue($this->comparison->isNotIn([
42 | 'value',
43 | 'another_value',
44 | ], 'expected_value'));
45 | $this->assertFalse($this->comparison->isNotIn([
46 | 'value',
47 | 'expected_value',
48 | 'another_value',
49 | ], 'expected_value'));
50 | }
51 |
52 | public function testIntersect()
53 | {
54 | $this->assertTrue($this->comparison->intersect([
55 | 'ROLE_USER',
56 | 'ROLE_MODERATOR',
57 | 'ROLE_ADMIN',
58 | ], [
59 | 'ROLE_USER',
60 | 'ROLE_POST_MANAGER',
61 | ]));
62 | $this->assertFalse($this->comparison->intersect([
63 | 'ROLE_MODERATOR',
64 | 'ROLE_ADMIN',
65 | ], [
66 | 'ROLE_USER',
67 | 'ROLE_POST_MANAGER',
68 | ]));
69 | }
70 |
71 | public function testDoNotIntersect()
72 | {
73 | $this->assertTrue($this->comparison->doNotIntersect([
74 | 'ROLE_MODERATOR',
75 | 'ROLE_ADMIN',
76 | ], [
77 | 'ROLE_USER',
78 | 'ROLE_POST_MANAGER',
79 | ]));
80 | $this->assertFalse($this->comparison->doNotIntersect([
81 | 'ROLE_USER',
82 | 'ROLE_MODERATOR',
83 | 'ROLE_ADMIN',
84 | ], [
85 | 'ROLE_USER',
86 | 'ROLE_POST_MANAGER',
87 | ]));
88 | }
89 |
90 | public function testContains()
91 | {
92 | $countries = include(__DIR__ . '/../fixtures/countries.php');
93 | $visas = include(__DIR__ . '/../fixtures/visas.php');
94 | $policyRuleAttributes = [
95 | (new PolicyRuleAttribute())
96 | ->setAttribute(
97 | (new Attribute())
98 | ->setType('resource')
99 | ->setProperty('country.code')
100 | ->setName('Code Pays')
101 | ->setSlug('code-pays')
102 | )
103 | ->setComparison('isEqual')
104 | ->setComparisonType('string')
105 | ->setValue('US'),
106 | (new PolicyRuleAttribute())
107 | ->setAttribute(
108 | (new Attribute())
109 | ->setType('resource')
110 | ->setProperty('lastRenewal')
111 | ->setName('Dernier renouvellement')
112 | ->setSlug('dernier-renouvellement')
113 | )
114 | ->setComparison('isMoreRecentThan')
115 | ->setComparisonType('datetime')
116 | ->setValue('-1Y'),
117 | ];
118 | $extraData = [
119 | 'attribute' =>
120 | (new Attribute())
121 | ->setProperty('visas')
122 | ->setName('Visas')
123 | ->setSlug('visas')
124 | ->setType('resource')
125 | ->setValue([$visas[0], $visas[1]])
126 | ,
127 | 'user' =>
128 | (new User())
129 | ->setId(1)
130 | ->setName('John Doe')
131 | ->setAge(36)
132 | ->setParentNationality('FR')
133 | ->addVisa($visas[0])
134 | ->addVisa($visas[1])
135 | ->setHasDoneJapd(true)
136 | ->setHasDrivingLicense(false)
137 | ,
138 | 'resource' => null
139 | ];
140 | $this->assertFalse($this->comparison->contains($policyRuleAttributes, [$visas[0], $visas[1]], $extraData));
141 | // $extraData['user']->addVisa($visas[2]);
142 | // $extraData['attribute']->setValue($visas);
143 | // $this->assertTrue($this->comparison->contains($policyRuleAttributes, [$visas[0], $visas[1], $visas[2]], $extraData));
144 | }
145 |
146 | public function getComparisonManagerMock()
147 | {
148 | $comparisonManagerMock = $this
149 | ->getMockBuilder(ComparisonManager::class)
150 | ->disableOriginalConstructor()
151 | ->getMock()
152 | ;
153 | $comparisonManagerMock
154 | ->expects($this->any())
155 | ->method('getAttributeManager')
156 | ->willReturnCallback([$this, 'getAttributeManagerMock'])
157 | ;
158 | return $comparisonManagerMock;
159 | }
160 |
161 | public function getAttributeManagerMock()
162 | {
163 | $attributeManagerMock = $this
164 | ->getMockBuilder(AttributeManager::class)
165 | ->disableOriginalConstructor()
166 | ->getMock()
167 | ;
168 | $attributeManagerMock
169 | ->expects($this->any())
170 | ->method('retrieveAttribute')
171 | ->willReturn('US')
172 | ;
173 | return $attributeManagerMock;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/tests/fixtures/policy_rules.json:
--------------------------------------------------------------------------------
1 | {
2 | "attributes": {
3 | "main_user": {
4 | "class": "PhpAbac\\Example\\User",
5 | "type": "user",
6 | "fields": {
7 | "id": {
8 | "name": "ID"
9 | },
10 | "age": {
11 | "name": "Age"
12 | },
13 | "parentNationality": {
14 | "name": "Nationalité des parents"
15 | },
16 | "hasDoneJapd": {
17 | "name": "JAPD"
18 | },
19 | "hasDrivingLicense": {
20 | "name": "Permis de conduire"
21 | },
22 | "visas": {
23 | "name": "Visas"
24 | }
25 | }
26 | },
27 | "vehicle": {
28 | "class": "PhpAbac\\Example\\Vehicle",
29 | "type": "resource",
30 | "fields": {
31 | "origin": {
32 | "name": "Origine"
33 | },
34 | "owner.id": {
35 | "name": "Propriétaire"
36 | },
37 | "manufactureDate": {
38 | "name": "Date de sortie d'usine"
39 | },
40 | "lastTechnicalReviewDate": {
41 | "name": "Dernière révision technique"
42 | }
43 | }
44 | },
45 | "country": {
46 | "class": "PhpAbac\\Example\\Country",
47 | "type": "resource",
48 | "fields": {
49 | "name": {
50 | "name": "Nom du pays"
51 | },
52 | "code": {
53 | "name": "Code international"
54 | }
55 | }
56 | },
57 | "visa": {
58 | "class": "PhpAbac\\Example\\Visa",
59 | "type": "resource",
60 | "fields": {
61 | "country.code": {
62 | "name": "Code Pays"
63 | },
64 | "lastRenewal": {
65 | "name": "Dernier renouvellement"
66 | }
67 | }
68 | },
69 | "environment": {
70 | "service_state": {
71 | "name": "Statut du service",
72 | "variable_name": "SERVICE_STATE"
73 | }
74 | }
75 | },
76 | "rules": {
77 | "nationality-access": {
78 | "attributes": {
79 | "main_user.age": {
80 | "comparison_type": "numeric",
81 | "comparison": "isGreaterThan",
82 | "value": 18
83 | },
84 | "main_user.parentNationality": {
85 | "comparison_type": "string",
86 | "comparison": "isEqual",
87 | "value": "FR"
88 | },
89 | "main_user.hasDoneJapd": {
90 | "comparison_type": "boolean",
91 | "comparison": "boolAnd",
92 | "value": true
93 | }
94 | }
95 | },
96 | "vehicle-homologation": {
97 | "attributes": {
98 | "main_user.hasDrivingLicense": {
99 | "comparison_type": "boolean",
100 | "comparison": "boolAnd",
101 | "value": true
102 | },
103 | "vehicle.lastTechnicalReviewDate": {
104 | "comparison_type": "datetime",
105 | "comparison": "isMoreRecentThan",
106 | "value": "-2Y"
107 | },
108 | "vehicle.manufactureDate": {
109 | "comparison_type": "datetime",
110 | "comparison": "isMoreRecentThan",
111 | "value": "-25Y"
112 | },
113 | "vehicle.owner.id": {
114 | "comparison_type": "user",
115 | "comparison": "isFieldEqual",
116 | "value": "main_user.id"
117 | },
118 | "vehicle.origin": {
119 | "comparison_type": "array",
120 | "comparison": "isIn",
121 | "value": [
122 | "FR",
123 | "DE",
124 | "IT",
125 | "L",
126 | "GB",
127 | "P",
128 | "ES",
129 | "NL",
130 | "B"
131 | ]
132 | },
133 | "environment.service_state": {
134 | "comparison_type": "string",
135 | "comparison": "isEqual",
136 | "value": "OPEN"
137 | }
138 | }
139 | },
140 | "gunlaw": {
141 | "attributes": {
142 | "main_user.age": {
143 | "comparison_type": "numeric",
144 | "comparison": "isGreaterThan",
145 | "value": 21
146 | }
147 | }
148 | },
149 | "travel-to-foreign-country": {
150 | "attributes": {
151 | "main_user.age": {
152 | "comparison_type": "numeric",
153 | "comparison": "isGreaterThan",
154 | "value": 18
155 | },
156 | "main_user.visas": {
157 | "comparison_type": "array",
158 | "comparison": "contains",
159 | "with": {
160 | "visa.country.code": {
161 | "comparison_type": "string",
162 | "comparison": "isEqual",
163 | "value": "dynamic"
164 | },
165 | "visa.lastRenewal": {
166 | "comparison_type": "datetime",
167 | "comparison": "isMoreRecentThan",
168 | "value": "-1Y"
169 | }
170 | }
171 | }
172 | }
173 | }
174 | }
175 | }
--------------------------------------------------------------------------------
/src/Abac.php:
--------------------------------------------------------------------------------
1 | attributeManager = $attributeManager;
29 | $this->policyRuleManager = $policyRuleManager;
30 | $this->cacheManager = $cacheManager;
31 | $this->comparisonManager = $comparisonManager;
32 | }
33 |
34 | /**
35 | * Return true if both user and object respects all the rules conditions
36 | * If the objectId is null, policy rules about its attributes will be ignored
37 | * In case of mismatch between attributes and expected values,
38 | * an array with the concerned attributes slugs will be returned.
39 | *
40 | * Available options are :
41 | * * dynamic_attributes: array
42 | * * cache_result: boolean
43 | * * cache_ttl: integer
44 | * * cache_driver: string
45 | *
46 | * Available cache drivers are :
47 | * * memory
48 | */
49 | public function enforce(string $ruleName, $user, $resource = null, array $options = []): bool
50 | {
51 | $this->errors = [];
52 | // If there is dynamic attributes, we pass them to the comparison manager
53 | // When a comparison will be performed, the passed values will be retrieved and used
54 | if (isset($options[ 'dynamic_attributes' ])) {
55 | $this->comparisonManager->setDynamicAttributes($options[ 'dynamic_attributes' ]);
56 | }
57 | // Retrieve cache value for the current rule and values if cache item is valid
58 | if (($cacheResult = isset($options[ 'cache_result' ]) && $options[ 'cache_result' ] === true) === true) {
59 | $cacheItem = $this->cacheManager->getItem("$ruleName-{$user->getId()}-" . (($resource !== null) ? $resource->getId() : ''), (isset($options[ 'cache_driver' ])) ? $options[ 'cache_driver' ] : null, (isset($options[ 'cache_ttl' ])) ? $options[ 'cache_ttl' ] : null);
60 | // We check if the cache value s valid before returning it
61 | if (($cacheValue = $cacheItem->get()) !== null) {
62 | return $cacheValue;
63 | }
64 | }
65 | $policyRules = $this->policyRuleManager->getRule($ruleName, $user, $resource);
66 |
67 | foreach ($policyRules as $policyRule) {
68 | // For each policy rule attribute, we retrieve the attribute value and proceed configured extra data
69 | foreach ($policyRule->getPolicyRuleAttributes() as $pra) {
70 | /** @var PolicyRuleAttribute $pra */
71 | $attribute = $pra->getAttribute();
72 |
73 | $getter_params = $this->prepareGetterParams($pra->getGetterParams(), $user, $resource);
74 | $attribute->setValue($this->attributeManager->retrieveAttribute($attribute, $user, $resource, $getter_params));
75 | if (count($pra->getExtraData()) > 0) {
76 | $this->processExtraData($pra, $user, $resource);
77 | }
78 | $this->comparisonManager->compare($pra);
79 | }
80 | // The given result could be an array of rejected attributes or true
81 | // True means that the rule is correctly enforced for the given user and resource
82 | $this->errors = $this->comparisonManager->getResult();
83 | if (count($this->errors) === 0) {
84 | break;
85 | }
86 | }
87 | if ($cacheResult) {
88 | $cacheItem->set((count($this->errors) > 0) ? $this->errors : true);
89 | $this->cacheManager->save($cacheItem);
90 | }
91 | return count($this->errors) === 0;
92 | }
93 |
94 | public function getErrors(): array
95 | {
96 | return $this->errors;
97 | }
98 |
99 | /**
100 | * Function to prepare Getter Params when getter require parameters ( this parameters must be specified in configuration file)
101 | *
102 | * @param $getter_params
103 | * @param $user
104 | * @param $resource
105 | *
106 | * @return array
107 | */
108 | private function prepareGetterParams($getter_params, $user, $resource)
109 | {
110 | if (empty($getter_params)) {
111 | return [];
112 | }
113 | $values = [];
114 | foreach ($getter_params as $getter_name=>$params) {
115 | foreach ($params as $param) {
116 | if ('@' !== $param[ 'param_name' ][ 0 ]) {
117 | $values[$getter_name][] = $param[ 'param_value' ];
118 | } else {
119 | $values[$getter_name][] = $this->attributeManager->retrieveAttribute($this->attributeManager->getAttribute($param[ 'param_value' ]), $user, $resource);
120 | }
121 | }
122 | }
123 | return $values;
124 | }
125 |
126 | private function processExtraData(PolicyRuleAttribute $pra, $user, $resource)
127 | {
128 | foreach ($pra->getExtraData() as $key => $data) {
129 | switch ($key) {
130 | case 'with':
131 | // This data has to be removed for it will be stored elsewhere
132 | // in the policy rule attribute
133 | $pra->removeExtraData('with');
134 | // The "with" extra data is an array of attributes, which are objects
135 | // Once we process it as policy rule attributes, we set it as the main policy rule attribute value
136 | $subPolicyRuleAttributes = [];
137 |
138 | foreach ($this->policyRuleManager->processRuleAttributes($data, $user, $resource) as $subPolicyRuleAttribute) {
139 | $subPolicyRuleAttributes[] = $subPolicyRuleAttribute;
140 | }
141 | $pra->setValue($subPolicyRuleAttributes);
142 | // This data can be used in complex comparisons
143 | $pra->addExtraData('attribute', $pra->getAttribute());
144 | $pra->addExtraData('user', $user);
145 | $pra->addExtraData('resource', $resource);
146 | break;
147 | }
148 | }
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/tests/Manager/PolicyRuleManagerTest.php:
--------------------------------------------------------------------------------
1 | manager = new PolicyRuleManager(
25 | $this->getConfigurationMock(),
26 | $this->getAttributeManagerMock()
27 | );
28 | }
29 |
30 | public function testGetRule()
31 | {
32 | $countries = include('tests/fixtures/countries.php');
33 | $visas = include('tests/fixtures/visas.php');
34 | $users = include('tests/fixtures/users.php');
35 | $vehicles = include('tests/fixtures/vehicles.php');
36 |
37 | $policyRule_a = $this->manager->getRule('vehicle-homologation', $users[0], $vehicles[0]);
38 |
39 | $policyRule = $policyRule_a[0];
40 |
41 | $this->assertInstanceof(PolicyRule::class, $policyRule);
42 | $this->assertEquals('vehicle-homologation', $policyRule->getName());
43 | $this->assertCount(6, $policyRule->getPolicyRuleAttributes());
44 |
45 | $policyRuleAttribute = $policyRule->getPolicyRuleAttributes()[0];
46 |
47 | $this->assertInstanceOf(PolicyRuleAttribute::class, $policyRuleAttribute);
48 | $this->assertInstanceOf(Attribute::class, $policyRuleAttribute->getAttribute());
49 | $this->assertEquals('boolean', $policyRuleAttribute->getComparisonType());
50 | $this->assertEquals('boolAnd', $policyRuleAttribute->getComparison());
51 | $this->assertTrue($policyRuleAttribute->getValue());
52 | }
53 |
54 | public function getConfigurationMock()
55 | {
56 | $configurationMock = $this
57 | ->getMockBuilder(Configuration::class)
58 | ->disableOriginalConstructor()
59 | ->getMock()
60 | ;
61 | $configurationMock
62 | ->expects($this->any())
63 | ->method('getRules')
64 | ->willReturnCallback([$this, 'getRulesMock'])
65 | ;
66 | return $configurationMock;
67 | }
68 |
69 | public function getRulesMock()
70 | {
71 | return [
72 | 'nationality-access' => [
73 | 'attributes' => [
74 | 'main_user.age' => [
75 | 'comparison_type' => 'numeric',
76 | 'comparison' => 'isGreaterThan',
77 | 'value' => 18
78 | ],
79 | 'main_user.parentNationality' => [
80 | 'comparison_type' => 'string',
81 | 'comparison' => 'isEqual',
82 | 'value' => 'FR'
83 | ],
84 | 'main_user.hasDoneJapd' => [
85 | 'comparison_type' => 'boolean',
86 | 'comparison' => 'boolAnd',
87 | 'value' => true
88 | ]
89 | ]
90 | ],
91 | 'vehicle-homologation' => [
92 | 'attributes' => [
93 | 'main_user.hasDrivingLicense' => [
94 | 'comparison_type' => 'boolean',
95 | 'comparison' => 'boolAnd',
96 | 'value' => true
97 | ],
98 | 'vehicle.lastTechnicalReviewDate' => [
99 | 'comparison_type' => 'datetime',
100 | 'comparison' => 'isMoreRecentThan',
101 | 'value' => '-2Y'
102 | ],
103 | 'vehicle.manufactureDate' => [
104 | 'comparison_type' => 'datetime',
105 | 'comparison' => 'isMoreRecentThan',
106 | 'value' => '-25Y'
107 | ],
108 | 'vehicle.owner.id' => [
109 | 'comparison_type' => 'user',
110 | 'comparison' => 'isFieldEqual',
111 | 'value' => 'main_user.id'
112 | ],
113 | 'vehicle.origin' => [
114 | 'comparison_type' => 'array',
115 | 'comparison' => 'isIn',
116 | 'value' => ["FR", "DE", "IT", "L", "GB", "P", "ES", "NL", "B"]
117 | ],
118 | 'environment.service_state' => [
119 | 'comparison_type' => 'string',
120 | 'comparison' => 'isEqual',
121 | 'value' => 'OPEN'
122 | ]
123 | ]
124 | ],
125 | 'gunlaw' => [
126 | 'attributes' => [
127 | 'main_user.age' => [
128 | 'comparison_type' => 'numeric',
129 | 'comparison' => 'isGreaterThan',
130 | 'value' => 21
131 | ]
132 | ]
133 | ],
134 | 'travel-to-foreign-country' => [
135 | 'attributes' => [
136 | 'main_user.age' => [
137 | 'comparison_type' => 'numeric',
138 | 'comparison' => 'isGreaterThan',
139 | 'value' => 18
140 | ],
141 | 'main_user.visas' => [
142 | 'comparison_type' => 'array',
143 | 'comparison' => 'contains',
144 | 'with' => [
145 | 'visa.country.code' => [
146 | 'comparison_type' => 'string',
147 | 'comparison' => 'isEqual',
148 | 'value' => 'dynamic'
149 | ],
150 | 'visa.lastRenewal' => [
151 | 'comparison_type' => 'datetime',
152 | 'comparison' => 'isMoreRecentThan',
153 | 'value' => '-1Y'
154 | ]
155 | ]
156 | ]
157 | ]
158 | ]
159 | ];
160 | }
161 |
162 | public function getAttributeManagerMock()
163 | {
164 | $attributeManagerMock = $this
165 | ->getMockBuilder(AttributeManager::class)
166 | ->disableOriginalConstructor()
167 | ->getMock()
168 | ;
169 | $attributeManagerMock
170 | ->expects($this->any())
171 | ->method('getAttribute')
172 | ->willReturnCallback([$this, 'getAttributeMock'])
173 | ;
174 | return $attributeManagerMock;
175 | }
176 |
177 | public function getAttributeMock($name)
178 | {
179 | return
180 | (new Attribute())
181 | ->setName($name)
182 | ->setSlug($name)
183 | ->setType((strpos('main_user', $name)) ? 'user' : 'resource')
184 | ->setValue($this->getAttributeValueMock($name))
185 | ;
186 | }
187 |
188 | public function getAttributeValueMock($name)
189 | {
190 | switch ($name) {
191 | case 'main_user.hasDrivingLicense':
192 | return true;
193 | case 'vehicle.lastTechnicalReviewDate':
194 | return new \DateTime('-6 months');
195 | case 'vehicle.manufactureDate':
196 | return new \DateTime('-2 years');
197 | case 'vehicle.owner.id':
198 | return 1;
199 | case 'vehicle.origin':
200 | return 'FR';
201 | case 'environment.service_state':
202 | return 'OPEN';
203 | default:
204 | var_dump($name);
205 | break;
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [CraftCamp] php-abac
2 | ========
3 |
4 | ### Attribute-Based Access Control implementation library
5 |
6 | [](https://packagist.org/packages/craftcamp/php-abac)
7 | [](https://packagist.org/packages/craftcamp/php-abac)
8 | [](https://travis-ci.org/CraftCamp/php-abac)
9 | [](https://scrutinizer-ci.com/g/CraftCamp/php-abac/?branch=master)
10 | [](https://scrutinizer-ci.com/g/CraftCamp/php-abac/?branch=master)
11 | [](https://packagist.org/packages/craftcamp/php-abac)
12 | [](https://packagist.org/packages/craftcamp/php-abac)
13 |
14 | Introduction
15 | ------------
16 |
17 | This library is meant to implement the concept of ABAC in your PHP applications.
18 |
19 | The concept is to manage access control using attributes : from users, from resources and environment.
20 |
21 | It allows us to define rules based on the properties of the user object and optionally the accessed object.
22 |
23 | These rules will be checked in your application to determine if an user is allowed to perform an action.
24 |
25 | The following links explain what ABAC is :
26 |
27 | * [ABAC Introduction](http://www.axiomatics.com/attribute-based-access-control.html)
28 | * [NIST specification](http://nvlpubs.nist.gov/nistpubs/specialpublications/NIST.sp.800-162.pdf)
29 |
30 | Installation
31 | ------------
32 |
33 | **Using composer :**
34 |
35 | ```sh
36 | composer require craftcamp/php-abac
37 | ```
38 |
39 | Then you will have to configure the attributes and the rules of your application.
40 |
41 | For more details about this, please refer to the [dedicated documentation](doc/configuration.md)
42 |
43 | Documentation
44 | ------------
45 |
46 | * [Configuration](doc/configuration.md)
47 | * [Dependency Injection](doc/dependency-injection.md)
48 | * [Access-control](doc/access-control.md)
49 | * [Comparisons](doc/comparisons.md)
50 | * [Caching](doc/caching.md)
51 |
52 | Usage Examples
53 | -------------
54 |
55 | **Example with only user attributes defined in the rule**
56 |
57 | We have in this example a single object, representing the current user.
58 |
59 | This object have properties, with getter methods to access the values.
60 |
61 | For example, we can code :
62 |
63 | ```php
64 | id;
75 | }
76 |
77 | public function setIsBanned($isBanned) {
78 | $this->isBanned = $isBanned;
79 |
80 | return $this;
81 | }
82 |
83 | public function getIsBanned() {
84 | return $this->isBanned;
85 | }
86 | }
87 |
88 | $user = new User();
89 | $user->setIsBanned(true);
90 |
91 | $abac = AbacFactory::getAbac([
92 | 'policy_rule_configuration.yml'
93 | ]);
94 | $abac->enforce('create-group', $user);
95 | ```
96 | The attributes checked by the rule can be :
97 |
98 | |User|
99 | |-----|
100 | |isBanned = false|
101 |
102 | **Example with both user and object attributes**
103 | ```php
104 | use PhpAbac\AbacFactory;
105 |
106 | $abac = AbacFactory::getAbac([
107 | 'policy_rule_configuration.yml'
108 | ]);
109 | $check = $abac->enforce('read-public-group', $user, $group);
110 | ```
111 | The checked attributes can be :
112 |
113 | |User|Group|
114 | |-----|----|
115 | |isBanned = 0|isActive = 1|
116 | ||isPublic = 1|
117 |
118 | **Example with dynamic attributes**
119 | ```php
120 | enforce('edit-group', $user, $group, [
128 | 'dynamic-attributes' => [
129 | 'group-owner' => $user->getId()
130 | ]
131 | ]);
132 | ```
133 |
134 | **Example with referenced attributes**
135 |
136 | The configuration shall be :
137 |
138 | ```yaml
139 | attributes:
140 | group:
141 | class: MyApp\Model\Group
142 | type: resource
143 | fields:
144 | author.id:
145 | name: Author ID
146 | app_user:
147 | class: MyApp\Model\User
148 | type: user
149 | fields:
150 | id:
151 | name: User ID
152 |
153 | rules:
154 | remove-group:
155 | attributes:
156 | app_user.id:
157 | comparison: object
158 | comparison_type: isFieldEqual
159 | value: group.author.id
160 | ```
161 | And then the code :
162 |
163 | ```php
164 | enforce('remove-group', $user, $group);
172 | ```
173 |
174 |
175 | **Example with cache**
176 | ```php
177 | $check = $abac->enforce('edit-group', $user, $group, [
178 | 'cache_result' => true,
179 | 'cache_ttl' => 3600, // Time To Live in seconds
180 | 'cache_driver' => 'memory' // memory is the default driver, you can avoid this option
181 | ]);
182 | ```
183 |
184 | **Example with multiple rules (ruleSet) for an unique rule.**
185 | Each rule are tested and the treatment stop when the first rule of the ruleSet allow access
186 |
187 | The configuration shall be (alcoolaw.yml):
188 |
189 | ```yaml
190 | attributes:
191 | main_user:
192 | class: PhpAbac\Example\User
193 | type: user
194 | fields:
195 | age:
196 | name: Age
197 | country:
198 | name: Code ISO du pays
199 | rules:
200 | alcoollaw:
201 | -
202 | attributes:
203 | main_user.age:
204 | comparison_type: numeric
205 | comparison: isGreaterThan
206 | value: 18
207 | main_user.country:
208 | comparison_type: string
209 | comparison: isEqual
210 | value: FR
211 | -
212 | attributes:
213 | main_user.age:
214 | comparison_type: numeric
215 | comparison: isGreaterThan
216 | value: 21
217 | main_user.country:
218 | comparison_type: string
219 | comparison: isNotEqual
220 | value: FR
221 |
222 | ```
223 |
224 | And then the code :
225 |
226 | ```php
227 | enforce('alcoollaw', $user);
235 | ```
236 |
237 | **Example with rules root directory passed to Abac class.**
238 | This feature allow to give a policy definition rules directory path directly to the Abac class without adding to all files :
239 |
240 | Considering we have 3 yaml files :
241 | - rest/conf/policy/user_def.yml
242 | - rest/conf/policy/gunlaw.yml
243 |
244 | The php code can be :
245 | ```php
246 | enforce('gunlaw', $user);
255 |
256 | ```
257 |
258 | Contribute
259 | ----------
260 |
261 | If you want to contribute, don't hesitate to fork the library and submit Pull Requests.
262 |
263 | You can also report issues, suggest enhancements, feel free to give advices and your feedback about this library.
264 |
265 | It's not finished yet, there's still a lot of features to implement to make it better. If you want to be a part of this library improvement, let us know !
266 |
267 | See also
268 | --------
269 |
270 | * [Symfony bundle to support this library](https://github.com/CraftCamp/abac-bundle)
271 |
--------------------------------------------------------------------------------
/doc/configuration.md:
--------------------------------------------------------------------------------
1 | Configuration
2 | =============
3 |
4 | When you initialize the PHP ABAC library, you can pass multiple configuration files as arguments.
5 |
6 | These files will be parsed and the data will be extracted.
7 |
8 | This way, you can avoid long configuration files in your application and use several files instead.
9 |
10 | The configurations will be merged.
11 |
12 |
13 | ```php
14 | enforce('vehicle-homologation', $user, $vehicle);
23 | ```
24 |
25 | Configuration file can be yaml or json files, and format can be mixed.
26 |
27 | ```php
28 | enforce('vehicle-homologation', $user, $vehicle);
37 | ```
38 |
39 | If all configuration file are in the same folder, you can add this folder in 3th paramter of Abac contructor.
40 |
41 | ```php
42 | enforce('vehicle-homologation', $user, $vehicle);
51 | ```
52 |
53 | Configuration Options
54 | ---------------------
55 | Abac constructor allow a 4th parameter called options :
56 | ```php
57 | public function __construct( $configPaths, $cacheOptions = [], $configPaths_root = null, $options = [] );
58 | ```
59 |
60 | This parameter must be an array and can contains this options :
61 | - getter_prefix (default='get') : Prefix to add before getter name
62 | - getter_name_transformation_function (default='ucfirst') : Function to apply on the getter name ( before adding prefix )
63 |
64 |
65 | Attributes
66 | ----------
67 |
68 | Attributes are object properties mapped to be used in the rule check.
69 |
70 | The are two types of attributes :
71 |
72 | * **Object attributes**: these are the fields of your users and resources classes, like described above.
73 | To declare an object property as an attribute, it must have a getter. For example, an user with an ``$age`` property must have a ``getAge()`` public method.
74 | * **Environment attributes**: These attributes are accessed with the [``getenv()``](http://php.net/manual/fr/function.getenv.php) PHP native function.
75 | It allows your rules to check environment variables along with the object attributes.
76 |
77 | This is an example of configured attributes in a YAML file :
78 |
79 | ```yaml
80 | ---
81 | attributes:
82 | main_user:
83 | class: PhpAbac\Example\User
84 | type: user
85 | fields:
86 | age:
87 | name: Age
88 | parentNationality:
89 | name: Nationalité des parents
90 | hasDoneJapd:
91 | name: JAPD
92 | hasDrivingLicense:
93 | name: Permis de conduire
94 |
95 | vehicle:
96 | class: PhpAbac\Example\Vehicle
97 | type: resource
98 | fields:
99 | origin:
100 | name: Origine
101 | owner.id:
102 | name: Propriétaire
103 | manufactureDate:
104 | name: Date de sortie d'usine
105 | lastTechnicalReviewDate:
106 | name: Dernière révision technique
107 |
108 | environment:
109 | service_state:
110 | name: Statut du service
111 | variable_name: SERVICE_STATE
112 | ```
113 |
114 | The ```class``` key is not used yet, but will be used soon to make a single rule securing different resources.
115 |
116 | The ```type``` key has two values : ``user`` and ``resource``.
117 |
118 | Rules
119 | -----
120 |
121 | To define a rule, you must give it a name, and configure the checked attributes.
122 |
123 | The attributes are already defined, you just have to link it to your rules,
124 |
125 | and add data about the comparison which will be performed by the library to determine if the user have access to the given resource.
126 |
127 | For example, you can have the following configuration :
128 |
129 | ```yaml
130 | ---
131 | rules:
132 | vehicle-homologation:
133 | attributes:
134 | main_user.hasDrivingLicense:
135 | comparison_type: boolean
136 | comparison: boolAnd
137 | value: true
138 | vehicle.lastTechnicalReviewDate:
139 | comparison_type: datetime
140 | comparison: isMoreRecentThan
141 | value: -2Y
142 | vehicle.manufactureDate:
143 | comparison_type: datetime
144 | comparison: isMoreRecentThan
145 | value: -25Y
146 | vehicle.origin:
147 | comparison_type: array
148 | comparison: isIn
149 | value: ["FR", "DE", "IT", "L", "GB", "P", "ES", "NL", "B"]
150 | environment.service_state:
151 | comparison_type: string
152 | comparison: isEqual
153 | value: OPEN
154 | ```
155 |
156 | A [list](comparisons.md) of the available comparisons is created and will be updated with new comparisons.
157 |
158 | Extra Data
159 | ===========
160 |
161 | Sometimes, you will have to do more complex comparisons.
162 |
163 | The basic configuration will not be sufficient to perform these comparisons.
164 |
165 | There is more advanced configuration properties available to make it.
166 |
167 | For example, we want to check if an user has a visa to travel to Germany.
168 |
169 | Each user can have several visas, we need to check that the visas collection contains a visa with the proper attributes :
170 |
171 | ```yaml
172 | # abac_config.yml
173 | attributes:
174 | visa:
175 | class: PhpAbac\Example\Visa
176 | type: resource
177 | fields:
178 | country:
179 | name: Pays
180 | lastRenewal:
181 | name: Dernier renouvellement
182 | rules:
183 | travel-to-germany:
184 | attributes:
185 | main_user.visas:
186 | comparison_type: array
187 | comparison: contains
188 | with:
189 | visa.country:
190 | comparison_type: string
191 | comparison: isEqual
192 | value: DE
193 | visa.lastRenewal:
194 | comparison_type: datetime
195 | comparison: isMoreRecentThan
196 | value: -1Y
197 | ```
198 |
199 | There is no value configured for the attribute, but a ``with`` property.
200 |
201 | This property contains an array of attributes to check.
202 |
203 | Then you can use ABAC the same way as before :
204 |
205 | ```php
206 | $isGranted = $abac->enforce('travel-to-germany', $user);
207 | ```
208 |
209 | Chained Attributes
210 | ==================
211 |
212 | If you want to check an attribute which is an already configured attribute property, you can use a special syntax to declare chained attributes.
213 |
214 | With the previous extra data example, let's create a Country model class, which is a Visa property.
215 |
216 | The previous configuration would be updated to :
217 |
218 | ```yaml
219 | # abac_config.yml
220 | attributes:
221 | visa:
222 | class: PhpAbac\Example\Visa
223 | type: resource
224 | fields:
225 | country.code:
226 | name: Code Pays
227 | lastRenewal:
228 | name: Dernier renouvellement
229 |
230 | country:
231 | class: PhpAbac\Example\Country
232 | type: resource
233 | fields:
234 | code:
235 | name: Code
236 |
237 | rules:
238 | travel-to-germany:
239 | attributes:
240 | main_user.visas:
241 | comparison_type: array
242 | comparison: contains
243 | with:
244 | visa.country.code:
245 | comparison_type: string
246 | comparison: isEqual
247 | value: DE
248 | visa.lastRenewal:
249 | comparison_type: datetime
250 | comparison: isMoreRecentThan
251 | value: -1Y
252 | ```
253 |
254 | This way, the library will perform something similar to :
255 |
256 | ```php
257 | $visa->getCountry()->getCode() === 'DE';
258 | ```
259 |
260 | Multiple Attributes rules for an unique named rule.
261 | ===================================================
262 | The first rules that return allow acces stop the check process and return true.
263 |
264 | If we update the previous configuration to :
265 | ```yaml
266 | attributes:
267 | main_user:
268 | class: PhpAbac\Example\User
269 | type: user
270 | fields:
271 | age:
272 | name: Age
273 | parentNationality:
274 | name: Nationalité des parents
275 | hasDoneJapd:
276 | name: JAPD
277 | hasDrivingLicense:
278 | name: Permis de conduire
279 | countryCode:
280 | name: ISO code du pays
281 |
282 | rules:
283 | travel-to-germany:
284 | # First test, User is a German User ?
285 | -
286 | attributes:
287 | main_user.countryCode:
288 | comparison_type: string
289 | comparison: isEqual
290 | value: DE
291 | # Or Second test, User have a visa for Germany
292 | -
293 | attributes:
294 | main_user.visas:
295 | comparison_type: array
296 | comparison: contains
297 | with:
298 | visa.country.code:
299 | comparison_type: string
300 | comparison: isEqual
301 | value: DE
302 | visa.lastRenewal:
303 | comparison_type: datetime
304 | comparison: isMoreRecentThan
305 | value: -1Y
306 | ```
307 |
308 |
309 | Import property
310 | =================
311 |
312 | The better way to define all attributes and rules is to make each definition in a specific file. Is more convenient to understand each rule an each objet{resource/user} definition.
313 |
314 | file : users/main_user.yml
315 | ```yaml
316 | ---
317 | attributes:
318 | main_user:
319 | class: PhpAbac\Example\User
320 | type: user
321 | fields:
322 | id:
323 | name: ID
324 | age:
325 | name: Age
326 | ```
327 |
328 |
329 | file : travel-to-foreign-country.yml
330 | ```yaml
331 | ---
332 | '@import':
333 | - users/main_user.yml
334 |
335 | rules:
336 | travel:
337 | attributes:
338 | main_user.age:
339 | comparison_type: numeric
340 | comparison: isGreaterThan
341 | value: 18
342 | ```
343 |
344 |
345 |
346 | Used Getter extended paramters
347 | ==============================
348 |
349 | Sometimes, you need to call getter with parameters.
350 |
351 | it's possible by adding getter_params list in attributes rules specification.
352 |
353 | ```yaml
354 | ---
355 | rules:
356 | travel-to-foreign-country:
357 | attributes:
358 | main_user.age:
359 | comparison_type: numeric
360 | comparison: isGreaterThan
361 | value: 18
362 | main_user.visa:
363 | comparison_type: array
364 | comparison: contains
365 | getter_params:
366 | visa:
367 | -
368 | param_name: '@country_code'
369 | param_value: country.code
370 | # The executed code will be : $main_user->getVisa($country->getCode)
371 | # If you want only simple value, remove @ in param_name value.
372 | with:
373 | visa.lastRenewal:
374 | comparison_type: datetime
375 | comparison: isMoreRecentThan
376 | value: -1Y
377 | ```
--------------------------------------------------------------------------------