├── 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 | [![Latest Stable Version](https://poser.pugx.org/craftcamp/php-abac/v/stable)](https://packagist.org/packages/craftcamp/php-abac) 7 | [![Latest Unstable Version](https://poser.pugx.org/craftcamp/php-abac/v/unstable)](https://packagist.org/packages/craftcamp/php-abac) 8 | [![Build Status](https://travis-ci.org/CraftCamp/php-abac.svg?branch=master)](https://travis-ci.org/CraftCamp/php-abac) 9 | [![Code Coverage](https://scrutinizer-ci.com/g/CraftCamp/php-abac/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/CraftCamp/php-abac/?branch=master) 10 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/CraftCamp/php-abac/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/CraftCamp/php-abac/?branch=master) 11 | [![Total Downloads](https://poser.pugx.org/craftcamp/php-abac/downloads)](https://packagist.org/packages/craftcamp/php-abac) 12 | [![License](https://poser.pugx.org/craftcamp/php-abac/license)](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 | ``` --------------------------------------------------------------------------------