├── .github └── workflows │ └── php.yml ├── .gitignore ├── .scrutinizer.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── app ├── composer_autoloader.php └── slim_rbac.php ├── bin └── slim_rbac ├── codeception.yml ├── composer.json ├── composer.lock ├── config └── sr_config.example.yaml ├── docker-compose.yml ├── migrations ├── 20171209082325_create_role.php ├── 20171210103530_create_permission.php ├── 20171210104049_create_role_permission.php ├── 20171210104724_create_user_role.php ├── 20171210195252_create_role_hierarchy.php ├── 20171224111307_add_description_field_for_role.php └── 20171224111730_add_description_field_for_permission.php ├── src ├── Component │ ├── ComponentsFactory.php │ ├── Config │ │ ├── RbacConfig.php │ │ ├── RbacConfigLoader.php │ │ └── RbacConfigStructure.php │ ├── PermissionNameExtractor │ │ ├── PermissionNameExtractor.php │ │ └── UriPathPermissionNameExtractor.php │ ├── RbacAccessChecker.php │ ├── RbacContainer.php │ ├── RbacManager.php │ ├── RbacMiddleware.php │ ├── UserIdExtractor │ │ ├── AttributeUserIdExtractor.php │ │ ├── BaseUserIdExtractor.php │ │ ├── CookieUserIdExtractor.php │ │ ├── HeaderUserIdExtractor.php │ │ └── UserIdExtractor.php │ └── services.yaml ├── Console │ ├── Command │ │ ├── BaseDatabaseCommand.php │ │ ├── CreateConfigCommand.php │ │ ├── MigrateDatabaseCommand.php │ │ └── RollbackDatabaseCommand.php │ └── SlimRbacConsoleApplication.php ├── Exception │ ├── BaseException.php │ ├── ConfigNotFoundException.php │ ├── CyclicException.php │ ├── DatabaseException.php │ ├── InvalidArgumentException.php │ ├── NotSupportedDatabaseException.php │ ├── NotUniqueException.php │ └── PermissionNotFoundException.php └── Models │ ├── Entity │ ├── Permission.php │ ├── Role.php │ ├── RoleHierarchy.php │ ├── RolePermission.php │ └── UserRole.php │ ├── Repository │ ├── PermissionRepository.php │ ├── RoleHierarchyRepository.php │ ├── RolePermissionRepository.php │ ├── RoleRepository.php │ └── UserRoleRepository.php │ └── RepositoryRegistry.php └── tests ├── _data └── .gitkeep ├── _output └── .gitignore ├── _support ├── AcceptanceTester.php ├── FunctionalTester.php ├── Helper │ ├── Acceptance.php │ ├── Functional.php │ └── Unit.php ├── UnitTester.php └── _generated │ └── .gitignore ├── unit.suite.yml └── unit ├── BaseTestCase.php ├── RbacManagerTest.php └── RbacMiddlewareTest.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Build and Testing 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | mysql: 16 | image: mysql:5.7 17 | env: 18 | MYSQL_ALLOW_EMPTY_PASSWORD: false 19 | MYSQL_ROOT_PASSWORD: password 20 | MYSQL_DATABASE: db 21 | ports: 22 | - 3306/tcp 23 | options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | 28 | - name: Setup PHP with PECL extension 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: '7.3' 32 | coverage: xdebug2 33 | 34 | - name: Validate composer.json and composer.lock 35 | run: composer validate --strict 36 | 37 | - name: Get composer cache directory 38 | id: composer-cache 39 | run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT 40 | 41 | - name: Cache dependencies 42 | uses: actions/cache@v3 43 | with: 44 | path: ${{ steps.composer-cache.outputs.dir }} 45 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 46 | restore-keys: ${{ runner.os }}-composer- 47 | 48 | - name: Install dependencies 49 | run: composer install --prefer-dist --no-progress 50 | 51 | - name: Create config file 52 | run: sed -e "s/3306/${{ job.services.mysql.ports['3306'] }}/" ./config/sr_config.example.yaml > ./config/sr_config.yaml 53 | 54 | - name: Run migrations 55 | run: ./bin/slim_rbac migrate 56 | 57 | - name: Run tests 58 | run: | 59 | vendor/bin/codecept run --coverage --coverage-xml 60 | cp tests/_output/coverage.xml ./coverage.xml 61 | cp tests/_output/coverage.serialized ./coverage.serialized 62 | 63 | - name: Upload coverage to Codecov 64 | uses: codecov/codecov-action@v3 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Exclude vendor packages 2 | vendor/* 3 | 4 | # Exclude PhpStorm files 5 | .idea 6 | 7 | # Exclude Phinx settings 8 | phinx.yml 9 | 10 | # Exclude configurations file 11 | config/sr_config.yaml -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | checks: 2 | php: true 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at potievdev@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | ## Pull Requests 4 | 5 | 1. Fork the RBAC Middleware repository 6 | 2. Create a new branch for each feature or improvement 7 | 3. Send a pull request from each feature branch against the version branch for which your fix is intended. 8 | 9 | It is very important to separate new features or improvements into separate feature branches, and to send a 10 | pull request for each branch. This allows each feature or improvement to be reviewed and merged individually. 11 | 12 | ## Style Guide 13 | 14 | All pull requests must adhere to the [PSR-2 standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md). 15 | 16 | ## Unit Testing 17 | 18 | All pull requests must be accompanied by passing unit tests and complete code coverage. The Slim Framework uses phpunit for testing. 19 | 20 | [Learn about PHPUnit](https://github.com/sebastianbergmann/phpunit/) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

Slim4 RBAC Middleware

3 |
4 | 5 | [![Build Status](https://app.travis-ci.com/potievdev/slim-rbac.svg?branch=master)](https://app.travis-ci.com/potievdev/slim-rbac) 6 | [![codecov](https://codecov.io/gh/potievdev/slim-rbac/branch/master/graph/badge.svg)](https://codecov.io/gh/potievdev/slim-rbac) 7 | [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fpotievdev%2Fslim-rbac.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fpotievdev%2Fslim-rbac?ref=badge_shield) 8 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/potievdev/slim-rbac/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/potievdev/slim-rbac/?branch=master) 9 | [![Build Status](https://scrutinizer-ci.com/g/potievdev/slim-rbac/badges/build.png?b=master)](https://scrutinizer-ci.com/g/potievdev/slim-rbac/build-status/master) 10 | [![Total Downloads](https://poser.pugx.org/potievdev/slim-rbac/downloads)](https://packagist.org/packages/potievdev/slim-rbac) 11 | 12 | This package helps you to release access control logic via [RBAC](https://en.wikipedia.org/wiki/Role-based_access_control) (Role Based Access Control) technology. The example app you can see [https://github.com/potievdev/slim-rbac-app](https://github.com/potievdev/slim-rbac-app) 13 | 14 | ## :clipboard: Requirements 15 | 16 | - The minimum required PHP version is PHP 7.3. 17 | - Supported database engines 18 | * MySQL 19 | * PostgreSQL 20 | * MariaDB 21 | 22 | ## :wrench: Installation 23 | 24 | ### First step 25 | 26 | ```sh 27 | $ composer require potievdev/slim_rbac "^2.0" 28 | ``` 29 | 30 | ## :crossed_flags: Contribution 31 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 32 | 33 | ## :memo: Licence 34 | MIT License 35 | -------------------------------------------------------------------------------- /app/composer_autoloader.php: -------------------------------------------------------------------------------- 1 | run(); 6 | -------------------------------------------------------------------------------- /codeception.yml: -------------------------------------------------------------------------------- 1 | paths: 2 | tests: tests 3 | output: tests/_output 4 | data: tests/_data 5 | support: tests/_support 6 | envs: tests/_envs 7 | actor_suffix: Tester 8 | extensions: 9 | enabled: 10 | - Codeception\Extension\RunFailed 11 | coverage: 12 | enabled: true 13 | include: 14 | - src/Component/* 15 | - src/Models/* 16 | exclude: 17 | - src/Models/Entity/* 18 | - src/Models/Repository/* 19 | - src/Component/services.yaml -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "potievdev/slim-rbac", 3 | "description": "Role Based Access Control middleware for Slim 3", 4 | "keywords": [ 5 | "slim framework", 6 | "slim 3", 7 | "middleware", 8 | "authentication", 9 | "authorization", 10 | "rbac", 11 | "potievdev" 12 | ], 13 | "homepage": "https://github.com/potievdev/slim-rbac", 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Abdulmalik Abdulpotiev", 18 | "email": "potievdev@gmail.com", 19 | "homepage": "https://potievdev.com/", 20 | "role": "Developer" 21 | } 22 | ], 23 | "require": { 24 | "php" : "~7.3", 25 | "robmorgan/phinx": "~0.9.2", 26 | "doctrine/orm": "~2.10.5", 27 | "psr/http-message": "~1.0", 28 | "symfony/cache": "~5.4.21", 29 | "symfony/dependency-injection": "^4.4", 30 | "doctrine/annotations": "^1.14" 31 | }, 32 | "require-dev": { 33 | "codeception/codeception": "3.1.3" 34 | }, 35 | "autoload": { 36 | "psr-4": { 37 | "Potievdev\\SlimRbac\\": "src" 38 | } 39 | }, 40 | "autoload-dev": { 41 | "psr-4": { 42 | "Tests\\Unit\\": "tests/unit" 43 | } 44 | }, 45 | "bin": [ 46 | "bin/slim_rbac" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /config/sr_config.example.yaml: -------------------------------------------------------------------------------- 1 | rbac: 2 | userId: 3 | fieldName: userId 4 | resourceType: attribute 5 | database: 6 | driver: pdo_mysql 7 | host: 127.0.0.1 8 | user: root 9 | password: password 10 | port: 3306 11 | dbname: db 12 | charset: utf8 13 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | services: 3 | mysql: 4 | image: mysql:5.7 5 | restart: always 6 | environment: 7 | MYSQL_DATABASE: 'db' 8 | MYSQL_ROOT_PASSWORD: 'password' 9 | ports: 10 | - '3306:3306' 11 | expose: 12 | - '3306' 13 | volumes: 14 | - mysql-db:/var/lib/mysql 15 | 16 | postgres: 17 | image: postgres:latest 18 | environment: 19 | POSTGRES_USER: root 20 | POSTGRES_PASSWORD: password 21 | POSTGRES_DB: db 22 | ports: 23 | - "5432:5432" 24 | expose: 25 | - '5432' 26 | volumes: 27 | - postgres-db:/var/lib/postgresql/data/ 28 | 29 | volumes: 30 | mysql-db: 31 | postgres-db: -------------------------------------------------------------------------------- /migrations/20171209082325_create_role.php: -------------------------------------------------------------------------------- 1 | table('role', ['signed' => false]); 17 | 18 | $roleTable->addColumn('name', 'string', ['limit' => 50]) 19 | ->addColumn('status', 'boolean', ['default' => true]) 20 | ->addColumn('created_at', 'datetime') 21 | ->addColumn('updated_at', 'datetime', ['null' => true]) 22 | ->addIndex('name', ['name' => 'idx_role_name', 'unique' => true]) 23 | ->create(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /migrations/20171210103530_create_permission.php: -------------------------------------------------------------------------------- 1 | table('permission', ['signed' => false]); 17 | 18 | $permissionTable->addColumn('name', 'string', ['limit' => 100]) 19 | ->addColumn('status', 'boolean', ['default' => true]) 20 | ->addColumn('created_at', 'datetime') 21 | ->addColumn('updated_at', 'datetime', ['null' => true]) 22 | ->addIndex('name', ['name' => 'idx_permission_name' ,'unique' => true]) 23 | ->create(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /migrations/20171210104049_create_role_permission.php: -------------------------------------------------------------------------------- 1 | table('role_permission', ['signed' => false]); 17 | 18 | $rolePermissionTable->addColumn('role_id', 'integer', ['signed' => false]) 19 | ->addColumn('permission_id', 'integer', ['signed' => false]) 20 | ->addColumn('created_at', 'datetime') 21 | ->addIndex(['role_id', 'permission_id'], ['name' => 'idx_role_permission_unique', 'unique' => true]) 22 | ->addForeignKey('role_id', 'role', 'id', ['delete' => 'RESTRICT', 'constraint' => 'fk_role_permission_role']) 23 | ->addForeignKey('permission_id', 'permission', 'id', ['delete' => 'RESTRICT', 'constraint' => 'fk_role_permission_permission']) 24 | ->create(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /migrations/20171210104724_create_user_role.php: -------------------------------------------------------------------------------- 1 | table('user_role', ['signed' => false]); 17 | 18 | $userRoleTable->addColumn('user_id', 'integer', ['signed' => false]) 19 | ->addColumn('role_id', 'integer', ['signed' => false]) 20 | ->addColumn('created_at', 'datetime') 21 | ->addIndex(['user_id', 'role_id'], ['name' => 'idx_user_role_unique', 'unique' => true]) 22 | ->addForeignKey('role_id', 'role', 'id', ['delete' => 'RESTRICT', 'constraint' => 'fk_user_role_role']) 23 | ->create(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /migrations/20171210195252_create_role_hierarchy.php: -------------------------------------------------------------------------------- 1 | table('role_hierarchy', ['signed' => false]); 15 | 16 | $userRoleTable->addColumn('parent_role_id', 'integer', ['signed' => false]) 17 | ->addColumn('child_role_id', 'integer', ['signed' => false]) 18 | ->addColumn('created_at', 'datetime') 19 | ->addIndex(['parent_role_id', 'child_role_id'], ['name' => 'idx_role_hierarchy_unique', 'unique' => true]) 20 | ->addForeignKey('parent_role_id', 'role', 'id', ['delete' => 'RESTRICT', 'constraint' => 'fk_role_hierarchy_parent']) 21 | ->addForeignKey('child_role_id', 'role', 'id', ['delete' => 'RESTRICT', 'constraint' => 'fk_role_hierarchy_child']) 22 | ->create(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /migrations/20171224111307_add_description_field_for_role.php: -------------------------------------------------------------------------------- 1 | table('role'); 32 | 33 | $roleTable 34 | ->addColumn('description', 'string', ['null' => true, 'comment' => 'description of role']) 35 | ->update(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /migrations/20171224111730_add_description_field_for_permission.php: -------------------------------------------------------------------------------- 1 | table('permission'); 32 | 33 | $permissionTable 34 | ->addColumn('description', 'string', ['null' => true, 'comment' => 'description of permission']) 35 | ->update(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Component/ComponentsFactory.php: -------------------------------------------------------------------------------- 1 | $rbacConfig->getDatabaseDriver(), 19 | 'host' => $rbacConfig->getDatabaseHost(), 20 | 'user' => $rbacConfig->getDatabaseUser(), 21 | 'password' => $rbacConfig->getDatabasePassword(), 22 | 'dbname' => $rbacConfig->getDatabaseName(), 23 | 'port' => $rbacConfig->getDatabasePort(), 24 | ]; 25 | 26 | $config = Setup::createAnnotationMetadataConfiguration([], false, null, null, false); 27 | 28 | return EntityManager::create($dbParams, $config); 29 | } 30 | 31 | public static function createUserIdExtractor(RbacConfig $rbacConfig): UserIdExtractor 32 | { 33 | $userIdFieldName = $rbacConfig->getUserIdFieldName(); 34 | $storageType = $rbacConfig->getUserIdResourceType(); 35 | 36 | /** @var integer $userId */ 37 | switch ($storageType) { 38 | 39 | case RbacConfig::HEADER_RESOURCE_TYPE: 40 | return new HeaderUserIdExtractor($userIdFieldName); 41 | 42 | case RbacConfig::COOKIE_RESOURCE_TYPE: 43 | return new CookieUserIdExtractor($userIdFieldName); 44 | 45 | case RbacConfig::ATTRIBUTE_RESOURCE_TYPE: 46 | default: 47 | return new AttributeUserIdExtractor($userIdFieldName); 48 | } 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /src/Component/Config/RbacConfig.php: -------------------------------------------------------------------------------- 1 | databaseDriver = $databaseDriver; 63 | $this->databaseHost = $databaseHost; 64 | $this->databaseUser = $databaseUser; 65 | $this->databasePassword = $databasePassword; 66 | $this->databasePort = $databasePort; 67 | $this->databaseName = $databaseName; 68 | $this->databaseCharset = $databaseCharset; 69 | $this->userIdFieldName = $userIdFieldName; 70 | $this->userIdResourceType = $userIdResourceType; 71 | } 72 | 73 | /** 74 | * @throws ConfigNotFoundException 75 | */ 76 | public static function createFromConfigFile(): RbacConfig 77 | { 78 | return self::createFromConfigs(RbacConfigLoader::loadConfigs()); 79 | } 80 | 81 | public static function createFromConfigs(array $configs): RbacConfig 82 | { 83 | return new self( 84 | $configs['database']['driver'], 85 | $configs['database']['host'], 86 | $configs['database']['user'], 87 | $configs['database']['password'], 88 | $configs['database']['port'], 89 | $configs['database']['dbname'], 90 | $configs['database']['charset'], 91 | $configs['userId']['fieldName'], 92 | $configs['userId']['resourceType'], 93 | ); 94 | } 95 | 96 | /** 97 | * @return string 98 | */ 99 | public function getDatabaseDriver(): string 100 | { 101 | return $this->databaseDriver; 102 | } 103 | 104 | /** 105 | * @return string 106 | */ 107 | public function getDatabaseHost(): string 108 | { 109 | return $this->databaseHost; 110 | } 111 | 112 | /** 113 | * @return string 114 | */ 115 | public function getDatabaseUser(): string 116 | { 117 | return $this->databaseUser; 118 | } 119 | 120 | /** 121 | * @return string 122 | */ 123 | public function getDatabasePassword(): string 124 | { 125 | return $this->databasePassword; 126 | } 127 | 128 | /** 129 | * @return int 130 | */ 131 | public function getDatabasePort(): int 132 | { 133 | return $this->databasePort; 134 | } 135 | 136 | /** 137 | * @return string 138 | */ 139 | public function getDatabaseName(): string 140 | { 141 | return $this->databaseName; 142 | } 143 | 144 | /** 145 | * @return string 146 | */ 147 | public function getDatabaseCharset(): string 148 | { 149 | return $this->databaseCharset; 150 | } 151 | 152 | /** 153 | * @return string 154 | */ 155 | public function getUserIdFieldName(): string 156 | { 157 | return $this->userIdFieldName; 158 | } 159 | 160 | /** 161 | * @return string 162 | */ 163 | public function getUserIdResourceType(): string 164 | { 165 | return $this->userIdResourceType; 166 | } 167 | 168 | } -------------------------------------------------------------------------------- /src/Component/Config/RbacConfigLoader.php: -------------------------------------------------------------------------------- 1 | locate('sr_config.yaml'); 25 | 26 | if ($fileName === null) { 27 | throw ConfigNotFoundException::configFileNotFound($configDirectories); 28 | } 29 | 30 | return (new Processor()) 31 | ->processConfiguration(new RbacConfigStructure(), Yaml::parseFile($fileName)); 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /src/Component/Config/RbacConfigStructure.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 17 | 18 | $rootNode 19 | ->children() 20 | ->arrayNode('database') 21 | ->children() 22 | ->enumNode('driver') 23 | ->values(['pdo_mysql', 'pdo_postgres']) 24 | ->isRequired() 25 | ->end() 26 | ->scalarNode('host') 27 | ->cannotBeEmpty() 28 | ->end() 29 | ->scalarNode('user') 30 | ->cannotBeEmpty() 31 | ->end() 32 | ->scalarNode('password') 33 | ->cannotBeEmpty() 34 | ->end() 35 | ->integerNode('port') 36 | ->isRequired() 37 | ->end() 38 | ->scalarNode('dbname') 39 | ->cannotBeEmpty() 40 | ->end() 41 | ->scalarNode('charset') 42 | ->cannotBeEmpty() 43 | ->end() 44 | ->end() 45 | ->end() 46 | ->arrayNode('userId') 47 | ->children() 48 | ->scalarNode('fieldName') 49 | ->cannotBeEmpty() 50 | ->end() 51 | ->enumNode('resourceType') 52 | ->values(['attribute', 'header', 'cookie']) 53 | ->isRequired() 54 | ->end() 55 | ->end() 56 | ->end() 57 | ->end() 58 | ; 59 | 60 | return $treeBuilder; 61 | } 62 | } -------------------------------------------------------------------------------- /src/Component/PermissionNameExtractor/PermissionNameExtractor.php: -------------------------------------------------------------------------------- 1 | getUri()->getPath(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/Component/RbacAccessChecker.php: -------------------------------------------------------------------------------- 1 | repositoryRegistry = $repositoryRegistry; 19 | } 20 | 21 | /** 22 | * Checks access status. 23 | * 24 | * @throws QueryException 25 | */ 26 | public function hasAccess(string $userId, string $permissionName): bool 27 | { 28 | /** @var integer $permissionId */ 29 | $permissionId = $this->repositoryRegistry 30 | ->getPermissionRepository() 31 | ->getPermissionIdByName($permissionName); 32 | 33 | if ($permissionId === null) { 34 | return false; 35 | } 36 | 37 | /** @var integer[] $rootRoleIds */ 38 | $rootRoleIds = $this->repositoryRegistry 39 | ->getUserRoleRepository() 40 | ->getUserRoleIds($userId); 41 | 42 | if (count($rootRoleIds) == 0) { 43 | return false; 44 | } 45 | 46 | $allRoleIds = $this->repositoryRegistry 47 | ->getRoleHierarchyRepository() 48 | ->getAllRoleIdsHierarchy($rootRoleIds); 49 | 50 | return $this->repositoryRegistry 51 | ->getRolePermissionRepository() 52 | ->isPermissionAssigned($permissionId, $allRoleIds); 53 | } 54 | } -------------------------------------------------------------------------------- /src/Component/RbacContainer.php: -------------------------------------------------------------------------------- 1 | containerBuilder = new ContainerBuilder(); 25 | 26 | if (isset($rbacConfig)) { 27 | $this->containerBuilder->set('rbacConfig', $rbacConfig); 28 | } 29 | 30 | $loader = new YamlFileLoader($this->containerBuilder, new FileLocator(__DIR__)); 31 | $loader->load('services.yaml'); 32 | } 33 | 34 | public function getRbacMiddleware(): RbacMiddleware 35 | { 36 | return $this->containerBuilder->get('middleware'); 37 | } 38 | 39 | public function getRbacManager(): RbacManager 40 | { 41 | return $this->containerBuilder->get('manager'); 42 | } 43 | 44 | public function getInnerContainer(): ContainerBuilder 45 | { 46 | return $this->containerBuilder; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Component/RbacManager.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 40 | $this->repositoryRegistry = $repositoryRegistry; 41 | } 42 | 43 | /** 44 | * Creates permission instance with given name and return it. 45 | * @throws NotUniqueException|DatabaseException|ORMException 46 | */ 47 | public function createPermission(string $permissionMane, ?string $description = null): Permission 48 | { 49 | $permission = new Permission(); 50 | $permission->setName($permissionMane); 51 | 52 | if (isset($description)) { 53 | $permission->setDescription($description); 54 | } 55 | 56 | try { 57 | $this->saveEntity($permission); 58 | } catch (UniqueConstraintViolationException $e) { 59 | throw NotUniqueException::permissionWithNameAlreadyCreated($permissionMane); 60 | } 61 | 62 | return $permission; 63 | } 64 | 65 | /** 66 | * Creates role instance with given name and return it. 67 | * 68 | * @throws NotUniqueException|DatabaseException|ORMException 69 | */ 70 | public function createRole(string $roleName, ?string $description = null): Role 71 | { 72 | $role = new Role(); 73 | $role->setName($roleName); 74 | 75 | if (isset($description)) { 76 | $role->setDescription($description); 77 | } 78 | 79 | try { 80 | $this->saveEntity($role); 81 | } catch (UniqueConstraintViolationException $e) { 82 | throw NotUniqueException::notUniqueRole($roleName); 83 | } 84 | 85 | return $role; 86 | } 87 | 88 | /** 89 | * Add permission to role. 90 | * 91 | * @throws DatabaseException 92 | * @throws NotUniqueException|ORMException 93 | */ 94 | public function attachPermission(Role $role, Permission $permission) 95 | { 96 | $rolePermission = new RolePermission(); 97 | 98 | $rolePermission->setPermission($permission); 99 | $rolePermission->setRole($role); 100 | 101 | try { 102 | $this->saveEntity($rolePermission); 103 | } catch (UniqueConstraintViolationException $e) { 104 | throw NotUniqueException::permissionAlreadyAttachedToRole($permission->getName(), $role->getName()); 105 | } 106 | } 107 | 108 | /** 109 | * Add child role to role. 110 | * 111 | * @throws CyclicException 112 | * @throws DatabaseException 113 | * @throws NotUniqueException 114 | * @throws QueryException|ORMException 115 | */ 116 | public function attachChildRole(Role $parentRole, Role $childRole) 117 | { 118 | $roleHierarchy = new RoleHierarchy(); 119 | 120 | $roleHierarchy->setParentRole($parentRole); 121 | $roleHierarchy->setChildRole($childRole); 122 | 123 | $this->checkForCyclicHierarchy($childRole->getId(), $parentRole->getId()); 124 | 125 | try { 126 | $this->saveEntity($roleHierarchy); 127 | } catch (UniqueConstraintViolationException $e) { 128 | throw NotUniqueException::childRoleAlreadyAttachedToGivenParentRole( 129 | $childRole->getName(), 130 | $parentRole->getName() 131 | ); 132 | } 133 | } 134 | 135 | /** 136 | * Assign role to user. 137 | * 138 | * @throws NotUniqueException 139 | * @throws DatabaseException|ORMException 140 | */ 141 | public function assignRoleToUser(Role $role, int $userId) 142 | { 143 | $userRole = new UserRole(); 144 | 145 | $userRole->setUserId($userId); 146 | $userRole->setRole($role); 147 | 148 | try { 149 | $this->saveEntity($userRole); 150 | } catch (UniqueConstraintViolationException $e) { 151 | throw NotUniqueException::roleAlreadyAssignedToUser($role->getName(), $userId); 152 | } 153 | } 154 | 155 | /** 156 | * Checking hierarchy cyclic line. 157 | * 158 | * @throws CyclicException 159 | * @throws QueryException 160 | */ 161 | private function checkForCyclicHierarchy(int $parentRoleId, int $childRoleId): void 162 | { 163 | $result = $this->repositoryRegistry 164 | ->getRoleHierarchyRepository() 165 | ->hasChildRoleId($parentRoleId, $childRoleId); 166 | 167 | if ($result === true) { 168 | throw CyclicException::cycleDetected($parentRoleId, $childRoleId); 169 | } 170 | } 171 | 172 | /** 173 | * Insert or update entity. 174 | * 175 | * @throws DatabaseException|ORMException 176 | */ 177 | private function saveEntity(object $entity): void 178 | { 179 | try { 180 | $this->entityManager->persist($entity); 181 | $this->entityManager->flush($entity); 182 | } catch (OptimisticLockException $e) { 183 | throw new DatabaseException($e->getMessage()); 184 | } 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /src/Component/RbacMiddleware.php: -------------------------------------------------------------------------------- 1 | accessChecker = $accessChecker; 41 | $this->userIdExtractor = $userIdExtractor; 42 | $this->permissionNameExtractor = $permissionNameExtractor; 43 | } 44 | 45 | /** 46 | * Check access. 47 | * 48 | * @param ServerRequestInterface $request PSR7 request 49 | * @param ResponseInterface $response PSR7 response 50 | * @param callable $next Next middleware 51 | * 52 | * @return ResponseInterface 53 | * @throws QueryException 54 | * @throws InvalidArgumentException 55 | */ 56 | public function __invoke( 57 | ServerRequestInterface $request, 58 | ResponseInterface $response, 59 | callable $next 60 | ): ResponseInterface { 61 | $userId = $this->userIdExtractor->getUserId($request); 62 | $permissionName = $this->permissionNameExtractor->getPermissionName($request); 63 | 64 | if ($this->accessChecker->hasAccess($userId, $permissionName)) { 65 | return $next($request, $response); 66 | } 67 | 68 | return $response->withStatus(self::PERMISSION_DENIED_CODE, self::PERMISSION_DENIED_MESSAGE); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Component/UserIdExtractor/AttributeUserIdExtractor.php: -------------------------------------------------------------------------------- 1 | getAttribute($this->userIdFieldName); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Component/UserIdExtractor/BaseUserIdExtractor.php: -------------------------------------------------------------------------------- 1 | userIdFieldName = $userIdFieldName; 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/Component/UserIdExtractor/CookieUserIdExtractor.php: -------------------------------------------------------------------------------- 1 | getCookieParams(); 12 | 13 | return $params[$this->userIdFieldName]; 14 | } 15 | } -------------------------------------------------------------------------------- /src/Component/UserIdExtractor/HeaderUserIdExtractor.php: -------------------------------------------------------------------------------- 1 | getHeaderLine($this->userIdFieldName); 12 | } 13 | } -------------------------------------------------------------------------------- /src/Component/UserIdExtractor/UserIdExtractor.php: -------------------------------------------------------------------------------- 1 | [ 41 | 'migrations' => self::MIGRATION_PATH 42 | ], 43 | 'environments' => [ 44 | 'default_migration_table' => self::DEFAULT_MIGRATION_TABLE, 45 | 'default_database' => self::DEFAULT_ENVIRONMENT_NAME, 46 | self::DEFAULT_ENVIRONMENT_NAME => $this->getAdapterConfigs($rbacConfig) 47 | ] 48 | ]; 49 | 50 | $this->config = new Config($configArray); 51 | } 52 | 53 | /** 54 | * @throws NotSupportedDatabaseException 55 | */ 56 | private function getAdapterConfigs(RbacConfig $rbacConfig): array 57 | { 58 | $platformName = $rbacConfig->getDatabaseDriver(); 59 | 60 | switch ($platformName) { 61 | case 'pdo_mysql': 62 | $adapterName = 'mysql'; 63 | break; 64 | case 'pdo_postgres': 65 | $adapterName = 'pgsql'; 66 | break; 67 | default: 68 | throw NotSupportedDatabaseException::notSupportedPlatform($platformName); 69 | } 70 | 71 | return [ 72 | 'adapter' => $adapterName, 73 | 'name' => $rbacConfig->getDatabaseName(), 74 | 'host' => $rbacConfig->getDatabaseHost(), 75 | 'user' => $rbacConfig->getDatabaseUser(), 76 | 'pass' => $rbacConfig->getDatabasePassword(), 77 | 'port' => $rbacConfig->getDatabasePort(), 78 | 'charset' => $rbacConfig->getDatabaseCharset(), 79 | ]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Console/Command/CreateConfigCommand.php: -------------------------------------------------------------------------------- 1 | setName('create-config') 19 | ->setDescription('This command creates sr_config.yaml file in working directory'); 20 | } 21 | 22 | /** 23 | * @param InputInterface $input 24 | * @param OutputInterface $output 25 | * @return void 26 | */ 27 | public function execute(InputInterface $input, OutputInterface $output) 28 | { 29 | $configFile = file_get_contents(__DIR__ . '/../../../config/sr_config.example.yaml'); 30 | $currentDir = getcwd(); 31 | file_put_contents($currentDir . '/sr_config.yaml', $configFile); 32 | $output->writeln("File sr_config.yaml created in directory: $currentDir"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Console/Command/MigrateDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | setName('migrate') 21 | ->setDescription('Applies migrations to database'); 22 | } 23 | 24 | /** 25 | * @param InputInterface $input 26 | * @param OutputInterface $output 27 | * @return void 28 | * @throws ConfigNotFoundException 29 | * @throws NotSupportedDatabaseException 30 | */ 31 | public function execute(InputInterface $input, OutputInterface $output): void 32 | { 33 | parent::execute($input, $output); 34 | $manager = new Manager($this->config, $input, $output); 35 | $manager->migrate($this->config->getDefaultEnvironment()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Console/Command/RollbackDatabaseCommand.php: -------------------------------------------------------------------------------- 1 | setName('rollback') 21 | ->setDescription('Rollback last migration to database'); 22 | } 23 | 24 | /** 25 | * @param InputInterface $input 26 | * @param OutputInterface $output 27 | * @return void 28 | * @throws ConfigNotFoundException 29 | * @throws NotSupportedDatabaseException 30 | */ 31 | public function execute(InputInterface $input, OutputInterface $output) 32 | { 33 | parent::execute($input, $output); 34 | $manager = new Manager($this->config, $input, $output); 35 | $manager->rollback($this->config->getDefaultEnvironment()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Console/SlimRbacConsoleApplication.php: -------------------------------------------------------------------------------- 1 | addCommands([ 23 | new Command\CreateConfigCommand(), 24 | new Command\MigrateDatabaseCommand(), 25 | new Command\RollbackDatabaseCommand(), 26 | ]); 27 | } 28 | 29 | /** 30 | * Runs the current application. 31 | * 32 | * @param InputInterface $input An Input instance 33 | * @param OutputInterface $output An Output instance 34 | * @return integer 0 if everything went fine, or an error code 35 | * @throws \Throwable 36 | */ 37 | public function doRun(InputInterface $input, OutputInterface $output): int 38 | { 39 | // always show the version information except when the user invokes the help 40 | // command as that already does it 41 | if (false === $input->hasParameterOption(['--help', '-h']) && null !== $input->getFirstArgument()) { 42 | $output->writeln($this->getLongVersion()); 43 | $output->writeln('Slim Role Based Access Control Middleware'); 44 | $output->writeln(''); 45 | } 46 | 47 | return parent::doRun($input, $output); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exception/BaseException.php: -------------------------------------------------------------------------------- 1 | additionalParams; 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/Exception/ConfigNotFoundException.php: -------------------------------------------------------------------------------- 1 | additionalParams = ['searchedPaths' => $searchedPaths]; 16 | 17 | return $e; 18 | } 19 | } -------------------------------------------------------------------------------- /src/Exception/CyclicException.php: -------------------------------------------------------------------------------- 1 | additionalParams = ['parentId' => $parentId, 'childId' => $childId]; 19 | 20 | return $e; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Exception/DatabaseException.php: -------------------------------------------------------------------------------- 1 | additionalParams = ['platformName' => $platformName]; 11 | 12 | return $e; 13 | } 14 | } -------------------------------------------------------------------------------- /src/Exception/NotUniqueException.php: -------------------------------------------------------------------------------- 1 | additionalParams = ['permissionName' => $permissionName]; 15 | 16 | return $e; 17 | } 18 | 19 | public static function notUniqueRole(string $roleName): self 20 | { 21 | $e = new self("Role with given name already created"); 22 | $e->additionalParams = ['roleName' => $roleName]; 23 | 24 | return $e; 25 | } 26 | 27 | public static function permissionAlreadyAttachedToRole(string $permissionName, string $roleName): self 28 | { 29 | $e = new self("Permission already attached to role."); 30 | $e->additionalParams = ['permissionName' => $permissionName, 'roleName' => $roleName]; 31 | 32 | return $e; 33 | } 34 | 35 | public static function childRoleAlreadyAttachedToGivenParentRole(string $childName, string $parentName): self 36 | { 37 | $e = new self("Child role already attached to parent role."); 38 | $e->additionalParams = ['childRoleName' => $childName, 'parentRoleName' => $parentName]; 39 | 40 | return $e; 41 | } 42 | 43 | public static function roleAlreadyAssignedToUser(string $roleName, string $userId): self 44 | { 45 | $e = new self("Role already assigned to user"); 46 | $e->additionalParams = ['roleName' => $roleName, 'userId' => $userId]; 47 | 48 | return $e; 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /src/Exception/PermissionNotFoundException.php: -------------------------------------------------------------------------------- 1 | id; 67 | } 68 | 69 | public function setId(int $id) 70 | { 71 | $this->id = $id; 72 | } 73 | 74 | public function getName(): string 75 | { 76 | return $this->name; 77 | } 78 | 79 | public function setName(string $name) 80 | { 81 | $this->name = $name; 82 | } 83 | 84 | public function isStatus(): bool 85 | { 86 | return $this->status; 87 | } 88 | 89 | public function setStatus(bool $status) 90 | { 91 | $this->status = $status; 92 | } 93 | 94 | public function getCreatedAt(): DateTime 95 | { 96 | return $this->createdAt; 97 | } 98 | 99 | public function setCreatedAt(DateTime $createdAt) 100 | { 101 | $this->createdAt = $createdAt; 102 | } 103 | 104 | public function getUpdatedAt(): DateTime 105 | { 106 | return $this->updatedAt; 107 | } 108 | 109 | public function setUpdatedAt(DateTime $updatedAt) 110 | { 111 | $this->updatedAt = $updatedAt; 112 | } 113 | 114 | public function getDescription(): string 115 | { 116 | return $this->description; 117 | } 118 | 119 | public function setDescription(string $description) 120 | { 121 | $this->description = $description; 122 | } 123 | 124 | /** @ORM\PrePersist */ 125 | public function prePersist() 126 | { 127 | $this->createdAt = new DateTime(); 128 | } 129 | 130 | /** @ORM\PreUpdate */ 131 | public function preUpdate() 132 | { 133 | $this->updatedAt = new DateTime(); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/Models/Entity/Role.php: -------------------------------------------------------------------------------- 1 | id; 68 | } 69 | 70 | public function setId(int $id) 71 | { 72 | $this->id = $id; 73 | } 74 | 75 | public function getName(): string 76 | { 77 | return $this->name; 78 | } 79 | 80 | public function setName(string $name) 81 | { 82 | $this->name = $name; 83 | } 84 | 85 | public function isStatus(): bool 86 | { 87 | return $this->status; 88 | } 89 | 90 | public function setStatus(bool $status) 91 | { 92 | $this->status = $status; 93 | } 94 | 95 | public function getCreatedAt(): DateTime 96 | { 97 | return $this->createdAt; 98 | } 99 | 100 | public function setCreatedAt(DateTime $createdAt) 101 | { 102 | $this->createdAt = $createdAt; 103 | } 104 | 105 | public function getUpdatedAt(): DateTime 106 | { 107 | return $this->updatedAt; 108 | } 109 | 110 | public function setUpdatedAt(DateTime $updatedAt) 111 | { 112 | $this->updatedAt = $updatedAt; 113 | } 114 | 115 | public function getDescription(): string 116 | { 117 | return $this->description; 118 | } 119 | 120 | public function setDescription(string $description) 121 | { 122 | $this->description = $description; 123 | } 124 | 125 | /** @ORM\PrePersist */ 126 | public function prePersist() 127 | { 128 | $this->createdAt = new DateTime(); 129 | } 130 | 131 | /** @ORM\PreUpdate */ 132 | public function preUpdate() 133 | { 134 | $this->updatedAt = new DateTime(); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Models/Entity/RoleHierarchy.php: -------------------------------------------------------------------------------- 1 | id; 77 | } 78 | 79 | public function setId(int $id) 80 | { 81 | $this->id = $id; 82 | } 83 | 84 | public function getCreatedAt(): DateTime 85 | { 86 | return $this->createdAt; 87 | } 88 | 89 | public function setCreatedAt(DateTime $createdAt) 90 | { 91 | $this->createdAt = $createdAt; 92 | } 93 | 94 | public function getChildRole(): Role 95 | { 96 | return $this->childRole; 97 | } 98 | 99 | public function setChildRole(Role $childRole) 100 | { 101 | $this->childRole = $childRole; 102 | } 103 | 104 | public function getParentRole(): Role 105 | { 106 | return $this->parentRole; 107 | } 108 | 109 | public function setParentRole(Role $parentRole) 110 | { 111 | $this->parentRole = $parentRole; 112 | } 113 | 114 | /** @ORM\PrePersist */ 115 | public function prePersist() 116 | { 117 | $this->createdAt = new DateTime(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Models/Entity/RolePermission.php: -------------------------------------------------------------------------------- 1 | id; 77 | } 78 | 79 | public function setId(int $id) 80 | { 81 | $this->id = $id; 82 | } 83 | 84 | public function getRoleId(): int 85 | { 86 | return $this->roleId; 87 | } 88 | 89 | public function setRoleId(int $roleId) 90 | { 91 | $this->roleId = $roleId; 92 | } 93 | 94 | public function getPermissionId(): int 95 | { 96 | return $this->permissionId; 97 | } 98 | 99 | public function setPermissionId(int $permissionId) 100 | { 101 | $this->permissionId = $permissionId; 102 | } 103 | 104 | public function getCreatedAt(): DateTime 105 | { 106 | return $this->createdAt; 107 | } 108 | 109 | public function setCreatedAt(DateTime $createdAt) 110 | { 111 | $this->createdAt = $createdAt; 112 | } 113 | 114 | public function getPermission(): Permission 115 | { 116 | return $this->permission; 117 | } 118 | 119 | public function setPermission(Permission $permission) 120 | { 121 | $this->permission = $permission; 122 | } 123 | 124 | public function getRole(): Role 125 | { 126 | return $this->role; 127 | } 128 | 129 | public function setRole(Role $role) 130 | { 131 | $this->role = $role; 132 | } 133 | 134 | /** @ORM\PrePersist */ 135 | public function prePersist() 136 | { 137 | $this->createdAt = new DateTime(); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Models/Entity/UserRole.php: -------------------------------------------------------------------------------- 1 | id; 64 | } 65 | 66 | public function setId(int $id) 67 | { 68 | $this->id = $id; 69 | } 70 | 71 | public function getUserId(): string 72 | { 73 | return $this->userId; 74 | } 75 | 76 | public function setUserId(string $userId) 77 | { 78 | $this->userId = $userId; 79 | } 80 | 81 | public function getRoleId(): int 82 | { 83 | return $this->roleId; 84 | } 85 | 86 | public function setRoleId(int $roleId) 87 | { 88 | $this->roleId = $roleId; 89 | } 90 | 91 | public function getCreatedAt(): DateTime 92 | { 93 | return $this->createdAt; 94 | } 95 | 96 | public function setCreatedAt(DateTime $createdAt) 97 | { 98 | $this->createdAt = $createdAt; 99 | } 100 | 101 | public function getRole(): Role 102 | { 103 | return $this->role; 104 | } 105 | 106 | public function setRole(Role $role) 107 | { 108 | $this->role = $role; 109 | } 110 | 111 | /** @ORM\PrePersist */ 112 | public function prePersist() 113 | { 114 | $this->createdAt = new DateTime(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Models/Repository/PermissionRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('permission'); 21 | 22 | $result = $qb->select('permission.id') 23 | ->where($qb->expr()->eq('permission.name', $qb->expr()->literal($permissionName))) 24 | ->setMaxResults(1) 25 | ->getQuery() 26 | ->getArrayResult(); 27 | 28 | if (count($result) > 0) { 29 | return $result[0]['id']; 30 | } 31 | 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/Repository/RoleHierarchyRepository.php: -------------------------------------------------------------------------------- 1 | getChildIds([$parentRoleId]); 23 | 24 | if (count($childIds) > 0) { 25 | 26 | if (in_array($findingChildId, $childIds)) { 27 | return true; 28 | } 29 | 30 | foreach ($childIds as $childId) { 31 | 32 | if ($this->hasChildRoleId($childId, $findingChildId) == true) { 33 | return true; 34 | } 35 | 36 | } 37 | } 38 | 39 | return false; 40 | } 41 | 42 | /** 43 | * @param integer[] $rootRoleIds 44 | * @return integer[] 45 | * @throws QueryException 46 | */ 47 | public function getAllRoleIdsHierarchy(array $rootRoleIds): array 48 | { 49 | $childRoleIds = $this->getAllChildRoleIds($rootRoleIds); 50 | 51 | return array_merge($rootRoleIds, $childRoleIds); 52 | } 53 | 54 | /** 55 | * Returns all hierarchically child role ids for given parent role ids. 56 | * 57 | * @param integer[] $parentIds 58 | * @return integer[] 59 | * @throws QueryException 60 | */ 61 | private function getAllChildRoleIds(array $parentIds): array 62 | { 63 | $allChildIds = []; 64 | 65 | while (count($parentIds) > 0) { 66 | $parentIds = $this->getChildIds($parentIds); 67 | $allChildIds = array_merge($allChildIds, $parentIds); 68 | }; 69 | 70 | return $allChildIds; 71 | } 72 | 73 | /** 74 | * Returns array of child role ids for given parent role ids. 75 | * 76 | * @param integer[] $parentIds 77 | * @return integer[] 78 | * @throws QueryException 79 | */ 80 | private function getChildIds(array $parentIds): array 81 | { 82 | $qb = $this->createQueryBuilder('roleHierarchy'); 83 | 84 | $qb->select('roleHierarchy.childRoleId') 85 | ->where($qb->expr()->in( 'roleHierarchy.parentRoleId', $parentIds)) 86 | ->indexBy('roleHierarchy', 'roleHierarchy.childRoleId'); 87 | 88 | $childRoleIds = $qb->getQuery()->getArrayResult(); 89 | 90 | return array_keys($childRoleIds); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Models/Repository/RolePermissionRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('rolePermission'); 23 | 24 | $qb->select('rolePermission.roleId') 25 | ->where($qb->expr()->eq('rolePermission.permissionId', $permission->getId())); 26 | 27 | return $qb->getQuery()->getArrayResult(); 28 | } 29 | 30 | /** 31 | * Search RolePermission record. If found return true else false 32 | * @param integer $permissionId 33 | * @param integer[] $roleIds 34 | * @return bool 35 | */ 36 | public function isPermissionAssigned(int $permissionId, array $roleIds): bool 37 | { 38 | $qb = $this->createQueryBuilder('rolePermission'); 39 | 40 | $result = $qb 41 | ->select('rolePermission.id') 42 | ->where( 43 | $qb->expr()->andX( 44 | $qb->expr()->eq('rolePermission.permissionId', $permissionId), 45 | $qb->expr()->in('rolePermission.roleId', $roleIds) 46 | ) 47 | ) 48 | ->setMaxResults(1) 49 | ->getQuery() 50 | ->getArrayResult(); 51 | 52 | return count($result) > 0; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Models/Repository/RoleRepository.php: -------------------------------------------------------------------------------- 1 | createQueryBuilder('userRole'); 24 | 25 | $qb->select('userRole.roleId') 26 | ->where($qb->expr()->eq('userRole.userId', $userId)) 27 | ->indexBy('userRole', 'userRole.roleId'); 28 | 29 | $roleIds = $qb->getQuery()->getArrayResult(); 30 | 31 | return array_keys($roleIds); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Models/RepositoryRegistry.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 20 | } 21 | 22 | public function getPermissionRepository(): PermissionRepository 23 | { 24 | return $this->entityManager->getRepository('\\Potievdev\\SlimRbac\\Models\\Entity\\Permission'); 25 | } 26 | 27 | public function getRoleRepository(): RoleRepository 28 | { 29 | return $this->entityManager->getRepository('\\Potievdev\\SlimRbac\\Models\\Entity\\Role'); 30 | } 31 | 32 | public function getUserRoleRepository(): UserRoleRepository 33 | { 34 | return $this->entityManager->getRepository('\\Potievdev\\SlimRbac\\Models\\Entity\\UserRole'); 35 | } 36 | 37 | public function getRolePermissionRepository(): RolePermissionRepository 38 | { 39 | return $this->entityManager->getRepository('\\Potievdev\\SlimRbac\\Models\\Entity\\RolePermission'); 40 | } 41 | 42 | public function getRoleHierarchyRepository(): RoleHierarchyRepository 43 | { 44 | return $this->entityManager->getRepository('\\Potievdev\\SlimRbac\\Models\\Entity\\RoleHierarchy'); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /tests/_data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/potievdev/slim-rbac/ce2df17ae736908e2538b924d3e8143b1e48ba75/tests/_data/.gitkeep -------------------------------------------------------------------------------- /tests/_output/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /tests/_support/AcceptanceTester.php: -------------------------------------------------------------------------------- 1 | rbacContainer = new RbacContainer(); 43 | $this->rbacManager = $this->rbacContainer->getRbacManager(); 44 | $this->repositoryRegistry = $this->rbacContainer->getInnerContainer()->get('repositoryRegistry'); 45 | $this->accessChecker = $this->rbacContainer->getInnerContainer()->get('accessChecker'); 46 | $this->clearDatabase(); 47 | } 48 | 49 | /** 50 | * @throws DatabaseException 51 | */ 52 | private function clearDatabase(): void 53 | { 54 | $pdo = $this->rbacContainer->getInnerContainer() 55 | ->get('entityManager') 56 | ->getConnection() 57 | ->getNativeConnection(); 58 | 59 | $pdo->beginTransaction(); 60 | 61 | try { 62 | $pdo->exec('DELETE FROM role_permission WHERE 1 > 0'); 63 | $pdo->exec('DELETE FROM role_hierarchy WHERE 1 > 0'); 64 | $pdo->exec('DELETE FROM permission WHERE 1 > 0'); 65 | $pdo->exec('DELETE FROM user_role WHERE 1 > 0'); 66 | $pdo->exec('DELETE FROM role WHERE 1 > 0'); 67 | 68 | $pdo->commit(); 69 | 70 | } catch (Exception $e) { 71 | $pdo->rollBack(); 72 | throw new DatabaseException($e->getMessage()); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/unit/RbacManagerTest.php: -------------------------------------------------------------------------------- 1 | rbacManager->createPermission('edit'); 30 | $write = $this->rbacManager->createPermission('write'); 31 | 32 | $moderator = $this->rbacManager->createRole('moderator'); 33 | $admin = $this->rbacManager->createRole('admin'); 34 | 35 | $this->rbacManager->attachPermission($moderator, $edit); 36 | $this->rbacManager->attachPermission($admin, $write); 37 | 38 | $this->rbacManager->attachChildRole($admin, $moderator); 39 | 40 | $this->rbacManager->assignRoleToUser($moderator, self::MODERATOR_USER_ID); 41 | $this->rbacManager->assignRoleToUser($admin, self::ADMIN_USER_ID); 42 | } 43 | 44 | public function successCasesProvider(): array 45 | { 46 | return [ 47 | 'moderator can edit' => [self::MODERATOR_USER_ID, 'edit'], 48 | 'admin can edit' => [self::ADMIN_USER_ID, 'edit'], 49 | 'admin can write' => [self::ADMIN_USER_ID, 'write'], 50 | ]; 51 | } 52 | 53 | /** 54 | * Testing has permission cases. 55 | * @param integer $userId user id 56 | * @param string $roleOrPermission role or permission name 57 | * @throws QueryException 58 | * @dataProvider successCasesProvider 59 | */ 60 | public function testCheckAccessSuccessCases(int $userId, string $roleOrPermission): void 61 | { 62 | $this->assertTrue($this->accessChecker->hasAccess($userId, $roleOrPermission)); 63 | } 64 | 65 | /** 66 | * @return array 67 | */ 68 | public function failCasesProvider(): array 69 | { 70 | return [ 71 | 'moderator has no write permission' => [self::MODERATOR_USER_ID, 'write'], 72 | 'not existing permission' => [self::ADMIN_USER_ID, 'none_permission'], 73 | 'not existing user id not has permission' => [self::NOT_USER_ID, 'edit'], 74 | 'not existing user id not has role' => [self::NOT_USER_ID, 'admin'] 75 | ]; 76 | } 77 | 78 | /** 79 | * Testing not have permission cases 80 | * @param integer $userId user id 81 | * @param string $roleOrPermission role or permission name 82 | * @throws QueryException 83 | * @dataProvider failCasesProvider 84 | */ 85 | public function testCheckAccessFailureCases(int $userId, string $roleOrPermission): void 86 | { 87 | $this->assertFalse($this->accessChecker->hasAccess($userId, $roleOrPermission)); 88 | } 89 | 90 | /** 91 | * Testing adding not unique permission 92 | * 93 | * @throws DatabaseException 94 | * @throws NotUniqueException|ORMException 95 | */ 96 | public function testCheckAddingNotUniquePermission() 97 | { 98 | $this->expectException(NotUniqueException::class); 99 | $this->rbacManager->createPermission('edit'); 100 | } 101 | 102 | /** 103 | * Testing adding not unique role 104 | * 105 | * @throws DatabaseException 106 | * @throws NotUniqueException|ORMException 107 | */ 108 | public function testCheckAddingNonUniqueRole() 109 | { 110 | $this->expectException(NotUniqueException::class); 111 | $this->rbacManager->createRole('moderator'); 112 | } 113 | 114 | /** 115 | * 116 | * @throws CyclicException 117 | * @throws DatabaseException 118 | * @throws NotUniqueException 119 | * @throws QueryException|ORMException 120 | */ 121 | public function testCheckCyclicException() 122 | { 123 | $this->expectException(CyclicException::class); 124 | $a = $this->rbacManager->createRole('a'); 125 | $b = $this->rbacManager->createRole('b'); 126 | 127 | $this->rbacManager->attachChildRole($a, $b); 128 | $this->rbacManager->attachChildRole($b, $a); 129 | } 130 | 131 | /** 132 | * Testing creating permission 133 | */ 134 | public function testCheckCreatingPermission() 135 | { 136 | $permission = $this->repositoryRegistry 137 | ->getPermissionRepository() 138 | ->findOneBy(['name' => 'edit']); 139 | 140 | $this->assertTrue($permission instanceof Permission); 141 | } 142 | 143 | /** 144 | * Testing creating role 145 | */ 146 | public function testCheckCreatingRole() 147 | { 148 | $role = $this->repositoryRegistry 149 | ->getRoleRepository() 150 | ->findOneBy(['name' => 'admin']); 151 | 152 | $this->assertTrue($role instanceof Role); 153 | } 154 | 155 | /** 156 | * @throws DatabaseException|ORMException 157 | */ 158 | public function testCheckDoubleAssigningPermissionToSameRole() 159 | { 160 | $this->expectException(NotUniqueException::class); 161 | 162 | /** @var Role $role */ 163 | $role = $this->repositoryRegistry 164 | ->getRoleRepository() 165 | ->findOneBy(['name' => 'admin']); 166 | 167 | /** @var Permission $permission */ 168 | $permission = $this->repositoryRegistry 169 | ->getPermissionRepository() 170 | ->findOneBy(['name' => 'write']); 171 | 172 | $this->rbacManager->attachPermission($role, $permission); 173 | } 174 | 175 | /** 176 | * @throws QueryException 177 | * @throws CyclicException 178 | * @throws DatabaseException 179 | * @throws NotUniqueException|ORMException 180 | * 181 | */ 182 | public function testCheckAddingSameChildRoleDoubleTime() 183 | { 184 | $this->expectException(NotUniqueException::class); 185 | 186 | /** @var Role $parent */ 187 | $parent = $this->repositoryRegistry 188 | ->getRoleRepository() 189 | ->findOneBy(['name' => 'admin']); 190 | 191 | /** @var Role $child */ 192 | $child = $this->repositoryRegistry 193 | ->getRoleRepository() 194 | ->findOneBy(['name' => 'moderator']); 195 | 196 | $this->rbacManager->attachChildRole($parent, $child); 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /tests/unit/RbacMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | rbacManager->createPermission('edit', 'Edit permission'); 45 | $write = $this->rbacManager->createPermission('write', 'Write permission'); 46 | 47 | $moderator = $this->rbacManager->createRole('moderator', 'Moderator role'); 48 | $admin = $this->rbacManager->createRole('admin', 'Admin role'); 49 | 50 | $this->rbacManager->attachPermission($moderator, $edit); 51 | $this->rbacManager->attachPermission($admin, $write); 52 | $this->rbacManager->attachChildRole($admin, $moderator); 53 | 54 | $this->rbacManager->assignRoleToUser($moderator, self::MODERATOR_USER_ID); 55 | $this->rbacManager->assignRoleToUser($admin, self::ADMIN_USER_ID); 56 | 57 | $this->callable = function (Request $request, Response $response) { 58 | return $response; 59 | }; 60 | $this->request = new ServerRequest('GET', 'write'); 61 | $this->response = new Response(); 62 | } 63 | 64 | /** 65 | * @throws QueryException 66 | * @throws InvalidArgumentException 67 | */ 68 | public function testCheckAccessSuccessCase() 69 | { 70 | $middleware = (new RbacContainer())->getRbacMiddleware(); 71 | $request = $this->request->withAttribute('userId', self::ADMIN_USER_ID); 72 | $response = $middleware($request, $this->response, $this->callable); 73 | $this->assertEquals(200, $response->getStatusCode()); 74 | } 75 | 76 | /** 77 | * @throws QueryException 78 | * @throws InvalidArgumentException 79 | */ 80 | public function testCheckAccessDeniedCase() 81 | { 82 | $middleware = (new RbacContainer())->getRbacMiddleware(); 83 | $request = $this->request->withAttribute('userId', self::MODERATOR_USER_ID); 84 | $response = $middleware($request, $this->response, $this->callable); 85 | $this->assertEquals(403, $response->getStatusCode()); 86 | } 87 | 88 | /** 89 | * @throws QueryException 90 | * @throws InvalidArgumentException 91 | * @throws ConfigNotFoundException 92 | */ 93 | public function testCheckReadingUserIdFromHeader() 94 | { 95 | $middleware = (new RbacContainer($this->createRbacConfig(RbacConfig::HEADER_RESOURCE_TYPE))) 96 | ->getRbacMiddleware(); 97 | $request = $this->request->withHeader('userId', self::ADMIN_USER_ID); 98 | $response = $middleware($request, $this->response, $this->callable); 99 | $this->assertEquals(200, $response->getStatusCode()); 100 | } 101 | 102 | /** 103 | * @throws QueryException 104 | * @throws InvalidArgumentException 105 | * @throws ConfigNotFoundException 106 | */ 107 | public function testCheckReadingUserIdFromCookie() 108 | { 109 | $middleware = (new RbacContainer($this->createRbacConfig(RbacConfig::COOKIE_RESOURCE_TYPE))) 110 | ->getRbacMiddleware(); 111 | $request = $this->request->withCookieParams(['userId' => self::ADMIN_USER_ID]); 112 | $response = $middleware($request, $this->response, $this->callable); 113 | $this->assertEquals(200, $response->getStatusCode()); 114 | } 115 | 116 | /** 117 | * @throws ConfigNotFoundException 118 | */ 119 | private function createRbacConfig(?string $resourceTypeId): RbacConfig 120 | { 121 | $rbacConfig = RbacConfig::createFromConfigFile(); 122 | 123 | return new RbacConfig( 124 | $rbacConfig->getDatabaseDriver(), 125 | $rbacConfig->getDatabaseHost(), 126 | $rbacConfig->getDatabaseUser(), 127 | $rbacConfig->getDatabasePassword(), 128 | $rbacConfig->getDatabasePort(), 129 | $rbacConfig->getDatabaseName(), 130 | $rbacConfig->getDatabaseCharset(), 131 | $rbacConfig->getUserIdFieldName(), 132 | $resourceTypeId ?? $rbacConfig->getUserIdResourceType() 133 | ); 134 | } 135 | 136 | } 137 | --------------------------------------------------------------------------------