├── resources ├── views │ └── .gitkeep └── boost │ └── guidelines │ └── core.blade.php ├── src ├── Enums │ ├── ActivityStatus.php │ ├── ABACLogicalOperator.php │ ├── RoleType.php │ └── ABACCondition.php ├── Interfaces │ ├── AAuthABACModelInterface.php │ └── AAuthOrganizationNodeInterface.php ├── Traits │ ├── AAuthABACModel.php │ ├── AAuthUser.php │ └── AAuthOrganizationNode.php ├── Commands │ └── AAuthCommand.php ├── Contracts │ └── AAuthUserContract.php ├── Http │ └── Requests │ │ ├── StoreOrganizationScopeRequest.php │ │ ├── StoreRoleRequest.php │ │ ├── UpdateOrganizationScopeRequest.php │ │ ├── UpdateOrganizationNodeRequest.php │ │ ├── StoreOrganizationNodeRequest.php │ │ └── UpdateRoleRequest.php ├── Exceptions │ ├── InvalidRoleException.php │ ├── InvalidUserException.php │ ├── UserHasNoAssignedRoleException.php │ ├── InvalidRoleTypeException.php │ ├── OrganizationNodeAuthException.php │ ├── InvalidLocationScopesException.php │ ├── InvalidOrganizationNodeException.php │ ├── InvalidOrganizationScopeException.php │ ├── OrganizationScopesMismatchException.php │ ├── MissingRoleException.php │ └── AuthorizationException.php ├── Models │ ├── RoleModelAbacRule.php │ ├── OrganizationScope.php │ ├── Role.php │ ├── OrganizationNode.php │ └── User.php ├── Facades │ └── AAuth.php ├── Utils │ └── ABACUtil.php ├── AAuthServiceProvider.php ├── Scopes │ ├── AAuthOrganizationNodeScope.php │ └── AAuthABACModelScope.php ├── Services │ ├── OrganizationService.php │ └── RolePermissionService.php └── AAuth.php ├── README-contr.md ├── database ├── factories │ └── ModelFactory.php ├── migrations │ ├── 2022_06_21_100000_create_abac_tables.php │ ├── 2014_10_12_100000_create_password_resets_table.php │ ├── 2014_10_12_000000_create_users_table.php │ ├── 2019_08_19_000000_create_failed_jobs_table.php │ ├── 2019_12_14_000001_create_personal_access_tokens_table.php │ ├── 2021_10_18_142336_seed_initial_data.php │ ├── 2021_10_15_085914_create_organization_tables.php │ └── 2021_10_14_130304_create_roles_and_permissions_tables.php └── seeders │ └── SampleDataSeeder.php ├── docker-compose.yml ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── docker-compose.yml ├── CHANGELOG.md ├── config └── aauth.php ├── LICENSE.md ├── .php-cs-fixer.dist.php ├── Readme-todo.md ├── phpunit.xml.dist.bak ├── composer.json ├── .cursor └── rules │ ├── aauth.mdc │ ├── aauth-abac.mdc │ └── aauth-orbac.mdc └── README.md /resources/views/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Enums/ActivityStatus.php: -------------------------------------------------------------------------------- 1 | '; 10 | case less_then = '<'; 11 | case greater_than_or_equal_to = '>='; 12 | case less_than_or_equal_to = '<='; 13 | case like = 'like'; 14 | } 15 | -------------------------------------------------------------------------------- /src/Traits/AAuthABACModel.php: -------------------------------------------------------------------------------- 1 | comment('All done'); 16 | $this->comment(config('aauth.aauth')); 17 | 18 | return self::SUCCESS; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | mariadb: 3 | image: mariadb:10.8 4 | ports: 5 | - "33062:3306" 6 | volumes: 7 | - aurora_aauth_root_mariadb_data:/var/lib/mysql 8 | environment: 9 | - MYSQL_ROOT_PASSWORD=aauth 10 | - MYSQL_PASSWORD=aauth 11 | - MYSQL_USER=aauth 12 | - MYSQL_DATABASE=aauth 13 | networks: 14 | default: 15 | driver: bridge 16 | ipam: 17 | config: 18 | - subnet: 172.16.57.0/24 19 | 20 | volumes: 21 | aurora_aauth_root_mariadb_data: 22 | -------------------------------------------------------------------------------- /src/Contracts/AAuthUserContract.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function roles(): BelongsToMany; 16 | } 17 | -------------------------------------------------------------------------------- /src/Http/Requests/StoreOrganizationScopeRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'min:5'], 11 | 'level' => [], 12 | ]; 13 | 14 | /** 15 | * @return array 16 | */ 17 | public static function getRules(): array 18 | { 19 | return self::$rules; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the official PHP image as a base 2 | FROM mcr.microsoft.com/devcontainers/php:1-8.2-bullseye 3 | 4 | # Install system dependencies 5 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 6 | && apt-get -y install --no-install-recommends default-mysql-client libonig-dev libxml2-dev php8.2-dev php-pear 7 | 8 | # Install PHP extensions 9 | RUN docker-php-ext-install pdo_mysql mbstring xml tokenizer ctype json curl dom fileinfo session bcmath 10 | 11 | # Set working directory 12 | WORKDIR /workspaces/${localWorkspaceFolderBasename} 13 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidRoleException.php: -------------------------------------------------------------------------------- 1 | [ 6 | 'system' => [ 7 | // example system permission 8 | // key => translation 9 | 'edit_something_for_system' => 'aauth/system.edit_something_for_system', 10 | 'create_something_for_system' => 'aauth/system.create_something_for_system', 11 | ], 12 | 'organization' => [ 13 | // example organization permission 14 | 'edit_something_for_organization' => 'aauth/organization.edit_something_for_organization', 15 | 'create_something_for_organization' => 'aauth/organization.create_something_for_organization', 16 | ], 17 | ], 18 | ]; 19 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidRoleTypeException.php: -------------------------------------------------------------------------------- 1 | ['required', 'min:5'], 12 | ]; 13 | 14 | /** 15 | * Determine if the user is authorized to make this request. 16 | * 17 | * @return bool 18 | */ 19 | public function authorize(): bool 20 | { 21 | return false; 22 | } 23 | 24 | /** 25 | * Get the validation rules that apply to the request. 26 | * 27 | * @return array 28 | */ 29 | public function rules(): array 30 | { 31 | return self::$rules; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidOrganizationNodeException.php: -------------------------------------------------------------------------------- 1 | ['required', 'min:5'], 11 | 'level' => [], 12 | ]; 13 | 14 | /** 15 | * Determine if the user is authorized to make this request. 16 | * 17 | * @return bool 18 | */ 19 | public function authorize(): bool 20 | { 21 | return false; 22 | } 23 | 24 | /** 25 | * Get the validation rules that apply to the request. 26 | * 27 | * @return array 28 | */ 29 | public function rules(): array 30 | { 31 | return self::$rules; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PHP & MariaDB", 3 | "dockerComposeFile": [ 4 | "docker-compose.yml" 5 | ], 6 | "service": "app", 7 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 8 | "features": { 9 | "ghcr.io/devcontainers/features/php:1": { 10 | "version": "8.2", 11 | "installComposer": true, 12 | "composerVersion": "latest" 13 | } 14 | }, 15 | "customizations": { 16 | "vscode": { 17 | "extensions": [ 18 | "bmewburn.vscode-intelephense-client", 19 | "xdebug.php-debug", 20 | "ms-azuretools.vscode-docker", 21 | "EditorConfig.EditorConfig", 22 | "streetsidesoftware.code-spell-checker" 23 | ] 24 | } 25 | }, 26 | "postCreateCommand": "composer install && cp .env.example .env && php artisan key:generate", 27 | "remoteUser": "vscode" 28 | } 29 | -------------------------------------------------------------------------------- /src/Http/Requests/UpdateOrganizationNodeRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'min:5'], 11 | 'parent_id' => ['required', 'int'], 12 | ]; 13 | 14 | /** 15 | * Determine if the user is authorized to make this request. 16 | * 17 | * @return bool 18 | */ 19 | public function authorize(): bool 20 | { 21 | return false; 22 | } 23 | 24 | /** 25 | * Get the validation rules that apply to the request. 26 | * 27 | * @return array 28 | */ 29 | public function rules(): array 30 | { 31 | return self::$rules; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Http/Requests/StoreOrganizationNodeRequest.php: -------------------------------------------------------------------------------- 1 | ['required', 'min:5'], 12 | 'parent_id' => ['required', 'int'], 13 | ]; 14 | 15 | /** 16 | * Determine if the user is authorized to make this request. 17 | * 18 | * @return bool 19 | */ 20 | public function authorize(): bool 21 | { 22 | return false; 23 | } 24 | 25 | /** 26 | * Get the validation rules that apply to the request. 27 | * 28 | * @return array 29 | */ 30 | public function rules(): array 31 | { 32 | return self::$rules; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Http/Requests/UpdateRoleRequest.php: -------------------------------------------------------------------------------- 1 | [ 11 | 'required', 12 | 'min:5', 13 | 'unique:permissions', 14 | ], 15 | ]; 16 | 17 | /** 18 | * Determine if the user is authorized to make this request. 19 | * 20 | * @return bool 21 | */ 22 | public function authorize(): bool 23 | { 24 | return false; 25 | } 26 | 27 | /** 28 | * Get the validation rules that apply to the request. 29 | * 30 | * @return array 31 | */ 32 | public function rules(): array 33 | { 34 | return self::$rules; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.devcontainer/docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile 6 | volumes: 7 | - ..:/workspaces/${localWorkspaceFolderBasename}:cached 8 | command: sleep infinity 9 | depends_on: 10 | - mariadb 11 | networks: 12 | - default 13 | 14 | mariadb: 15 | image: mariadb:10.8 16 | ports: 17 | - "33062:3306" 18 | volumes: 19 | - aurora_aauth_mariadb_data:/var/lib/mysql 20 | environment: 21 | - MYSQL_ROOT_PASSWORD=aauth 22 | - MYSQL_PASSWORD=aauth 23 | - MYSQL_USER=aauth 24 | - MYSQL_DATABASE=aauth 25 | networks: 26 | - default 27 | 28 | volumes: 29 | aurora_aauth_mariadb_data: 30 | 31 | networks: 32 | default: 33 | driver: bridge 34 | ipam: 35 | config: 36 | - subnet: 172.16.58.0/24 # Changed subnet to avoid conflict with existing one 37 | -------------------------------------------------------------------------------- /database/migrations/2022_06_21_100000_create_abac_tables.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->unsignedBigInteger('role_id')->index(); 19 | $table->string('model_type')->index(); 20 | $table->json('rules_json'); 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('role_model_abac_rules'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_100000_create_password_resets_table.php: -------------------------------------------------------------------------------- 1 | string('email')->index(); 19 | $table->string('token'); 20 | $table->timestamp('created_at')->nullable(); 21 | }); 22 | } 23 | } 24 | 25 | /** 26 | * Reverse the migrations. 27 | * 28 | * @return void 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists('password_resets'); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Models/RoleModelAbacRule.php: -------------------------------------------------------------------------------- 1 | > */ 26 | use HasFactory; 27 | 28 | protected $casts = [ 29 | 'rules_json' => 'array', 30 | ]; 31 | 32 | protected $fillable = ['role_id', 'model_type', 'rules_json']; 33 | } 34 | -------------------------------------------------------------------------------- /database/migrations/2014_10_12_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('name'); 20 | $table->string('email')->unique(); 21 | $table->timestamp('email_verified_at')->nullable(); 22 | $table->string('password'); 23 | $table->rememberToken(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('users'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /database/migrations/2019_08_19_000000_create_failed_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('uuid')->unique(); 20 | $table->text('connection'); 21 | $table->text('queue'); 22 | $table->longText('payload'); 23 | $table->longText('exception'); 24 | $table->timestamp('failed_at')->useCurrent(); 25 | }); 26 | } 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('failed_jobs'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Aurora 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->morphs('tokenable'); 20 | $table->string('name'); 21 | $table->string('token', 64)->unique(); 22 | $table->text('abilities')->nullable(); 23 | $table->timestamp('last_used_at')->nullable(); 24 | $table->timestamps(); 25 | }); 26 | } 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | * @return void 33 | */ 34 | public function down() 35 | { 36 | Schema::dropIfExists('personal_access_tokens'); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Exceptions/MissingRoleException.php: -------------------------------------------------------------------------------- 1 | guards = $guards; 36 | $this->redirectTo = $redirectTo; 37 | } 38 | 39 | /** 40 | * Get the guards that were checked. 41 | * 42 | * @return array 43 | */ 44 | public function guards() 45 | { 46 | return $this->guards; 47 | } 48 | 49 | /** 50 | * Get the path the user should be redirected to. 51 | * 52 | * @return string|null 53 | */ 54 | public function redirectTo() 55 | { 56 | return $this->redirectTo; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Exceptions/AuthorizationException.php: -------------------------------------------------------------------------------- 1 | guards = $guards; 36 | $this->redirectTo = $redirectTo; 37 | } 38 | 39 | /** 40 | * Get the guards that were checked. 41 | * 42 | * @return array 43 | */ 44 | public function guards() 45 | { 46 | return $this->guards; 47 | } 48 | 49 | /** 50 | * Get the path the user should be redirected to. 51 | * 52 | * @return string|null 53 | */ 54 | public function redirectTo() 55 | { 56 | return $this->redirectTo; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /Readme-todo.md: -------------------------------------------------------------------------------- 1 | ## Todo 2 | - Facade yerine sadece service provide kullanılabilir mi? aliass? 3 | - singleton içinde facade yerine service class olabilir 4 | - phpStan problemleri 5 | - pint, github actions 6 | - can fonkisyonlarında yetki 1 kez çekilebilir 7 | - Readme contribution 8 | 9 | 10 | ## Done 11 | - gerekli migration'lar 12 | - config dosyaları 13 | - aauth tüm methodların testi 14 | - model'ler 15 | - can fonkisyonlarının testi 16 | - Aauth testleri 17 | - blade direktif 18 | - facade 19 | - modelin içideni create ve update delete metodları 20 | - service provider ?? 21 | - trait yazılması ? scope yazılması 22 | - local scope? global scope? -> şuanda global yazıldı. 23 | - rolepermisson service class ve validations 24 | - testler 25 | - org. service 26 | - role perm service 27 | - phpstan 28 | - packagist 29 | - docs 30 | 31 | ## todo v2 32 | - role permisson service ve validations 33 | - org. permisson service ve validations 34 | - role perm. service validation ve excepiton unit testleri, validation excepitonarlını testti 35 | - org. service validation ve excepiton unit testleri, validation excepitonarlını testti 36 | - test with coverage 37 | - translations 38 | - request's ve validations 39 | - test'lerin publish edilmesi ve namespacelerin replace edilmesi 40 | - postgress testleri için github actions 41 | - laravel gates register policy 42 | 43 | 44 | ## Dökümantasyon 45 | - config'ler 46 | - migr. ve seeder'ların çalıştırılması 47 | - github pages docs 48 | ## backlog 49 | - github pages ? 50 | -------------------------------------------------------------------------------- /src/Facades/AAuth.php: -------------------------------------------------------------------------------- 1 | 'array', 23 | '||' => 'array', 24 | ]; 25 | 26 | foreach (ABACCondition::cases() as $condition) { 27 | $validationRules[$condition->value] = ['array']; 28 | if (array_key_exists($condition->value, $abacRules)) { 29 | $validationRules[$condition->value.'.attribute'] = ['string', 'required']; 30 | $validationRules[$condition->value.'.value'] = ['string', 'required']; 31 | } 32 | } 33 | 34 | $validation = Validator::make($abacRules, $validationRules); 35 | 36 | if ($validation->fails()) { 37 | throw new Exception($validation->messages()); 38 | } 39 | 40 | foreach ($abacRules as $abacRule) { 41 | if (is_array($abacRule)) { 42 | ABACUtil::validateAbacRuleArray($abacRule); 43 | } 44 | } 45 | } 46 | 47 | /** 48 | * @param string $ruleJson 49 | * @return void 50 | * 51 | * @throws Exception 52 | */ 53 | public static function validateAbacRuleJson(string $ruleJson): void 54 | { 55 | ABACUtil::validateAbacRuleArray(json_decode($ruleJson)); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /database/migrations/2021_10_18_142336_seed_initial_data.php: -------------------------------------------------------------------------------- 1 | id = 1; 19 | $organizationScope->name = 'Root Scope'; 20 | $organizationScope->level = 1; 21 | $organizationScope->status = 'active'; 22 | $organizationScope->save(); 23 | 24 | if (config('database.default') == 'pgsql') { 25 | DB::select(" 26 | SELECT setval(pg_get_serial_sequence('organization_scopes', 'id'), coalesce(max(id)+1, 1), false) 27 | FROM organization_scopes; 28 | "); 29 | } 30 | 31 | $on = new OrganizationNode(); 32 | $on->id = 1; 33 | $on->organization_scope_id = 1; 34 | $on->name = 'Root Node'; 35 | $on->path = '1'; 36 | $on->save(); 37 | 38 | if (config('database.default') == 'pgsql') { 39 | DB::select(" 40 | SELECT setval(pg_get_serial_sequence('organization_scopes', 'id'), coalesce(max(id)+1, 1), false) 41 | FROM organization_scopes; 42 | "); 43 | } 44 | } 45 | 46 | /** 47 | * Reverse the migrations. 48 | * 49 | * @return void 50 | */ 51 | public function down() 52 | { 53 | OrganizationScope::whereId(1)->delete(); 54 | OrganizationNode::whereId(1)->delete(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /phpunit.xml.dist.bak: -------------------------------------------------------------------------------- 1 | 2 | 21 | 22 | 23 | tests 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/AAuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('aauth') 23 | ->hasConfigFile() 24 | // ->hasViews() 25 | // ->hasMigration() 26 | // ->hasCommand(AAuthCommand::class) 27 | ; 28 | } 29 | 30 | /** 31 | * @return void 32 | */ 33 | public function boot(): void 34 | { 35 | parent::boot(); 36 | 37 | // load packages migrations 38 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); 39 | 40 | $this->publishes([ 41 | __DIR__.'/../database/seeders' => resource_path('../database/seeders'), 42 | ], 'aauth-seeders'); 43 | 44 | $this->publishes([ 45 | __DIR__.'/../config' => config_path(), 46 | ], 'aauth-config'); 47 | 48 | // todo singleton bind ?? 49 | $this->app->singleton('aauth', function ($app) { 50 | return new AAuth( 51 | Auth::user(), // @phpstan-ignore-line 52 | Session::get('roleId') 53 | ); 54 | }); 55 | 56 | Gate::before(function ($user, $ability, $arguments = []) { 57 | return app('aauth')->can($ability) ?: null; 58 | }); 59 | 60 | Blade::directive('aauth', function ($permission) { 61 | return ""; 62 | }); 63 | Blade::directive('endaauth', function () { 64 | return ''; 65 | }); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Models/OrganizationScope.php: -------------------------------------------------------------------------------- 1 | |\AuroraWebSoftware\AAuth\Models\OrganizationNode[] $organization_nodes 30 | * @property-read int|null $organization_nodes_count 31 | */ 32 | class OrganizationScope extends Model 33 | { 34 | /** @use \Illuminate\Database\Eloquent\Factories\HasFactory<\Illuminate\Database\Eloquent\Factories\Factory<\AuroraWebSoftware\AAuth\Models\OrganizationScope>> */ 35 | use HasFactory; 36 | 37 | protected $primaryKey = 'id'; 38 | 39 | protected $fillable = ['name', 'level', 'status']; 40 | 41 | /** 42 | * @return int 43 | */ 44 | public function getNodeCountAttribute(): int 45 | { 46 | // todo new attribute syntax 47 | return OrganizationNode::whereOrganizationScopeId($this->id)->count(); 48 | } 49 | 50 | /** 51 | * @return bool 52 | */ 53 | public function getDeletableAttribute(): bool 54 | { 55 | // todo new attribute syntax 56 | return $this->getNodeCountAttribute() == 0; 57 | } 58 | 59 | /** 60 | * @return \Illuminate\Database\Eloquent\Relations\HasMany<\AuroraWebSoftware\AAuth\Models\OrganizationNode, \AuroraWebSoftware\AAuth\Models\OrganizationScope> 61 | */ 62 | public function organization_nodes(): HasMany 63 | { 64 | return $this->hasMany(OrganizationNode::class); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Scopes/AAuthOrganizationNodeScope.php: -------------------------------------------------------------------------------- 1 | $builder 13 | * @param Model $model 14 | * @return void 15 | */ 16 | public function apply(Builder $builder, Model $model): void 17 | { 18 | $organizationNodesQuery = AAuth::organizationNodesQuery(true, $model::getModelType()); 19 | $query = $builder->getQuery(); 20 | $from = $this->getFromTableName($query->from); 21 | $query->wheres = array_map(function ($where) use ($from) { 22 | return $this->prefixWhereColumn($where, $from); 23 | }, $query->wheres); 24 | 25 | $builder->join('organization_nodes', 'organization_nodes.model_id', '=', sprintf('%s.id', $from)) 26 | ->select($this->getSelectColumns($from)); 27 | 28 | $builder->mergeWheres($organizationNodesQuery->getQuery()->wheres, $organizationNodesQuery->getBindings()); 29 | } 30 | 31 | /** 32 | * Get the table name from the query's "from" clause 33 | * 34 | * @param mixed $from 35 | * @return string 36 | */ 37 | protected function getFromTableName(mixed $from): string 38 | { 39 | return is_string($from) ? $from : ''; 40 | } 41 | 42 | /** 43 | * Prefix the where column with the table name if needed 44 | * 45 | * @param array $where 46 | * @param string $from 47 | * @return array 48 | */ 49 | protected function prefixWhereColumn(array $where, string $from): array 50 | { 51 | if (isset($where['column']) && ! str_contains($where['column'], $from . '.')) { 52 | $where['column'] = sprintf('%s.%s', $from, $where['column']); 53 | } 54 | 55 | return $where; 56 | } 57 | 58 | /** 59 | * Get the select columns for the query (only selects fields from the left table) 60 | * 61 | * @param string $from 62 | * @return array 63 | */ 64 | protected function getSelectColumns(string $from): array 65 | { 66 | return ["{$from}.*"]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aurorawebsoftware/aauth", 3 | "description": "Laravel Aauth", 4 | "keywords": [ 5 | "Aurora", 6 | "laravel", 7 | "aauth" 8 | ], 9 | "homepage": "https://github.com/AuroraWebSoftware/AAuth", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Aurora Web Software Team", 14 | "email": "websoftwareteam@aurorabilisim.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2|^8.3|^8.4", 20 | "laravel/pint": "^1.13", 21 | "spatie/laravel-package-tools": "^1.16.0", 22 | "laravel/framework": "^11.0|^12.0" 23 | }, 24 | "require-dev": { 25 | "friendsofphp/php-cs-fixer": "^3.35", 26 | "larastan/larastan": "^3.0", 27 | "nunomaduro/collision": "^8.1", 28 | "orchestra/testbench": "^10.0", 29 | "pestphp/pest": "^3.0", 30 | "pestphp/pest-plugin-laravel": "^3.0", 31 | "phpstan/extension-installer": "^1.3", 32 | "phpstan/phpstan": "^2.1", 33 | "phpstan/phpstan-deprecation-rules": "^2.0", 34 | "phpstan/phpstan-phpunit": "^2.0", 35 | "phpunit/phpunit": "^11.0", 36 | "spatie/laravel-ray": "^1.33" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "AuroraWebSoftware\\AAuth\\": "src", 41 | "AuroraWebSoftware\\AAuth\\Database\\Factories\\": "database/factories", 42 | "AuroraWebSoftware\\AAuth\\Database\\Seeders\\": "database/seeders" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "AuroraWebSoftware\\AAuth\\Tests\\": "tests" 48 | } 49 | }, 50 | "scripts": { 51 | "analyse": "vendor/bin/phpstan analyse", 52 | "test": "vendor/bin/pest", 53 | "test-coverage": "vendor/bin/pest --coverage", 54 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 55 | }, 56 | "config": { 57 | "sort-packages": true, 58 | "allow-plugins": { 59 | "pestphp/pest-plugin": true, 60 | "phpstan/extension-installer": true 61 | } 62 | }, 63 | "extra": { 64 | "laravel": { 65 | "providers": [ 66 | "AuroraWebSoftware\\AAuth\\AAuthServiceProvider" 67 | ], 68 | "aliases": { 69 | "AAuth": "AuroraWebSoftware\\AAuth\\Facades\\AAuth" 70 | } 71 | } 72 | }, 73 | "minimum-stability": "stable", 74 | "prefer-stable": true 75 | } 76 | -------------------------------------------------------------------------------- /src/Models/Role.php: -------------------------------------------------------------------------------- 1 | > */ 30 | use HasFactory; 31 | 32 | protected $fillable = ['organization_scope_id', 'type', 'name', 'status']; 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function permissions(): array 38 | { 39 | return $this 40 | ->join('role_permission', 'role_permission.role_id', '=', 'roles.id') 41 | ->pluck('permission')->toArray(); 42 | } 43 | 44 | /** 45 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\AuroraWebSoftware\AAuth\Models\OrganizationScope, \AuroraWebSoftware\AAuth\Models\Role> 46 | */ 47 | public function organization_scope(): BelongsTo 48 | { 49 | return $this->belongsTo(OrganizationScope::class); 50 | } 51 | 52 | /** 53 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\AuroraWebSoftware\AAuth\Models\OrganizationNode, \AuroraWebSoftware\AAuth\Models\Role, \Illuminate\Database\Eloquent\Relations\Pivot> 54 | */ 55 | public function organization_nodes(): BelongsToMany 56 | { 57 | return $this->belongsToMany(OrganizationNode::class, 'user_role_organization_node'); 58 | } 59 | 60 | /** 61 | * @return int 62 | */ 63 | public function getAssignedUserCountAttribute(): int 64 | { 65 | // new attribute syntax 66 | return DB::table('user_role_organization_node') 67 | ->where('role_id', $this->id)->groupBy('user_id')->count(); 68 | } 69 | 70 | /** 71 | * @return bool 72 | */ 73 | public function getDeletableAttribute(): bool 74 | { 75 | // new attribute syntax 76 | return $this->getAssignedUserCountAttribute() == 0; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Traits/AAuthUser.php: -------------------------------------------------------------------------------- 1 | belongsToMany(Role::class, 'user_role_organization_node'); 19 | } 20 | 21 | /** 22 | * @return Collection 23 | */ 24 | public function rolesWithOrganizationNodes(): Collection 25 | { 26 | $rolesCollection = collect(); 27 | $rolesWithOrganizationNodes = DB::table('user_role_organization_node')->where('user_id', '=', $this->id)->get(); 28 | 29 | foreach ($rolesWithOrganizationNodes as $rolesWithOrganizationNode) { 30 | $role = Role::find($rolesWithOrganizationNode->role_id); 31 | $role->organizationNode = OrganizationNode::find($rolesWithOrganizationNode->organization_node_id); 32 | 33 | $rolesCollection->push($role); 34 | } 35 | 36 | return $rolesCollection; 37 | } 38 | 39 | /** 40 | * @return BelongsToMany 41 | */ 42 | public function system_roles(): BelongsToMany 43 | { 44 | return $this->belongsToMany(Role::class, 'user_role_organization_node') 45 | ->where('type', 'system'); 46 | } 47 | 48 | /** 49 | * @return BelongsToMany 50 | */ 51 | public function organization_roles(): BelongsToMany 52 | { 53 | return $this->belongsToMany(Role::class, 'user_role_organization_node') 54 | ->where('type', 'organization'); 55 | } 56 | 57 | /** 58 | * @return int 59 | */ 60 | public function getAssignedUserCountAttribute(): int 61 | { 62 | return DB::table('user_role_organization_node') 63 | ->where('user_id', $this->id)->count(); 64 | } 65 | 66 | /** 67 | * @return bool 68 | */ 69 | public function getDeletableAttribute(): bool 70 | { 71 | // todo new syntax 72 | return $this->getAssignedUserCountAttribute() == 0; 73 | } 74 | 75 | public function can($abilities, $arguments = []): bool 76 | { 77 | if (is_string($abilities)) { 78 | return app('aauth')->can($abilities); 79 | } 80 | 81 | if (is_array($abilities)) { 82 | foreach ($abilities as $ability) { 83 | if (! app('aauth')->can($ability)) { 84 | return false; 85 | } 86 | } 87 | 88 | return true; 89 | } 90 | 91 | return parent::can($abilities, $arguments); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /database/migrations/2021_10_15_085914_create_organization_tables.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 19 | $table->string('name'); 20 | $table->unsignedInteger('level'); 21 | $table->enum('status', ['active', 'passive']); 22 | $table->timestamps(); 23 | 24 | $table->unique(['name']); 25 | 26 | $table->index(['name', 'status', 'level']); 27 | $table->index(['name', 'status']); 28 | $table->index(['name']); 29 | }); 30 | 31 | Schema::create('organization_nodes', function (Blueprint $table) { 32 | $table->bigIncrements('id')->unsigned(); 33 | $table->unsignedBigInteger('organization_scope_id'); 34 | $table->string('name'); 35 | 36 | // for polymorphic relations 37 | $table->string('model_type')->nullable(); 38 | $table->unsignedBigInteger('model_id')->nullable(); 39 | $table->string('path'); 40 | 41 | $table->unsignedBigInteger('parent_id')->nullable(); 42 | $table->timestamps(); 43 | 44 | $table->unique(['model_type', 'model_id']); 45 | $table->unique(['path']); 46 | 47 | $table->index(['model_type', 'model_id']); 48 | $table->index(['parent_id']); 49 | $table->index(['path']); 50 | }); 51 | 52 | Schema::table('user_role_organization_node', function (Blueprint $table) { 53 | $table->foreign('organization_node_id') 54 | ->references('id') 55 | ->on('organization_nodes') 56 | ->onUpdate('cascade'); 57 | }); 58 | 59 | Schema::table('organization_nodes', function (Blueprint $table) { 60 | $table->foreign('organization_scope_id') 61 | ->references('id') 62 | ->on('organization_scopes') 63 | ->onUpdate('cascade'); 64 | 65 | $table->foreign('parent_id') 66 | ->references('id') 67 | ->on('organization_nodes') 68 | ->onUpdate('cascade'); 69 | }); 70 | 71 | Schema::table('roles', function (Blueprint $table) { 72 | $table->foreign('organization_scope_id') 73 | ->references('id') 74 | ->on('organization_scopes') 75 | ->onUpdate('cascade'); 76 | }); 77 | } 78 | 79 | /** 80 | * Reverse the migrations. 81 | * 82 | * @return void 83 | */ 84 | public function down() 85 | { 86 | Schema::dropIfExists('organization_scopes'); 87 | Schema::dropIfExists('organization_nodes'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /database/migrations/2021_10_14_130304_create_roles_and_permissions_tables.php: -------------------------------------------------------------------------------- 1 | id(); 20 | // the roles' organization scope 21 | $table->unsignedBigInteger('organization_scope_id')->nullable(); 22 | $table->enum('type', ['system', 'organization']); 23 | $table->string('name'); 24 | $table->enum('status', ['active', 'passive']); 25 | $table->timestamps(); 26 | 27 | $table->unique(['name', 'type']); 28 | 29 | $table->index(['type', 'name', 'status']); 30 | $table->index(['name', 'status']); 31 | $table->index(['name']); 32 | }); 33 | 34 | Schema::create('role_permission', function (Blueprint $table) { 35 | $table->id(); 36 | $table->unsignedBigInteger('role_id'); 37 | $table->string('permission', 128); 38 | 39 | $table->unique(['role_id', 'permission']); 40 | 41 | $table->index(['role_id', 'permission']); 42 | $table->index(['role_id']); 43 | }); 44 | 45 | Schema::create('user_role_organization_node', function (Blueprint $table) { 46 | $table->id(); 47 | $table->unsignedBigInteger('user_id'); 48 | $table->unsignedBigInteger('role_id'); 49 | $table->unsignedBigInteger('organization_node_id')->nullable(); 50 | $table->timestamps(); 51 | 52 | $table->index(['user_id', 'role_id', 'organization_node_id'], 'user_role_organization_node_3_index'); 53 | $table->index(['user_id', 'role_id']); 54 | $table->index(['user_id', 'organization_node_id']); 55 | $table->index(['role_id', 'organization_node_id']); 56 | $table->index(['user_id']); 57 | }); 58 | 59 | Schema::table('user_role_organization_node', function (Blueprint $table) { 60 | $table->foreign('user_id') 61 | ->references('id') 62 | ->on('users') 63 | ->onUpdate('cascade'); 64 | 65 | $table->foreign('role_id') 66 | ->references('id') 67 | ->on('roles') 68 | ->onUpdate('cascade'); 69 | 70 | // organization node id is below 71 | }); 72 | 73 | Schema::table('role_permission', function (Blueprint $table) { 74 | $table->foreign('role_id') 75 | ->references('id') 76 | ->on('roles') 77 | ->onUpdate('cascade'); 78 | }); 79 | } 80 | 81 | /** 82 | * Reverse the migrations. 83 | * 84 | * @return void 85 | */ 86 | public function down() 87 | { 88 | Schema::dropIfExists('role_permission'); 89 | Schema::dropIfExists('user_role_organization_node'); 90 | Schema::dropIfExists('user_role'); 91 | Schema::dropIfExists('roles'); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Scopes/AAuthABACModelScope.php: -------------------------------------------------------------------------------- 1 | $builder 20 | * @param Model $model 21 | * @param mixed $rules 22 | * @param string $parentOperator 23 | * @return void 24 | * @throws Exception 25 | */ 26 | public function apply(Builder $builder, Model $model, mixed $rules = false, string $parentOperator = '&&'): void 27 | { 28 | if ($rules === false) { 29 | /** 30 | * @var AAuthABACModelInterface $model 31 | * 32 | * PHPStan analysis does not return any errors, but it underlines the ABACRules method because it somehow 33 | * does not see it, even though it is defined in the facade. 34 | * @phpstan-ignore-next-line 35 | */ 36 | $rules = AAuth::ABACRules($model::getModelType()) ?? []; 37 | 38 | /** 39 | * @var array $rules 40 | */ 41 | ABACUtil::validateAbacRuleArray($rules); 42 | 43 | $builder->where(function ($query) use ($rules, $model) { 44 | /** 45 | * @var Model $model 46 | */ 47 | $this->apply($query, $model, $rules); 48 | }); 49 | } else { 50 | $logicalOperators = ["&&","||"]; 51 | 52 | foreach ($rules as $rule) { 53 | $firstKey = array_key_first($rule); 54 | $abacRule = $rule[$firstKey]; 55 | 56 | if (in_array($firstKey, $logicalOperators)) { 57 | $this->applyLogicalOperator($builder, $abacRule, $model, $firstKey, $parentOperator); 58 | } else { 59 | $this->applyConditionalOperator($builder, $rule, $parentOperator); 60 | } 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Apply logical operator (&& or ||) to the query builder. 67 | * 68 | * @param Builder $builder 69 | * @param array $abacRule 70 | * @param Model $model 71 | * @param string $logicalOperator 72 | * @param string $parentOperator 73 | * 74 | * @return void 75 | * @throws Exception 76 | */ 77 | protected function applyLogicalOperator(Builder $builder, array $abacRule, Model $model, string $logicalOperator, string $parentOperator): void 78 | { 79 | $queryMethod = $parentOperator == '&&' ? 'where' : 'orWhere'; 80 | 81 | $builder->{$queryMethod}(function ($query) use ($abacRule, $model, $logicalOperator) { 82 | $this->apply($query, $model, $abacRule, $logicalOperator); 83 | }); 84 | } 85 | 86 | /** 87 | * Apply conditional operator to the query builder. 88 | * 89 | * @param Builder $builder 90 | * @param array $rule 91 | * @param string $parentOperator 92 | * 93 | * @return void 94 | */ 95 | protected function applyConditionalOperator(Builder $builder, array $rule, string $parentOperator): void 96 | { 97 | $operator = array_key_first($rule); 98 | 99 | $queryMethod = $parentOperator == '||' ? 'orWhere' : 'where'; 100 | 101 | $from = sprintf('%s.', is_string($builder->getQuery()->from) ? $builder->getQuery()->from : ''); 102 | 103 | $builder->{$queryMethod}( 104 | $from.$rule[$operator]['attribute'], 105 | $operator, 106 | $rule[$operator]['value'] 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Models/OrganizationNode.php: -------------------------------------------------------------------------------- 1 | > */ 49 | use HasFactory; 50 | 51 | protected $fillable = ['organization_scope_id', 'name', 'model_type', 'model_id', 'path', 'parent_id']; 52 | 53 | /** 54 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo<\AuroraWebSoftware\AAuth\Models\OrganizationScope, static> 55 | */ 56 | public function organization_scope(): BelongsTo 57 | { 58 | // todo 59 | return $this->belongsTo(OrganizationScope::class); 60 | } 61 | 62 | public function getAssignedNodeCountAttribute(): int 63 | { 64 | //todo new attribute syntax 65 | return DB::table('user_role_organization_node') 66 | ->where('organization_node_id', $this->id)->count(); 67 | } 68 | 69 | /** 70 | * @return bool 71 | */ 72 | public function getDeletableAttribute(): bool 73 | { 74 | //todo new attrbiute syntax 75 | if (OrganizationNode::whereParentId($this->id)->exists()) { 76 | return false; 77 | } 78 | 79 | return $this->getAssignedNodeCountAttribute() == 0; 80 | } 81 | 82 | /** 83 | * @return Collection 84 | * todo daha güzel fonksiyon ismi bulunmalı 85 | */ 86 | public function availableScopes(): Collection 87 | { 88 | return OrganizationScope::where([ 89 | ['status', 'active'], 90 | ['level', '>', $this->organization_scope->level], 91 | ])->get(); 92 | } 93 | 94 | /** 95 | * @return \Illuminate\Support\Collection 96 | */ 97 | public function breadCrumbs(): \Illuminate\Support\Collection 98 | { 99 | $pathNodeIds = explode('/', $this->path); 100 | /* @phpstan-ignore-next-line */ 101 | $breadCrumbs = collect(); 102 | foreach ($pathNodeIds as $pathNodeId) { 103 | $breadCrumbs->push(OrganizationNode::findOrFail($pathNodeId)); 104 | } 105 | 106 | return $breadCrumbs; 107 | } 108 | 109 | /** 110 | * @return \Illuminate\Database\Eloquent\Relations\MorphTo<\Illuminate\Database\Eloquent\Model, \AuroraWebSoftware\AAuth\Models\OrganizationNode> 111 | */ 112 | public function relatedModel(): MorphTo 113 | { 114 | return $this->morphTo(__FUNCTION__, 'model_type', 'model_id'); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /.cursor/rules/aauth.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: false 5 | --- 6 | # AAuth Core Concepts 7 | 8 | ## Combined Access Control: OrBAC + ABAC 9 | 10 | AAuth provides a powerful combination of Organization-Based Access Control (OrBAC) and Attribute-Based Access Control (ABAC) that can be used simultaneously to create sophisticated access control systems. This combination allows for both hierarchical organization-based access and fine-grained attribute-based filtering. 11 | 12 | ### How They Work Together 13 | 14 | 1. **OrBAC (Organization-Based Access Control)** 15 | - Controls access based on organizational hierarchy 16 | - Manages permissions at different organizational levels 17 | - Handles data isolation between organizations 18 | - Provides role-based access within organizations 19 | 20 | 2. **ABAC (Attribute-Based Access Control)** 21 | - Controls access based on model attributes 22 | - Provides fine-grained filtering of data 23 | - Allows dynamic access rules based on data properties 24 | - Enables complex business rule implementation 25 | 26 | ### Implementation Example 27 | 28 | Here's how to implement both OrBAC and ABAC in a single model: 29 | 30 | ```php 31 | use AuroraWebSoftware\AAuth\Contracts\OrganizationNodeContract; 32 | use AuroraWebSoftware\AAuth\Contracts\AAuthABACModelInterface; 33 | use AuroraWebSoftware\AAuth\Traits\OrganizationNode; 34 | use AuroraWebSoftware\AAuth\Traits\AAuthABACModel; 35 | 36 | class Order extends Model implements OrganizationNodeContract, AAuthABACModelInterface 37 | { 38 | use OrganizationNode, AAuthABACModel; 39 | 40 | protected $fillable = [ 41 | 'name', 42 | 'amount', 43 | 'status', 44 | 'organization_id' 45 | ]; 46 | 47 | public static function getModelType(): string 48 | { 49 | return 'order'; 50 | } 51 | 52 | public function getModelId(): int 53 | { 54 | return $this->id; 55 | } 56 | 57 | public static function getABACRules(): array 58 | { 59 | return [ 60 | '&&' => [ 61 | ['=' => ['attribute' => 'status', 'value' => 'active']] 62 | ] 63 | ]; 64 | } 65 | } 66 | ``` 67 | 68 | ### Access Control Flow 69 | 70 | When both OrBAC and ABAC are implemented: 71 | 72 | 1. **First Layer: OrBAC** 73 | - Checks if the user has access to the organization 74 | - Verifies organizational hierarchy permissions 75 | - Filters data based on organizational structure 76 | 77 | 2. **Second Layer: ABAC** 78 | - Applies attribute-based rules to the filtered data 79 | - Further refines access based on model attributes 80 | - Implements business-specific access rules 81 | 82 | ### Example Scenarios 83 | 84 | 1. **School System Example** 85 | ```php 86 | class Student extends Model implements OrganizationNodeContract, AAuthABACModelInterface 87 | { 88 | use OrganizationNode, AAuthABACModel; 89 | 90 | // OrBAC: Teacher can only see students in their department 91 | // ABAC: Teacher can only see active students with grade > 70 92 | 93 | public static function getABACRules(): array 94 | { 95 | return [ 96 | '&&' => [ 97 | ['=' => ['attribute' => 'status', 'value' => 'active']], 98 | ['>' => ['attribute' => 'grade', 'value' => 70]] 99 | ] 100 | ]; 101 | } 102 | } 103 | ``` 104 | 105 | 2. **E-commerce Example** 106 | ```php 107 | class Order extends Model implements OrganizationNodeContract, AAuthABACModelInterface 108 | { 109 | use OrganizationNode, AAuthABACModel; 110 | 111 | // OrBAC: Regional manager can only see orders in their region 112 | // ABAC: Can only see orders with amount > 1000 and status = 'completed' 113 | 114 | public static function getABACRules(): array 115 | { 116 | return [ 117 | '&&' => [ 118 | ['>' => ['attribute' => 'amount', 'value' => 1000]], 119 | ['=' => ['attribute' => 'status', 'value' => 'completed']] 120 | ] 121 | ]; 122 | } 123 | } 124 | ``` 125 | 126 | 127 | ### Common Use Cases 128 | 129 | 1. **School Management Systems** 130 | - OrBAC: Department/Class access 131 | - ABAC: Grade/Status filtering 132 | 133 | 2. **E-commerce Platforms** 134 | - OrBAC: Regional access 135 | - ABAC: Order status/amount rules 136 | 137 | 3. **Healthcare Systems** 138 | - OrBAC: Department access 139 | - ABAC: Patient age data filtering -------------------------------------------------------------------------------- /src/Models/User.php: -------------------------------------------------------------------------------- 1 | > */ 40 | use HasFactory; 41 | use Notifiable; 42 | 43 | protected $fillable = [ 44 | 'name', 45 | 'email', 46 | 'password', 47 | ]; 48 | 49 | /** 50 | * The attributes that should be hidden for serialization. 51 | */ 52 | protected $hidden = [ 53 | 'password', 54 | 'remember_token', 55 | ]; 56 | 57 | /** 58 | * The attributes that should be cast. 59 | */ 60 | protected $casts = [ 61 | 'email_verified_at' => 'datetime', 62 | ]; 63 | 64 | /** 65 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\AuroraWebSoftware\AAuth\Models\Role, \AuroraWebSoftware\AAuth\Models\User, \Illuminate\Database\Eloquent\Relations\Pivot> 66 | */ 67 | public function roles(): BelongsToMany 68 | { 69 | return $this->belongsToMany(Role::class, 'user_role_organization_node'); 70 | } 71 | 72 | /** 73 | * @return Collection 74 | */ 75 | public function rolesWithOrganizationNodes(): Collection 76 | { 77 | // @phpstan-ignore-next-line 78 | $rolesCollection = collect(); 79 | 80 | $rolesWithOrganizationNodes = DB::table('user_role_organization_node')->where('user_id', '=', $this->id)->get(); 81 | 82 | foreach ($rolesWithOrganizationNodes as $rolesWithOrganizationNode) { 83 | $role = Role::find($rolesWithOrganizationNode->role_id); 84 | /** 85 | * @var Role $role 86 | * @phpstan-ignore-next-line 87 | */ 88 | $role->organizationNode = OrganizationNode::find($rolesWithOrganizationNode->organization_node_id); 89 | 90 | $rolesCollection->push($role); 91 | } 92 | 93 | return $rolesCollection; 94 | } 95 | 96 | /** 97 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\AuroraWebSoftware\AAuth\Models\Role, \AuroraWebSoftware\AAuth\Models\User, \Illuminate\Database\Eloquent\Relations\Pivot> 98 | */ 99 | public function system_roles(): BelongsToMany 100 | { 101 | return $this->belongsToMany(Role::class, 'user_role_organization_node') 102 | ->where('type', 'system'); 103 | } 104 | 105 | /** 106 | * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\AuroraWebSoftware\AAuth\Models\Role, \AuroraWebSoftware\AAuth\Models\User, \Illuminate\Database\Eloquent\Relations\Pivot> 107 | */ 108 | public function organization_roles(): BelongsToMany 109 | { 110 | return $this->belongsToMany(Role::class, 'user_role_organization_node') 111 | ->where('type', 'organization'); 112 | } 113 | 114 | /** 115 | * @return int 116 | */ 117 | public function getAssignedUserCountAttribute(): int 118 | { 119 | return DB::table('user_role_organization_node') 120 | ->where('user_id', $this->id)->count(); 121 | } 122 | 123 | /** 124 | * @return bool 125 | */ 126 | public function getDeletableAttribute(): bool 127 | { 128 | // todo new syntax 129 | return $this->getAssignedUserCountAttribute() == 0; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/Traits/AAuthOrganizationNode.php: -------------------------------------------------------------------------------- 1 | slug = $model->generateSlug($model->title); 29 | }); 30 | */ 31 | } 32 | 33 | /** 34 | * @return mixed 35 | */ 36 | public function allWithoutAAuthOrganizationNodeScope(): mixed 37 | { 38 | return self::withoutGlobalScopes()->all(); 39 | } 40 | 41 | /** 42 | * @return OrganizationNode|Builder|Model|null 43 | */ 44 | public function relatedAAuthOrganizationNode(): Model|OrganizationNode|Builder|null 45 | { 46 | return OrganizationNode::whereModelId($this->getModelId()) 47 | ->whereModelType(self::getModelType()) 48 | ->first(); 49 | } 50 | 51 | /** 52 | * @throws Throwable 53 | */ 54 | public static function createWithAAuthOrganizationNode(array $modelCreateData, int $parentOrganizationNodeId, int $organizationScopeId) 55 | { 56 | // todo di 57 | $organizationService = new OrganizationService(); 58 | 59 | // todo yetki kontrolü ? serviste mi olmalı? 60 | // gerekli validationlar, organization scope validationları vs. 61 | // commit rollback 62 | $parentOrganizationNode = OrganizationNode::find($parentOrganizationNodeId); 63 | 64 | throw_if($parentOrganizationNode == null, new InvalidOrganizationNodeException()); 65 | 66 | $organizationScope = OrganizationScope::find($organizationScopeId); 67 | 68 | throw_if($organizationScope == null, new InvalidOrganizationScopeException()); 69 | 70 | $createdModel = self::create($modelCreateData); 71 | 72 | $OrgNodeCreateData = [ 73 | 'name' => $createdModel->getModelName(), 74 | 'organization_scope_id' => $organizationScope->id, 75 | 'parent_id' => $parentOrganizationNode->id, 76 | 'model_type' => self::getModelType(), 77 | 'model_id' => $createdModel->getModelId(), 78 | ]; 79 | $createdON = $organizationService->createOrganizationNode($OrgNodeCreateData); 80 | 81 | return $createdModel; 82 | } 83 | 84 | /** 85 | * @throws Throwable 86 | */ 87 | public static function updateWithAAuthOrganizationNode(int $modelId, int $nodeId, array $modelUpdateData, int $parentOrganizationNodeId, int $organizationScopeId) 88 | { 89 | 90 | $organizationService = new OrganizationService(); 91 | 92 | $parentOrganizationNode = OrganizationNode::find($parentOrganizationNodeId); 93 | 94 | throw_if($parentOrganizationNode == null, new InvalidOrganizationNodeException()); 95 | 96 | $organizationScope = OrganizationScope::find($organizationScopeId); 97 | 98 | throw_if($organizationScope == null, new InvalidOrganizationScopeException()); 99 | 100 | 101 | $modelInfo = self::find($modelId); 102 | 103 | $updatedModel = $modelInfo->update($modelUpdateData); 104 | 105 | 106 | 107 | $OrgNodeUpdateData = [ 108 | 'name' => $modelInfo->getModelName(), 109 | 'organization_scope_id' => $organizationScope->id, 110 | 'parent_id' => $parentOrganizationNode->id, 111 | 'model_type' => self::getModelType(), 112 | 'model_id' => $modelId, 113 | ]; 114 | $updateON = $organizationService->updateOrganizationNodesRecursively($OrgNodeUpdateData, $nodeId); 115 | 116 | return $updatedModel; 117 | } 118 | 119 | /** 120 | * @param int $modelId 121 | * @return bool 122 | * @throws Throwable 123 | */ 124 | public static function deleteWithAAuthOrganizationNode(int $modelId) 125 | { 126 | 127 | $organizationService = new OrganizationService(); 128 | 129 | $organizationNode = OrganizationNode::where('model_id', $modelId)->first(); 130 | 131 | 132 | throw_if($organizationNode == null, new InvalidOrganizationNodeException()); 133 | 134 | 135 | $modelInfo = self::findOrFail($modelId); 136 | 137 | $deleteModel = $modelInfo->delete($modelInfo); 138 | 139 | 140 | $deleteON = $organizationService->deleteOrganizationNodesRecursively($organizationNode->id); 141 | 142 | return true; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Services/OrganizationService.php: -------------------------------------------------------------------------------- 1 | fails()) { 35 | $message = implode(' , ', $validator->getMessageBag()->all()); 36 | 37 | throw new ValidationException($validator, new Response($message, Response::HTTP_UNPROCESSABLE_ENTITY)); 38 | } 39 | } 40 | 41 | return OrganizationScope::create($organizationScope); 42 | } 43 | 44 | /** 45 | * Updates a Perm. 46 | * 47 | * @param array $organizationScope 48 | * @param int $id 49 | * @param bool $withValidation 50 | * @return ?OrganizationScope 51 | * 52 | * @throws ValidationException 53 | */ 54 | public function updateOrganizationScope(array $organizationScope, int $id, bool $withValidation = true): ?OrganizationScope 55 | { 56 | if ($withValidation) { 57 | $validator = Validator::make($organizationScope, UpdateOrganizationScopeRequest::$rules); 58 | 59 | if ($validator->fails()) { 60 | $message = implode(' , ', $validator->getMessageBag()->all()); 61 | 62 | throw new ValidationException($validator, new Response($message, Response::HTTP_UNPROCESSABLE_ENTITY)); 63 | } 64 | } 65 | $organizationScopeModel = OrganizationScope::find($id); 66 | 67 | return $organizationScopeModel->update($organizationScope) ? $organizationScopeModel : null; 68 | } 69 | 70 | /** 71 | * deletes perm. 72 | * 73 | * @param int $id 74 | * @return bool|null 75 | */ 76 | public function deleteOrganizationScope(int $id): ?bool 77 | { 78 | return OrganizationScope::find($id)->delete(); 79 | } 80 | 81 | /** 82 | * Creates an org. node with given array 83 | * 84 | * @param array $organizationNode 85 | * @param bool $withValidation 86 | * @return OrganizationNode 87 | * 88 | * @throws ValidationException 89 | */ 90 | public function createOrganizationNode(array $organizationNode, bool $withValidation = true): OrganizationNode 91 | { 92 | // todo scope eşleşmeleri 93 | if ($withValidation) { 94 | $validator = Validator::make($organizationNode, StoreOrganizationNodeRequest::$rules); 95 | if ($validator->fails()) { 96 | $message = implode(' , ', $validator->getMessageBag()->all()); 97 | 98 | throw new ValidationException($validator, new Response($message, Response::HTTP_UNPROCESSABLE_ENTITY)); 99 | } 100 | } 101 | 102 | $parentPath = $this->getPath($organizationNode['parent_id'] ?? null); 103 | 104 | // add temp path before determine actual path 105 | $organizationNode['path'] = $parentPath . '/?'; 106 | $organizationNode = OrganizationNode::create($organizationNode); 107 | 108 | // todo , can be add inside model's created event 109 | $organizationNode->path = $parentPath . $organizationNode->id; 110 | $organizationNode->save(); 111 | 112 | return $organizationNode; 113 | } 114 | 115 | /** 116 | * @param Model $model 117 | * @param int $parentOrganizationId 118 | * @return OrganizationNode|null 119 | */ 120 | public function createOrganizationNodeForModel(Model $model, int $parentOrganizationId): ?OrganizationNode 121 | { 122 | return null; 123 | } 124 | 125 | /** 126 | * Return path with trailing slash (/) 127 | * @param int|null $organizationNodeId 128 | * @return string|null 129 | */ 130 | public function getPath(?int $organizationNodeId): ?string 131 | { 132 | if ($organizationNodeId == null) { 133 | return ''; 134 | } 135 | 136 | return OrganizationNode::find($organizationNodeId)?->path . '/'; 137 | } 138 | 139 | /** 140 | * @param int $organizationNodeId 141 | */ 142 | public function calculatePath(int $organizationNodeId): void 143 | { 144 | // todo 145 | } 146 | 147 | /** 148 | * Updates organization node recursively using breadth first method 149 | * @param OrganizationNode $node 150 | * @param bool|null $withDBTransaction 151 | * @return void 152 | */ 153 | public function updateNodePathsRecursively(OrganizationNode $node, ?bool $withDBTransaction = true): void 154 | { 155 | if ($withDBTransaction) { 156 | DB::beginTransaction(); 157 | } 158 | 159 | try { 160 | $node->path = $this->getPath($node->parent_id) . $node->id; 161 | $node->save(); 162 | 163 | $subNodes = OrganizationNode::whereParentId($node->id)->get(); 164 | 165 | foreach ($subNodes as $subNode) { 166 | $this->updateNodePathsRecursively($subNode, false); 167 | } 168 | 169 | } catch (\Exception $exception) { 170 | DB::rollback(); 171 | } 172 | 173 | if ($withDBTransaction) { 174 | DB::commit(); 175 | } 176 | } 177 | 178 | /** 179 | * deletes organization nodes using depth first search 180 | * @param OrganizationNode $node 181 | * @param bool|null $withDBTransaction 182 | * @return void 183 | */ 184 | public function deleteOrganizationNodesRecursively(OrganizationNode $node, ?bool $withDBTransaction = true): void 185 | { 186 | if ($withDBTransaction) { 187 | DB::beginTransaction(); 188 | } 189 | 190 | try { 191 | // 192 | $subNodes = OrganizationNode::whereParentId($node->id)->get(); 193 | 194 | foreach ($subNodes as $subNode) { 195 | $this->deleteOrganizationNodesRecursively($subNode, false); 196 | } 197 | 198 | $node->delete(); 199 | 200 | } catch (\Exception $exception) { 201 | DB::rollback(); 202 | } 203 | 204 | if ($withDBTransaction) { 205 | DB::commit(); 206 | } 207 | 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /.cursor/rules/aauth-abac.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # AAuth ABAC Documentation 7 | 8 | ## What is ABAC in AAuth? 9 | 10 | Attribute-Based Access Control (ABAC) in AAuth is an advanced system for creating dynamic, attribute-based filtering scopes for your Eloquent models. It allows you to define granular access rules that operate directly on the attributes of the models themselves. When a user attempts to retrieve data, these rules are automatically applied to the database queries, ensuring that only records matching the specified attribute conditions for the user's current role are returned. 11 | 12 | ## Core Concepts 13 | 14 | ### Model Attributes 15 | - These are the characteristics of your Eloquent model instances 16 | - Directly correspond to the columns in your database tables 17 | - Examples: `status`, `amount`, `created_at`, `customer_id` 18 | - ABAC rules are built around evaluating these attributes 19 | 20 | ### Rules/Policies 21 | - Structured statements that define conditions based on model attributes 22 | - Stored and associated with user roles 23 | - Conditions operate on the attributes of the Eloquent model being queried 24 | - Can be complex with nested logical operators 25 | 26 | ## Implementation 27 | 28 | ### 1. Model Setup 29 | 30 | To make your Eloquent models controllable via ABAC: 31 | 32 | ```php 33 | use AuroraWebSoftware\AAuth\Contracts\AAuthABACModelInterface; 34 | use AuroraWebSoftware\AAuth\Traits\AAuthABACModel; 35 | 36 | class Order extends Model implements AAuthABACModelInterface 37 | { 38 | use AAuthABACModel; 39 | 40 | /** 41 | * Get the type of the model for ABAC. 42 | * This is typically a string that identifies your model. 43 | * 44 | * @return string 45 | */ 46 | public static function getModelType(): string 47 | { 48 | return 'order'; 49 | } 50 | 51 | /** 52 | * Define the ABAC rules for this model. 53 | * These rules determine how access is granted based on attributes. 54 | * 55 | * @return array 56 | */ 57 | public static function getABACRules(): array 58 | { 59 | return [ 60 | '&&' => [ 61 | ['=' => ['attribute' => 'status', 'value' => 'active']] 62 | ] 63 | ]; 64 | } 65 | } 66 | ``` 67 | 68 | ### 2. Rule Structure 69 | 70 | ABAC rules are defined as PHP arrays with the following structure: 71 | 72 | ```php 73 | [ 74 | '&&' => [ // Top-level logical operator (AND) 75 | ['=' => ['attribute' => 'status', 'value' => 'active']], // Condition 1 76 | ['>' => ['attribute' => 'amount', 'value' => 100]], // Condition 2 77 | ['||' => [ // Nested logical operator (OR) 78 | ['=' => ['attribute' => 'category', 'value' => 'electronics']], 79 | ['=' => ['attribute' => 'category', 'value' => 'books']] 80 | ]] 81 | ] 82 | ] 83 | ``` 84 | 85 | ### 3. Logical Operators 86 | 87 | #### AND (&&) 88 | All conditions within the block must be true: 89 | ```php 90 | [ 91 | '&&' => [ 92 | ['=' => ['attribute' => 'status', 'value' => 'active']], 93 | ['>' => ['attribute' => 'amount', 'value' => 100]] 94 | ] 95 | ] 96 | ``` 97 | 98 | #### OR (||) 99 | At least one condition must be true: 100 | ```php 101 | [ 102 | '||' => [ 103 | ['=' => ['attribute' => 'status', 'value' => 'active']], 104 | ['=' => ['attribute' => 'status', 'value' => 'pending']] 105 | ] 106 | ] 107 | ``` 108 | 109 | ### 4. Conditional Operators 110 | 111 | - `=` : Equal to 112 | - `!=` or `<>` : Not equal to 113 | - `>` : Greater than 114 | - `<` : Less than 115 | - `>=` : Greater than or equal to 116 | - `<=` : Less than or equal to 117 | - `LIKE` : String matching (e.g., `'%' . $searchTerm . '%'`) 118 | - `NOT LIKE` : Negated string matching 119 | - `IN` : Value is within a given array 120 | - `NOT IN` : Value is not within a given array 121 | 122 | ### 5. Rule Examples 123 | 124 | 1. Simple Equality: 125 | ```php 126 | [ 127 | '&&' => [ 128 | ['=' => ['attribute' => 'status', 'value' => 'active']] 129 | ] 130 | ] 131 | ``` 132 | 133 | 2. Multiple Conditions: 134 | ```php 135 | [ 136 | '&&' => [ 137 | ['=' => ['attribute' => 'status', 'value' => 'active']], 138 | ['>=' => ['attribute' => 'amount', 'value' => 1000]] 139 | ] 140 | ] 141 | ``` 142 | 143 | 3. Pattern Matching: 144 | ```php 145 | [ 146 | '&&' => [ 147 | ['like' => ['attribute' => 'name', 'value' => '%test%']] 148 | ] 149 | ] 150 | ``` 151 | 152 | 4. Complex Nested Conditions: 153 | ```php 154 | [ 155 | '&&' => [ 156 | ['=' => ['attribute' => 'is_published', 'value' => true]], 157 | ['||' => [ 158 | ['=' => ['attribute' => 'visibility', 'value' => 'public']], 159 | ['=' => ['attribute' => 'owner_id', 'value' => '$USER_ID']] 160 | ]] 161 | ] 162 | ] 163 | ``` 164 | 165 | ## Managing ABAC Rules 166 | 167 | ### 1. Creating Rules 168 | 169 | Rules are stored in the `role_model_abac_rules` table: 170 | 171 | ```php 172 | use AuroraWebSoftware\AAuth\Models\RoleModelAbacRule; 173 | 174 | $rules = [ 175 | '&&' => [ 176 | ['=' => ['attribute' => 'status', 'value' => 'active']], 177 | ['>=' => ['attribute' => 'amount', 'value' => 100]] 178 | ] 179 | ]; 180 | 181 | RoleModelAbacRule::create([ 182 | 'role_id' => $roleId, 183 | 'model_type' => Order::getModelType(), 184 | 'rules_json' => $rules 185 | ]); 186 | ``` 187 | 188 | ### 2. Updating Rules 189 | 190 | ```php 191 | $rule = RoleModelAbacRule::where('role_id', $roleId) 192 | ->where('model_type', Order::getModelType()) 193 | ->first(); 194 | 195 | $rule->update([ 196 | 'rules_json' => $newRules 197 | ]); 198 | ``` 199 | 200 | ### 3. Deleting Rules 201 | 202 | ```php 203 | RoleModelAbacRule::where('role_id', $roleId) 204 | ->where('model_type', Order::getModelType()) 205 | ->delete(); 206 | ``` 207 | 208 | ## Automatic Query Filtering 209 | 210 | The `AAuthABACModelScope` automatically applies ABAC rules to all queries: 211 | 212 | ```php 213 | // This query will automatically include ABAC conditions 214 | $orders = Order::all(); 215 | 216 | // Complex queries are also filtered 217 | $highValueOrders = Order::where('amount', '>', 500)->get(); 218 | ``` 219 | 220 | ### Bypassing ABAC Scope 221 | 222 | To bypass ABAC filtering: 223 | 224 | ```php 225 | use AuroraWebSoftware\AAuth\Scopes\AAuthABACModelScope; 226 | 227 | // Retrieve all records without ABAC filtering 228 | $allOrders = Order::withoutGlobalScope(AAuthABACModelScope::class)->get(); 229 | ``` 230 | 231 | ## Best Practices 232 | 233 | 234 | 235 | 236 | 4. Maintenance: 237 | - Document rule changes 238 | - Version control rules 239 | - Regular rule reviews 240 | - Clean up unused rules 241 | - Monitor rule effectiveness 242 | 243 | ## Testing 244 | 245 | ### Unit Tests 246 | 247 | ```php 248 | test('can validate abac rule array', function () { 249 | $rules = [ 250 | '&&' => [ 251 | ['=' => ['attribute' => 'name', 'value' => 'Test']], 252 | ['=' => ['attribute' => 'age', 'value' => '19']], 253 | ] 254 | ]; 255 | 256 | ABACUtil::validateAbacRuleArray($rules); 257 | $this->assertTrue(true); 258 | }); 259 | ``` 260 | 261 | ### Integration Tests 262 | 263 | ```php 264 | test('can get filtered model instances', function () { 265 | // Create test data 266 | $order1 = Order::create(['status' => 'active', 'amount' => 100]); 267 | $order2 = Order::create(['status' => 'pending', 'amount' => 200]); 268 | 269 | // Create ABAC rule 270 | $rules = [ 271 | '&&' => [ 272 | ['=' => ['attribute' => 'status', 'value' => 'active']] 273 | ] 274 | ]; 275 | 276 | RoleModelAbacRule::create([ 277 | 'role_id' => $roleId, 278 | 'model_type' => Order::getModelType(), 279 | 'rules_json' => $rules 280 | ]); 281 | 282 | // Test filtering 283 | $this->assertEquals(1, Order::count()); 284 | $this->assertEquals('active', Order::first()->status); 285 | }); 286 | ``` 287 | 288 | ## Common Use Cases 289 | 290 | 1. Status-Based Access: 291 | ```php 292 | [ 293 | '&&' => [ 294 | ['=' => ['attribute' => 'status', 'value' => 'active']] 295 | ] 296 | ] 297 | ``` 298 | 299 | 2. Amount-Based Access: 300 | ```php 301 | [ 302 | '&&' => [ 303 | ['>=' => ['attribute' => 'amount', 'value' => 1000]] 304 | ] 305 | ] 306 | ``` 307 | 308 | 3. Category-Based Access: 309 | ```php 310 | [ 311 | '||' => [ 312 | ['=' => ['attribute' => 'category', 'value' => 'electronics']], 313 | ['=' => ['attribute' => 'category', 'value' => 'books']] 314 | ] 315 | ] 316 | ``` 317 | 318 | 4. Date-Based Access: 319 | ```php 320 | [ 321 | '&&' => [ 322 | ['>=' => ['attribute' => 'created_at', 'value' => '2024-01-01']] 323 | ] 324 | ] 325 | ``` 326 | 327 | 5. Complex Business Rules: 328 | ```php 329 | [ 330 | '&&' => [ 331 | ['=' => ['attribute' => 'status', 'value' => 'active']], 332 | ['>=' => ['attribute' => 'amount', 'value' => 1000]], 333 | ['||' => [ 334 | ['=' => ['attribute' => 'region', 'value' => 'EU']], 335 | ['=' => ['attribute' => 'region', 'value' => 'US']] 336 | ]] 337 | ] 338 | ] 339 | ``` 340 | -------------------------------------------------------------------------------- /database/seeders/SampleDataSeeder.php: -------------------------------------------------------------------------------- 1 | table_name, $ignores)) { 32 | // Get the max id from that table and add 1 to it 33 | $seq = DB::table($table->table_name)->max('id') + 1; 34 | 35 | // alter the sequence to now RESTART WITH the new sequence index from above 36 | DB::select('ALTER SEQUENCE '.$table->table_name.'_id_seq RESTART WITH '.$seq); 37 | } 38 | } 39 | } 40 | 41 | $user1 = User::create( 42 | [ 43 | 'name' => 'Example User 1', 44 | 'email' => 'user1@example.com', 45 | 'password' => Hash::make('password'), 46 | ] 47 | ); 48 | 49 | $user2 = User::create( 50 | [ 51 | 'name' => 'Example User 2', 52 | 'email' => 'user2@example.com', 53 | 'password' => Hash::make('password'), 54 | ] 55 | ); 56 | 57 | $user3 = User::create( 58 | [ 59 | 'name' => 'Example User 3', 60 | 'email' => 'user3@example.com', 61 | 'password' => Hash::make('password'), 62 | ] 63 | ); 64 | 65 | $organizationScope1 = OrganizationScope::whereName('Root Scope')->first(); 66 | 67 | $organizationScope2 = OrganizationScope::create([ 68 | 'name' => 'Sub-Scope', 69 | 'level' => 10, 70 | 'status' => 'active', 71 | ]); 72 | 73 | $organizationScope3 = OrganizationScope::create([ 74 | 'name' => 'Sub-Sub-Scope', 75 | 'level' => 20, 76 | 'status' => 'active', 77 | ]); 78 | 79 | $organizationNode1 = OrganizationNode::whereName('Root Node')->first(); 80 | 81 | $organizationNode2 = OrganizationNode::create( 82 | [ 83 | 'organization_scope_id' => $organizationScope2->id, 84 | 'name' => 'Organization Node 1.2', 85 | 'model_type' => null, 86 | 'model_id' => null, 87 | 'path' => '1/temp', 88 | 'parent_id' => $organizationNode1->id, 89 | ] 90 | ); 91 | $organizationNode2->path = $organizationNode1->id.'/'.$organizationNode2->id; 92 | $organizationNode2->save(); 93 | 94 | $organizationNode3 = OrganizationNode::create( 95 | [ 96 | 'organization_scope_id' => $organizationScope2->id, 97 | 'name' => 'Organization Node 1.3', 98 | 'model_type' => null, 99 | 'model_id' => null, 100 | 'path' => '1/temp', 101 | 'parent_id' => $organizationNode1->id, 102 | ] 103 | ); 104 | $organizationNode3->path = $organizationNode1->id.'/'.$organizationNode3->id; 105 | $organizationNode3->save(); 106 | 107 | $organizationNode4 = OrganizationNode::create( 108 | [ 109 | 'organization_scope_id' => $organizationScope3->id, 110 | 'name' => 'Organization Node 1.2.4', 111 | 'model_type' => null, 112 | 'model_id' => null, 113 | 'path' => '1/temp', 114 | 'parent_id' => $organizationNode2->id, 115 | ] 116 | ); 117 | $organizationNode4->path = $organizationNode2->path.'/'.$organizationNode4->id; 118 | $organizationNode4->save(); 119 | 120 | $role1 = Role::create([ 121 | 'type' => 'system', 122 | 'name' => 'System Role 1', 123 | 'status' => 'active', 124 | ]); 125 | 126 | $role2 = Role::create([ 127 | 'type' => 'system', 128 | 'name' => 'System Role 2', 129 | 'status' => 'active', 130 | ]); 131 | 132 | $role3 = Role::create([ 133 | 'organization_scope_id' => $organizationScope1->id, 134 | 'type' => 'organization', 135 | 'name' => 'Root Role 1', 136 | 'status' => 'active', 137 | ]); 138 | 139 | $role4 = Role::create([ 140 | 'organization_scope_id' => $organizationScope1->id, 141 | 'type' => 'organization', 142 | 'name' => 'Root Role 2', 143 | 'status' => 'active', 144 | ]); 145 | 146 | $role5 = Role::create([ 147 | 'organization_scope_id' => $organizationScope2->id, 148 | 'type' => 'organization', 149 | 'name' => 'Sub-Scope Role 1', 150 | 'status' => 'active', 151 | ]); 152 | 153 | $role6 = Role::create([ 154 | 'organization_scope_id' => $organizationScope2->id, 155 | 'type' => 'organization', 156 | 'name' => 'Sub-Scope Role 2', 157 | 'status' => 'active', 158 | ]); 159 | 160 | $role7 = Role::create([ 161 | 'organization_scope_id' => $organizationScope3->id, 162 | 'type' => 'organization', 163 | 'name' => 'Sub-Sub-Scope Role 1', 164 | 'status' => 'active', 165 | ]); 166 | 167 | DB::table('user_role_organization_node')->insert([ 168 | 'user_id' => $user1->id, 169 | 'role_id' => $role1->id, 170 | ]); 171 | 172 | DB::table('user_role_organization_node')->insert([ 173 | 'user_id' => $user1->id, 174 | 'role_id' => $role2->id, 175 | ]); 176 | 177 | DB::table('user_role_organization_node')->insert([ 178 | 'user_id' => $user1->id, 179 | 'role_id' => $role3->id, 180 | 'organization_node_id' => $organizationNode1->id, 181 | ]); 182 | 183 | DB::table('user_role_organization_node')->insert([ 184 | 'user_id' => $user1->id, 185 | 'role_id' => $role5->id, 186 | 'organization_node_id' => $organizationNode2->id, 187 | ]); 188 | 189 | DB::table('user_role_organization_node')->insert([ 190 | 'user_id' => $user1->id, 191 | 'role_id' => $role7->id, 192 | 'organization_node_id' => $organizationNode4->id, 193 | ]); 194 | 195 | $systemPermissions = config('aauth.permissions.system'); 196 | 197 | foreach ($systemPermissions as $key => $val) { 198 | DB::table('role_permission')->insert([ 199 | 'role_id' => $role1->id, 200 | 'permission' => $key, 201 | ]); 202 | } 203 | 204 | foreach ($systemPermissions as $key => $val) { 205 | DB::table('role_permission')->insert([ 206 | 'role_id' => $role2->id, 207 | 'permission' => $key, 208 | ]); 209 | } 210 | 211 | $organizationPermissions = config('aauth.permissions.organization'); 212 | 213 | foreach ($organizationPermissions as $key => $val) { 214 | DB::table('role_permission')->insert([ 215 | 'role_id' => $role3->id, 216 | 'permission' => $key, 217 | ]); 218 | } 219 | 220 | foreach ($organizationPermissions as $key => $val) { 221 | DB::table('role_permission')->insert([ 222 | 'role_id' => $role4->id, 223 | 'permission' => $key, 224 | ]); 225 | } 226 | 227 | foreach ($organizationPermissions as $key => $val) { 228 | DB::table('role_permission')->insert([ 229 | 'role_id' => $role5->id, 230 | 'permission' => $key, 231 | ]); 232 | } 233 | 234 | foreach ($organizationPermissions as $key => $val) { 235 | DB::table('role_permission')->insert([ 236 | 'role_id' => $role6->id, 237 | 'permission' => $key, 238 | ]); 239 | } 240 | 241 | foreach ($organizationPermissions as $key => $val) { 242 | DB::table('role_permission')->insert([ 243 | 'role_id' => $role7->id, 244 | 'permission' => $key, 245 | ]); 246 | } 247 | 248 | if (config('database.default') == 'pgsql') { 249 | $tables = DB::select('SELECT table_name FROM information_schema.tables WHERE table_schema = \'public\' ORDER BY table_name;'); 250 | 251 | // Set the tables in the database you would like to ignore 252 | $ignores = ['admin_setting', 'model_has_permissions', 'model_has_roles', 'password_resets', 'role_has_permissions', 'sessions', 'cache', 'cache_locks', 'job_batches', 'password_reset_tokens']; 253 | 254 | // loop through the tables 255 | foreach ($tables as $table) { 256 | // if the table is not to be ignored then: 257 | if (! in_array($table->table_name, $ignores)) { 258 | // Get the max id from that table and add 1 to it 259 | $seq = DB::table($table->table_name)->max('id') + 1; 260 | 261 | // alter the sequence to now RESTART WITH the new sequence index from above 262 | DB::select('ALTER SEQUENCE '.$table->table_name.'_id_seq RESTART WITH '.$seq); 263 | } 264 | } 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Services/RolePermissionService.php: -------------------------------------------------------------------------------- 1 | fails()) { 39 | $message = implode(' , ', $validator->getMessageBag()->all()); 40 | 41 | throw new ValidationException($validator, new Response($message, Response::HTTP_UNPROCESSABLE_ENTITY)); 42 | } 43 | } 44 | 45 | return Role::create($role); 46 | } 47 | 48 | /** 49 | * Updates a Perm. 50 | * 51 | * @param array $role 52 | * @param int $id 53 | * @param bool $withValidation 54 | * @return Role|null 55 | */ 56 | public function updateRole(array $role, int $id, bool $withValidation = true): ?Role 57 | { 58 | if ($withValidation) { 59 | $validator = Validator::make($role, UpdateRoleRequest::$rules); 60 | 61 | if ($validator->fails()) { 62 | $message = implode(' , ', $validator->getMessageBag()->all()); 63 | 64 | abort(422, 'Invalid Update Role Request, '.$message); 65 | } 66 | } 67 | $roleModel = Role::find($id); 68 | 69 | return $roleModel->update($role) ? $roleModel : null; 70 | } 71 | 72 | /** 73 | * deletes the role. 74 | * 75 | * @param int $id 76 | * @return bool|null 77 | */ 78 | public function deleteRole(int $id): ?bool 79 | { 80 | return Role::find($id)->delete(); 81 | } 82 | 83 | /** 84 | * activates the roles 85 | * 86 | * @param int $roleId 87 | * @return bool 88 | */ 89 | public function activateRole(int $roleId): bool 90 | { 91 | $role = Role::find($roleId); 92 | $role->status = 'active'; 93 | 94 | return $role->save(); 95 | } 96 | 97 | /** 98 | * deactivates the roles 99 | * 100 | * @param int $roleId 101 | * @return bool 102 | */ 103 | public function deactivateRole(int $roleId): bool 104 | { 105 | $roleId = Role::find($roleId); 106 | $roleId->status = 'passive'; 107 | 108 | return $roleId->save(); 109 | } 110 | 111 | /** 112 | * @param string|array $permissionOrPermissions 113 | * @param int $roleId 114 | * @return bool 115 | */ 116 | public function attachPermissionToRole(string|array $permissionOrPermissions, int $roleId): bool 117 | { 118 | $roleId = Role::find($roleId)->id; 119 | 120 | if (is_array($permissionOrPermissions)) { 121 | foreach ($permissionOrPermissions as $permission) { 122 | $this->attachPermissionToRole($permission, $roleId); 123 | } 124 | } else { 125 | $permissionQueryBuilder = DB::table('role_permission') 126 | ->where('role_id', $roleId) 127 | ->where('permission', $permissionOrPermissions); 128 | 129 | if ($permissionQueryBuilder->doesntExist()) { 130 | return DB::table('role_permission')->insert([ 131 | 'role_id' => $roleId, 132 | 'permission' => $permissionOrPermissions, 133 | ]); 134 | } 135 | } 136 | 137 | return true; 138 | } 139 | 140 | /** 141 | * @param string|array $permissions 142 | * @param int $roleId 143 | * @return bool 144 | */ 145 | public function detachPermissionFromRole(string|array $permissions, int $roleId): bool 146 | { 147 | $roleId = Role::find($roleId)->id; 148 | 149 | if (is_array($permissions)) { 150 | foreach ($permissions as $permission) { 151 | $this->detachPermissionFromRole($permission, $roleId); 152 | } 153 | } else { 154 | DB::table('role_permission')->where([ 155 | 'role_id' => $roleId, 156 | 'permission' => $permissions, 157 | ])->delete(); 158 | } 159 | 160 | return true; 161 | } 162 | 163 | /** 164 | * @param int $roleId 165 | * @return bool 166 | */ 167 | public function detachAllPermissionsFromRole(int $roleId): bool 168 | { 169 | $roleId = Role::find($roleId)->id; 170 | 171 | DB::table('role_permission')->where([ 172 | 'role_id' => $roleId, 173 | ])->delete(); 174 | 175 | return true; 176 | } 177 | 178 | /** 179 | * @param array $permissions 180 | * @param int $roleId 181 | * @return bool 182 | * 183 | * @throws Throwable 184 | */ 185 | public function syncPermissionsOfRole(array $permissions, int $roleId): bool 186 | { 187 | // todo need refactor 188 | $role = Role::find($roleId); 189 | throw_if($role == null, new InvalidRoleException()); 190 | 191 | $detached = $this->detachAllPermissionsFromRole($roleId); 192 | $attached = $this->attachPermissionToRole($permissions, $roleId); 193 | 194 | return $attached && $detached; 195 | } 196 | 197 | /** 198 | * @param int $userId 199 | * @param array $roleIdOrIds 200 | * @return array 201 | * 202 | * @throws Throwable 203 | */ 204 | public function attachSystemRoleToUser(array|int $roleIdOrIds, int $userId): array 205 | { 206 | // todo burası belki user trait'i ile yapılabilir ? 207 | 208 | if (! is_array($roleIdOrIds)) { 209 | $tempRoleId[0] = $roleIdOrIds; 210 | $roleIdOrIds = $tempRoleId; 211 | } 212 | 213 | throw_unless(User::whereId($userId) 214 | ->exists(), new InvalidUserException()); 215 | 216 | throw_unless(Role::whereId($roleIdOrIds) 217 | ->where('type', '=', 'system') 218 | ->exists(), new InvalidRoleException()); 219 | 220 | return User::find($userId)->system_roles()->sync($roleIdOrIds, false); 221 | } 222 | 223 | /** 224 | * @param int $userId 225 | * @param int $roleIdOrIds 226 | * @return int 227 | * 228 | * @throws Throwable 229 | */ 230 | public function detachSystemRoleFromUser(array|int $roleIdOrIds, int $userId): int 231 | { 232 | if (! is_array($roleIdOrIds)) { 233 | $tempRoleId[0] = $roleIdOrIds; 234 | $roleIdOrIds = $tempRoleId; 235 | } 236 | 237 | throw_unless(User::whereId($userId) 238 | ->exists(), new InvalidUserException()); 239 | 240 | throw_unless(Role::whereId($roleIdOrIds) 241 | ->where('type', '=', 'system') 242 | ->exists(), new InvalidRoleException()); 243 | 244 | return User::find($userId)->system_roles()->detach($roleIdOrIds); 245 | } 246 | 247 | /** 248 | * @param int $userId 249 | * @param array $roleIds 250 | * @return array 251 | */ 252 | public function syncUserSystemRoles(int $userId, array $roleIds): array 253 | { 254 | // todo 255 | // to be unit tested 256 | return User::find($userId)->system_roles()->sync($roleIds); 257 | } 258 | 259 | /** 260 | * it makes organization insert and return the pivot table id's 261 | * 262 | * @param int $userId 263 | * @param int $roleId 264 | * @param int $organizationNodeId 265 | * @return bool 266 | * 267 | * @throws Throwable 268 | */ 269 | public function attachOrganizationRoleToUser(int $organizationNodeId, int $roleId, int $userId): bool 270 | { 271 | // todo burası belki user trait'i ile yapılabilir ? 272 | throw_unless(User::whereId($userId) 273 | ->exists(), new InvalidUserException()); 274 | 275 | throw_unless(Role::whereId($roleId) 276 | ->where('type', '=', 'organization') 277 | ->exists(), new InvalidRoleException()); 278 | 279 | throw_unless(OrganizationNode::whereId($organizationNodeId) 280 | ->exists(), new InvalidOrganizationNodeException()); 281 | 282 | return DB::table('user_role_organization_node') 283 | ->updateOrInsert([ 284 | 'user_id' => $userId, 285 | 'role_id' => $roleId, 286 | 'organization_node_id' => $organizationNodeId, 287 | ]); 288 | } 289 | 290 | /** 291 | * @param int $userId 292 | * @param int $roleId 293 | * @param int $organizationNodeId 294 | * @return int 295 | * 296 | * @throws Throwable 297 | */ 298 | public function detachOrganizationRoleFromUser(int $userId, int $roleId, int $organizationNodeId): int 299 | { 300 | // todo burası belki user trait'i ile yapılabilir ? 301 | throw_unless(User::whereId($userId) 302 | ->exists(), new InvalidUserException()); 303 | 304 | throw_unless(Role::whereId($roleId) 305 | ->where('type', '=', 'organization') 306 | ->exists(), new InvalidRoleException()); 307 | 308 | throw_unless(OrganizationNode::whereId($organizationNodeId) 309 | ->exists(), new InvalidOrganizationNodeException()); 310 | 311 | return DB::table('user_role_organization_node') 312 | ->where([ 313 | 'user_id' => $userId, 314 | 'role_id' => $roleId, 315 | 'organization_node_id' => $organizationNodeId, ]) 316 | ->delete(); 317 | // todo attach ve sync ile olmayacak gibi direk db query yazmank lazım 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/AAuth.php: -------------------------------------------------------------------------------- 1 | roles()->where('roles.id', '=', $roleId)->count() < 1, 53 | new UserHasNoAssignedRoleException() 54 | ); 55 | 56 | $this->user = $user; 57 | $this->role = Role::find($roleId); 58 | 59 | throw_unless($this->role, new MissingRoleException()); 60 | 61 | /** 62 | * @var User $user 63 | */ 64 | 65 | $this->organizationNodeIds = DB::table('user_role_organization_node') 66 | ->where('user_id', '=', $user->id) 67 | ->where('role_id', '=', $roleId) 68 | ->pluck('organization_node_id')->toArray(); 69 | } 70 | 71 | /** 72 | * @return Role|null 73 | */ 74 | public function currentRole(): ?Role 75 | { 76 | // todo unit test 77 | return $this->role; 78 | } 79 | 80 | /** 81 | * @return array|Collection|\Illuminate\Support\Collection 82 | */ 83 | public function switchableRoles(): array|Collection|\Illuminate\Support\Collection 84 | { 85 | // @phpstan-ignore-next-line 86 | return Role::where('uro.user_id', '=', $this->user->id) 87 | ->leftJoin('user_role_organization_node as uro', 'uro.role_id', '=', 'roles.id') 88 | ->distinct() 89 | ->select('roles.id', 'name')->get(); 90 | } 91 | 92 | /** 93 | * @param int $userId 94 | * @return array|Collection|\Illuminate\Support\Collection 95 | */ 96 | public static function switchableRolesStatic(int $userId): array|Collection|\Illuminate\Support\Collection 97 | { 98 | // todo test'i yazılacak 99 | return Role::where('uro.user_id', '=', $userId) 100 | ->leftJoin('user_role_organization_node as uro', 'uro.role_id', '=', 'roles.id') 101 | ->distinct() 102 | ->select('roles.id', 'name')->get(); 103 | } 104 | 105 | /** 106 | * Role's all permissions 107 | * 108 | * @return array 109 | */ 110 | public function permissions(): array 111 | { 112 | return Role::where('roles.id', '=', $this->role->id) 113 | ->leftJoin('role_permission as rp', 'rp.role_id', '=', 'roles.id') 114 | ->pluck('permission')->toArray(); 115 | } 116 | 117 | /** 118 | * @return array 119 | */ 120 | public function organizationPermissions(): array 121 | { 122 | return Role::where('roles.id', '=', $this->role->id) 123 | ->where('type', '=', 'organization') 124 | ->leftJoin('role_permission as rp', 'rp.role_id', '=', 'roles.id') 125 | ->pluck('permission')->toArray(); 126 | } 127 | 128 | /** 129 | * @return array 130 | */ 131 | public function systemPermissions(): array 132 | { 133 | return Role::where('roles.id', '=', $this->role->id) 134 | ->where('type', '=', 'system') 135 | ->leftJoin('role_permission as rp', 'rp.role_id', '=', 'roles.id') 136 | ->pluck('permission')->toArray(); 137 | } 138 | 139 | /** 140 | * check if user can 141 | * 142 | * @param string $permission 143 | * @return bool 144 | */ 145 | public function can(string $permission): bool 146 | { 147 | $permissions = Context::get('role_permissions'); 148 | 149 | if (is_null($permissions)) { 150 | $permissions = Role::where('roles.id', '=', $this->role->id) 151 | ->leftJoin('role_permission as rp', 'rp.role_id', '=', 'roles.id') 152 | ->select('rp.permission as permission_from_rp') 153 | ->pluck('permission_from_rp') 154 | ->toArray(); 155 | 156 | Context::add('role_permissions', $permissions); 157 | } 158 | 159 | return in_array($permission, $permissions); 160 | } 161 | 162 | /** 163 | * @param string $permission 164 | * @param string $message 165 | * @return void 166 | */ 167 | public function passOrAbort(string $permission, string $message = 'No Permission'): void 168 | { 169 | // todo mesaj dil dosyasından gelecek. 170 | if (! $this->can($permission)) { 171 | abort(ResponseAlias::HTTP_UNAUTHORIZED, $message); 172 | } 173 | } 174 | 175 | /** 176 | * Returns user's current role's authorized organization nodes 177 | * if model type is given, returns only this model typed nodes. 178 | * 179 | * @param bool $includeRootNode 180 | * @param string|null $modelType 181 | * @return \Illuminate\Support\Collection 182 | * 183 | * @throws Throwable 184 | */ 185 | public function organizationNodes(bool $includeRootNode = false, ?string $modelType = null): \Illuminate\Support\Collection 186 | { 187 | // todo scope eklenecek. $scopeLevel $scopeName 188 | // todo depth ler eklenecek $maxDepthFromRoot $minDepthFromRoot 189 | 190 | return OrganizationNode::where(function ($query) use ($includeRootNode) { 191 | foreach ($this->organizationNodeIds as $organizationNodeId) { 192 | $rootNode = OrganizationNode::find($organizationNodeId); 193 | throw_unless($rootNode, new InvalidOrganizationNodeException()); 194 | 195 | /** 196 | * @phpstan-ignore-next-line 197 | */ 198 | $query->orWhere('path', 'like', $rootNode->path . '/%'); 199 | 200 | if ($includeRootNode) { 201 | /** 202 | * @phpstan-ignore-next-line 203 | */ 204 | $query->orWhere('path', $rootNode->path); 205 | } 206 | 207 | } 208 | }) 209 | ->when($modelType !== null, function ($query) use ($modelType) { 210 | return $query->where('model_type', '=', $modelType); 211 | })->get(); 212 | } 213 | 214 | /** 215 | * @param bool $includeRootNode 216 | * @param string|null $modelType 217 | * @return OrganizationNode|Builder 218 | * @throws Throwable 219 | */ 220 | public function organizationNodesQuery(bool $includeRootNode = false, ?string $modelType = null): OrganizationNode|Builder 221 | { 222 | $rootNodes = OrganizationNode::whereIn('id', $this->organizationNodeIds)->get(); 223 | throw_unless($rootNodes->isNotEmpty(), new InvalidOrganizationNodeException()); 224 | 225 | return OrganizationNode::where(function ($query) use ($rootNodes, $includeRootNode) { 226 | foreach ($rootNodes as $rootNode) { 227 | $query->orWhere('path', 'like', $rootNode->path . '/%'); 228 | 229 | if ($includeRootNode) { 230 | $query->orWhere('path', '=', $rootNode->path); 231 | } 232 | } 233 | })->when($modelType !== null, function ($query) use ($modelType) { 234 | return $query->where('model_type', '=', $modelType); 235 | }); 236 | } 237 | 238 | /** 239 | * checks if current role authorized to access given node id 240 | * 241 | * @param int $nodeId 242 | * @param string|null $modelType 243 | * @return OrganizationNode 244 | * 245 | * @throws InvalidOrganizationNodeException|Throwable 246 | */ 247 | public function organizationNode(int $nodeId, ?string $modelType = null): OrganizationNode 248 | { 249 | $organizationNodes = $this->organizationNodes(true, $modelType); 250 | 251 | foreach ($organizationNodes as $organizationNode) { 252 | if ($nodeId == $organizationNode->id) { 253 | return $organizationNode; 254 | } 255 | } 256 | 257 | /* 258 | if ($organizationNodes->contains(fn($node, $key) => $node->id == $nodeId)) { 259 | return OrganizationNode::findOrFail($nodeId)->first(); 260 | } 261 | */ 262 | throw new InvalidOrganizationNodeException(); 263 | } 264 | 265 | /** 266 | * @return array|null 267 | */ 268 | public function organizationNodeIds(): ?array 269 | { 270 | return $this->organizationNodeIds; 271 | } 272 | 273 | /** 274 | * Checks if tree has given child 275 | * No permission check. 276 | * 277 | * @param int $rootNodeId 278 | * @param int $childNodeId 279 | * @return bool 280 | * 281 | * @throws Throwable 282 | */ 283 | public function descendant(int $rootNodeId, int $childNodeId): bool 284 | { 285 | $subTreeRootNode = OrganizationNode::find($rootNodeId); 286 | throw_unless($subTreeRootNode, new InvalidOrganizationNodeException()); 287 | 288 | return OrganizationNode::where('path', 'like', $subTreeRootNode->path . '%') 289 | ->where('id', '=', $childNodeId)->exists(); 290 | } 291 | 292 | /** 293 | * @param string $modelType 294 | * @return array|null 295 | */ 296 | public function ABACRules(string $modelType): ?array 297 | { 298 | return RoleModelAbacRule::where('role_id', '=', $this->role->id) 299 | ->where('model_type', '=', $modelType) 300 | ->first()?->rules_json; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /.cursor/rules/aauth-orbac.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: 3 | globs: 4 | alwaysApply: true 5 | --- 6 | # AAuth Package Usage Rules 7 | 8 | ## OrBAC Core Concepts 9 | 10 | ### Organization Structure 11 | - Every model that needs organization-based access control must implement the organization node structure 12 | - Organization nodes form a hierarchical tree with unlimited depth 13 | - Each node can be polymorphically related to any Eloquent model 14 | - Organization scopes determine the level and access boundaries 15 | - Each node can have multiple roles and permissions assigned 16 | - Supports multi-tenant applications with data isolation 17 | 18 | ### Understanding Organization Nodes 19 | 20 | #### What is an Organization Node? 21 | An Organization Node is any Laravel Eloquent model that implements `AuroraWebSoftware\AAuth\Contracts\OrganizationNodeContract` interface and uses `AuroraWebSoftware\AAuth\Traits\OrganizationNode` trait. When a model implements these requirements, it automatically becomes an organization node and gains the ability to participate in the hierarchical access control system. The model will be automatically filtered based on the user's organization permissions and role assignments. 22 | 23 | #### Important Note About Relationships 24 | When implementing organization nodes, it's crucial to understand that hierarchical relationships between nodes should be established through the `organization_nodes` table, not through Laravel's standard relationships (like hasMany, belongsTo, etc.). While you can still use Laravel's relationships for other purposes (like data integrity or convenience), the actual hierarchical structure and access control should be managed through the organization_nodes table. 25 | 26 | #### Organization Scope Requirement 27 | Every organization node must have an organization scope. The organization scope determines the level and boundaries of access control for that node. This is implemented through the `organizationScope()` relationship in the OrganizationNode trait. 28 | 29 | For example: 30 | ```php 31 | // ❌ Missing organization scope 32 | class School extends Model implements OrganizationNodeContract 33 | { 34 | use OrganizationNode; 35 | // Missing organizationScope relationship 36 | } 37 | 38 | // ✅ Correct implementation with organization scope 39 | class School extends Model implements OrganizationNodeContract 40 | { 41 | use OrganizationNode; 42 | 43 | public function organizationScope() 44 | { 45 | return $this->belongsTo(OrganizationScope::class); 46 | } 47 | } 48 | ``` 49 | 50 | The organization scope is used to: 51 | 1. Define the access boundaries for the node 52 | 2. Control which users can access the node 53 | 3. Determine the level of access control 54 | 4. Manage permission inheritance 55 | 5. Control data visibility across the organization hierarchy 56 | 57 | #### Database Structure 58 | 1. Organization Node Table: 59 | - Each model that implements OrganizationNodeContract has a corresponding record in the `organization_nodes` table 60 | - The relationship is polymorphic (morphable) 61 | - Contains organization scope information 62 | - Stores hierarchical relationships 63 | - Maintains path information for efficient querying 64 | - Uses proper indexes for performance optimization 65 | 66 | 2. Model Scope Implementation: 67 | - Laravel's global scopes are automatically applied through the OrganizationNode trait 68 | - No need to manually add scopes when the trait is used 69 | - Global scopes handle all organization-based filtering automatically 70 | 71 | #### When to Use Organization Nodes? 72 | 1. Hierarchical Data Access: 73 | - When you need to control access to data based on organizational hierarchy 74 | - When different users should see different subsets of data based on their position 75 | - When you need to implement multi-tenant functionality 76 | - When you need to implement role-based access control at different levels 77 | - When you need data isolation between different organizational units 78 | 79 | 2. Common Use Cases in School System: 80 | - School Districts 81 | - Schools 82 | - Departments 83 | - Classes 84 | - Students 85 | - Teachers 86 | - Administrative Staff 87 | - Programs 88 | - Grade Levels 89 | 90 | #### Which Models Should Be Organization Nodes? 91 | 1. Primary Organization Models: 92 | - School District 93 | - School 94 | - Department 95 | - Administrative Units 96 | - Regional Offices 97 | - Class 98 | - Section 99 | - Grade Level 100 | - Program 101 | - Course 102 | - Student Group 103 | 104 | 105 | 106 | 3. Criteria for Organization Node: 107 | - Model represents a hierarchical unit 108 | - Model needs access control based on hierarchy 109 | - Model's data should be accessible only to specific users/groups 110 | - Model has parent-child relationships 111 | - Model needs to participate in permission inheritance 112 | - Model requires data isolation 113 | 114 | 4. When NOT to Use Organization Nodes: 115 | - Models that don't need hierarchical access control 116 | - Models that are accessible to all users 117 | - Models that don't have organizational context 118 | - Models that don't need data isolation 119 | - Models that are purely reference data 120 | - Models that are used for system-wide settings 121 | 122 | #### Organization Node Implementation Examples 123 | 124 | 1. School Model: 125 | ```php 126 | use AuroraWebSoftware\AAuth\Contracts\OrganizationNodeContract; 127 | use AuroraWebSoftware\AAuth\Traits\OrganizationNode; 128 | 129 | class School extends Model implements OrganizationNodeContract 130 | { 131 | use OrganizationNode; 132 | 133 | protected $fillable = [ 134 | 'name', 135 | 'address', 136 | 'phone', 137 | 'email', 138 | ]; 139 | 140 | /** 141 | * Get the unique identifier of the model. 142 | * This ID is used in two ways: 143 | * 1. Stored in the organization_nodes table as model_id 144 | * 2. Used by AAuth's global scope to filter models based on user's organization permissions 145 | * 146 | * The relationship between model and organization_nodes is polymorphic: 147 | * - model_type: The model's class name 148 | * - model_id: The value returned by this method 149 | * 150 | * @return int The unique identifier of the model 151 | */ 152 | public function getModelId(): int 153 | { 154 | return $this->id; 155 | } 156 | 157 | /** 158 | * Get the display name of the model. 159 | * This name is used in the organization hierarchy display and logs. 160 | * 161 | * @return string|null The display name of the model or null if not set 162 | */ 163 | public function getModelName(): ?string 164 | { 165 | return $this->name; 166 | } 167 | 168 | 169 | } 170 | ``` 171 | 172 | 2. Department Model: 173 | ```php 174 | use AuroraWebSoftware\AAuth\Contracts\OrganizationNodeContract; 175 | use AuroraWebSoftware\AAuth\Traits\OrganizationNode; 176 | 177 | class Department extends Model implements OrganizationNodeContract 178 | { 179 | use OrganizationNode; 180 | 181 | protected $fillable = [ 182 | 'name', 183 | 'code', 184 | 'description', 185 | ]; 186 | 187 | 188 | public function getModelId(): int 189 | { 190 | return $this->id; 191 | } 192 | 193 | public function getModelName(): ?string 194 | { 195 | return $this->name; 196 | } 197 | } 198 | ``` 199 | 200 | 3. Student Model: 201 | ```php 202 | use AuroraWebSoftware\AAuth\Contracts\OrganizationNodeContract; 203 | use AuroraWebSoftware\AAuth\Traits\OrganizationNode; 204 | 205 | class Student extends Model implements OrganizationNodeContract 206 | { 207 | use OrganizationNode; 208 | 209 | protected $fillable = [ 210 | 'first_name', 211 | 'last_name', 212 | 'student_id', 213 | 'grade_level', 214 | ]; 215 | 216 | 217 | public function getModelId(): int 218 | { 219 | return $this->id; 220 | } 221 | 222 | public function getModelName(): ?string 223 | { 224 | return $this->first_name . ' ' . $this->last_name; 225 | } 226 | } 227 | ``` 228 | 229 | #### How Organization Node Filtering Works 230 | 231 | 1. Materialized Path Structure: 232 | - Each organization node has a `path` column in the `organization_nodes` table 233 | - Path format: `1/5/7/` (parent IDs separated by slashes) 234 | - Example hierarchy: 235 | ``` 236 | ID: 1 (School District) 237 | ├── ID: 5 (School A) 238 | │ ├── ID: 7 (Department A) 239 | │ │ └── ID: 9 (Student 1) 240 | │ └── ID: 8 (Department B) 241 | │ └── ID: 10 (Student 2) 242 | └── ID: 6 (School B) 243 | ├── ID: 11 (Department A) 244 | └── ID: 12 (Department B) 245 | ``` 246 | - Path examples: 247 | - School District (ID: 1): `1/` 248 | - School A (ID: 5): `1/5/` 249 | - Department A (ID: 7): `1/5/7/` 250 | - Student 1 (ID: 9): `1/5/7/9/` 251 | 252 | 2. Access Control Flow: 253 | - Teacher is assigned a role for a specific department 254 | - System stores the department's path in teacher's accessible paths 255 | - Global scope automatically filters models based on these paths 256 | - Teacher can access: 257 | - The assigned department 258 | - All students in that department 259 | - All related department data 260 | - Access is automatically inherited down the hierarchy 261 | - Access can be restricted to specific levels 262 | - Permissions are cached for better performance 263 | 264 | #### Organization Node Benefits 265 | 266 | 1. Hierarchical Access Control: 267 | - Control who can access what data 268 | - Implement role-based access at different levels 269 | - Manage permissions across the organization tree 270 | 271 | 2. Data Isolation: 272 | - Keep data separate between different units 273 | - Prevent unauthorized access across boundaries 274 | - Maintain data privacy and security 275 | 276 | 3. Flexible Structure: 277 | - Support unlimited depth of hierarchy 278 | - Allow different types of organizational units 279 | - Enable dynamic organizational changes 280 | - Support complex organizational structures 281 | - Adapt to changing business requirements 282 | 283 | 284 | #### Common Organization Node Patterns 285 | 286 | School System Hierarchy: 287 | ``` 288 | School District (Root) 289 | ├── School A 290 | │ ├── Department A 291 | │ │ └── Student 1 292 | │ └── Department B 293 | │ └── Student 2 294 | └── School B 295 | ├── Department A 296 | └── Department B 297 | ``` 298 | 299 | ## Implementation Rules 300 | 301 | ### Model Requirements 302 | 303 | 1. User Model Requirements: 304 | - Must implement `AuroraWebSoftware\AAuth\Contracts\AAuthUserContract` 305 | - Must use `AuroraWebSoftware\AAuth\Traits\AAuthUser` trait 306 | - Must extend `Illuminate\Foundation\Auth\User` 307 | - Must support role switching functionality 308 | 309 | 2. Organization Node Model Requirements: 310 | - Must implement `AuroraWebSoftware\AAuth\Contracts\OrganizationNodeContract` 311 | - Must use `AuroraWebSoftware\AAuth\Traits\OrganizationNode` trait 312 | - Must implement `getModelId()` and `getModelName()` methods 313 | -------------------------------------------------------------------------------- /resources/boost/guidelines/core.blade.php: -------------------------------------------------------------------------------- 1 | ## AAuth 2 | 3 | AAuth is a comprehensive Laravel authentication and authorization package that combines Organization-Based Access Control (OrBAC), Role-Based Access Control (RBAC), and Attribute-Based Access Control (ABAC) in a single, powerful solution. It provides limitless hierarchical organization levels and attribute-based filtering for fine-grained access control. 4 | 5 | ### Features 6 | 7 | - **Organization-Based Access Control (OrBAC)**: Hierarchical organization tree structure with unlimited levels 8 | - **Role-Based Access Control (RBAC)**: System and Organization roles with permissions 9 | - **Attribute-Based Access Control (ABAC)**: Model-level attribute filtering with JSON rules 10 | - **Automatic Data Filtering**: Global scopes automatically filter data based on user's authorized organization nodes 11 | - **Polymorphic Relationships**: Models can be linked to organization nodes polymorphically 12 | - **Blade Directives**: Built-in Blade directives for permission checks in views 13 | - **Multi-Database Support**: MySQL, MariaDB, and PostgreSQL compatible 14 | 15 | ### Installation 16 | 17 | Install AAuth via Composer: 18 | 19 | @verbatim 20 | 21 | composer require aurorawebsoftware/aauth 22 | 23 | @endverbatim 24 | 25 | Publish and run migrations: 26 | 27 | @verbatim 28 | 29 | php artisan migrate 30 | php artisan vendor:publish --tag="aauth-config" 31 | 32 | @endverbatim 33 | 34 | ### User Model Setup 35 | 36 | The User model must implement `AAuthUserContract` and use the `AAuthUser` trait; 37 | 38 | @verbatim 39 | 40 | use Illuminate\Foundation\Auth\User as Authenticatable; 41 | use AuroraWebSoftware\AAuth\Traits\AAuthUser; 42 | use AuroraWebSoftware\AAuth\Contracts\AAuthUserContract; 43 | 44 | class User extends Authenticatable implements AAuthUserContract 45 | { 46 | use AAuthUser; 47 | 48 | // Your model code... 49 | } 50 | 51 | @endverbatim 52 | 53 | ### Making Models Organization-Controllable 54 | 55 | To make an Eloquent model controllable by organization hierarchy, implement `AAuthOrganizationNodeInterface` and use the `AAuthOrganizationNode` trait: 56 | 57 | @verbatim 58 | 59 | use AuroraWebSoftware\AAuth\Interfaces\AAuthOrganizationNodeInterface; 60 | use AuroraWebSoftware\AAuth\Traits\AAuthOrganizationNode; 61 | use Illuminate\Database\Eloquent\Model; 62 | 63 | class School extends Model implements AAuthOrganizationNodeInterface 64 | { 65 | use AAuthOrganizationNode; 66 | 67 | public static function getModelType(): string 68 | { 69 | return self::class; 70 | } 71 | 72 | public function getModelId(): int 73 | { 74 | return $this->id; 75 | } 76 | 77 | public function getModelName(): ?string 78 | { 79 | return $this->name; 80 | } 81 | } 82 | 83 | @endverbatim 84 | 85 | Once a model uses `AAuthOrganizationNode` trait, it automatically filters data based on the current user's authorized organization nodes. Queries like `School::all()` will only return schools the user has access to. 86 | 87 | ### How Organization-Based Filtering Works 88 | 89 | AAuth uses the **Materialized Path Pattern** combined with **Laravel Global Scopes** to automatically filter every database query based on the user's authorized organization nodes. This ensures that users can only access data within their organizational boundaries without writing any additional filtering code. 90 | 91 | #### Understanding Materialized Path Pattern 92 | 93 | The **Materialized Path Pattern** stores the full path from root to each node in a single column. This eliminates the need for recursive queries and makes hierarchical filtering extremely fast. 94 | 95 | **Path Format:** `/parent_id/current_id/` 96 | 97 | **Example Hierarchy with Paths:** 98 | ``` 99 | Organization Node ID: 1 (School District) 100 | ├── Path: /1/ 101 | ├── Organization Node ID: 5 (School A) 102 | │ ├── Path: /1/5/ 103 | │ ├── Organization Node ID: 7 (Mathematics Department) 104 | │ │ ├── Path: /1/5/7/ 105 | │ │ ├── Organization Node ID: 9 (Student: John Doe) 106 | │ │ │ └── Path: /1/5/7/9/ 107 | │ │ └── Organization Node ID: 10 (Student: Jane Smith) 108 | │ │ └── Path: /1/5/7/10/ 109 | │ └── Organization Node ID: 8 (Science Department) 110 | │ ├── Path: /1/5/8/ 111 | │ └── Organization Node ID: 11 (Student: Bob Wilson) 112 | │ └── Path: /1/5/8/11/ 113 | └── Organization Node ID: 6 (School B) 114 | └── Path: /1/6/ 115 | ``` 116 | 117 | **Key Benefits of Materialized Path:** 118 | - **Single Query**: Find all descendants with one LIKE query instead of recursive joins 119 | - **Fast Filtering**: Path matching is index-friendly 120 | - **Simple Logic**: Easy to understand and maintain 121 | - **Scalable**: Works efficiently even with deep hierarchies 122 | 123 | #### How Global Scope Applies Materialized Path Filtering 124 | 125 | When a model uses `AAuthOrganizationNode` trait, Laravel automatically applies the `AAuthOrganizationNodeScope` to **every query** on that model. This happens transparently - you don't need to remember to add filtering. 126 | 127 | **Step-by-Step Process:** 128 | 129 | 1. **User Role Assignment** - User is assigned a role at a specific organization node 130 | 2. **Session Tracking** - Current role ID is stored in session 131 | 3. **Path Resolution** - AAuth finds the path of user's authorized organization nodes 132 | 4. **Query Modification** - Global scope modifies every query to include path-based filtering 133 | 5. **Automatic Filtering** - Only records matching authorized paths are returned 134 | 135 | #### Detailed Example: Teacher Querying Students 136 | 137 | **Setup:** 138 | @verbatim 139 | 140 | // Teacher is assigned "Teacher" role at Mathematics Department (node ID: 7) 141 | $rolePermissionService->attachOrganizationRoleToUser( 142 | organizationNodeId: 7, // Mathematics Department 143 | roleId: $teacherRole->id, 144 | userId: $teacher->id 145 | ); 146 | 147 | // This creates a record in user_role_organization_node: 148 | // user_id: 123, role_id: 5, organization_node_id: 7 149 | 150 | @endverbatim 151 | 152 | **When Teacher Queries Students:** 153 | @verbatim 154 | 155 | // Teacher writes simple query: 156 | $students = Student::all(); 157 | 158 | // AAuth Global Scope automatically transforms this to: 159 | // 160 | // SELECT students.* 161 | // FROM students 162 | // INNER JOIN organization_nodes 163 | // ON organization_nodes.model_id = students.id 164 | // WHERE organization_nodes.model_type = 'App\Models\Student' 165 | // AND ( 166 | // organization_nodes.path LIKE '/1/5/7/%' -- All descendants of node 7 167 | // OR organization_nodes.path = '/1/5/7/' -- Node 7 itself 168 | // ) 169 | // 170 | // Result: Only returns Student ID 9 and 10 (John Doe, Jane Smith) 171 | // Does NOT return Student ID 11 (Bob Wilson from Science Department) 172 | 173 | @endverbatim 174 | 175 | #### Complex Query Examples 176 | 177 | **Example 1: Filtered Query with Additional Conditions** 178 | @verbatim 179 | 180 | // Teacher queries students with additional filters: 181 | $activeStudents = Student::where('status', 'active') 182 | ->where('grade', '>', 70) 183 | ->orderBy('name') 184 | ->get(); 185 | 186 | // AAuth automatically adds organization filtering: 187 | // 188 | // SELECT students.* 189 | // FROM students 190 | // INNER JOIN organization_nodes 191 | // ON organization_nodes.model_id = students.id 192 | // WHERE students.status = 'active' -- Your condition 193 | // AND students.grade > 70 -- Your condition 194 | // AND organization_nodes.model_type = 'App\Models\Student' 195 | // AND ( 196 | // organization_nodes.path LIKE '/1/5/7/%' 197 | // OR organization_nodes.path = '/1/5/7/' 198 | // ) 199 | // ORDER BY students.name 200 | // 201 | // Returns only active students with grade > 70 from Mathematics Department 202 | 203 | @endverbatim 204 | 205 | **Example 2: Relationship Queries** 206 | @verbatim 207 | 208 | // Teacher accesses students through school relationship: 209 | $school = School::find(5); // High School A 210 | $students = $school->students; // Eager loading relationship 211 | 212 | // AAuth filters both School and Student queries: 213 | // 214 | // First query (School): 215 | // SELECT schools.* FROM schools 216 | // INNER JOIN organization_nodes ON organization_nodes.model_id = schools.id 217 | // WHERE schools.id = 5 218 | // AND organization_nodes.model_type = 'App\Models\School' 219 | // AND (organization_nodes.path LIKE '/1/5/7/%' OR organization_nodes.path = '/1/5/7/') 220 | // 221 | // Second query (Students): 222 | // SELECT students.* FROM students 223 | // INNER JOIN organization_nodes ON organization_nodes.model_id = students.id 224 | // WHERE students.school_id = 5 225 | // AND organization_nodes.model_type = 'App\Models\Student' 226 | // AND (organization_nodes.path LIKE '/1/5/7/%' OR organization_nodes.path = '/1/5/7/') 227 | // 228 | // Both queries are automatically filtered! 229 | 230 | @endverbatim 231 | 232 | **Example 3: Aggregations and Counts** 233 | @verbatim 234 | 235 | // Teacher counts students: 236 | $studentCount = Student::count(); 237 | 238 | // AAuth transforms to: 239 | // SELECT COUNT(*) FROM students 240 | // INNER JOIN organization_nodes ON organization_nodes.model_id = students.id 241 | // WHERE organization_nodes.model_type = 'App\Models\Student' 242 | // AND (organization_nodes.path LIKE '/1/5/7/%' OR organization_nodes.path = '/1/5/7/') 243 | // 244 | // Returns: 2 (only students from Mathematics Department) 245 | 246 | // Average grade calculation: 247 | $avgGrade = Student::avg('grade'); 248 | // Automatically calculates average only for authorized students 249 | 250 | @endverbatim 251 | 252 | #### Multiple Organization Nodes Access 253 | 254 | AAuth supports assigning a user to **multiple organization nodes simultaneously**, even within the same organization scope. When a user has roles at multiple organization nodes, AAuth automatically combines all authorized paths using **OR conditions** in the WHERE clause, allowing access to data from all assigned organizations. 255 | 256 | **Key Concept:** 257 | - A user can be assigned to multiple organization nodes **at the same time** 258 | - These nodes can be at the **same organization scope level** or **different levels** 259 | - All assigned nodes' paths are combined with **OR** operators 260 | - The user gets **combined access** to all descendant data from all assigned nodes 261 | 262 | **Example: User Assigned to Multiple Departments** 263 | 264 | @verbatim 265 | 266 | // Organization Structure: 267 | // School District (ID: 1, path: /1/) 268 | // └── High School A (ID: 5, path: /1/5/) 269 | // ├── Mathematics Dept (ID: 7, path: /1/5/7/) 270 | // │ ├── Student: John (ID: 9, path: /1/5/7/9/) 271 | // │ └── Student: Jane (ID: 10, path: /1/5/7/10/) 272 | // └── Science Dept (ID: 8, path: /1/5/8/) 273 | // └── Student: Bob (ID: 11, path: /1/5/8/11/) 274 | 275 | // Assign user to BOTH departments (same organization scope): 276 | $rolePermissionService->attachOrganizationRoleToUser( 277 | organizationNodeId: 7, // Mathematics Dept 278 | roleId: $teacherRole->id, 279 | userId: $teacher->id 280 | ); 281 | 282 | $rolePermissionService->attachOrganizationRoleToUser( 283 | organizationNodeId: 8, // Science Dept 284 | roleId: $teacherRole->id, 285 | userId: $teacher->id 286 | ); 287 | 288 | // Now user has access to BOTH departments 289 | 290 | @endverbatim 291 | 292 | **How OR Conditions Work:** 293 | 294 | When the user queries data, AAuth automatically builds a query that combines all assigned organization node paths with OR conditions: 295 | 296 | @verbatim 297 | 298 | // User queries students: 299 | $students = Student::all(); 300 | 301 | // AAuth internally: 302 | // 1. Gets user's organization node IDs: [7, 8] 303 | // 2. Finds paths: 304 | // - Node 7: /1/5/7/ 305 | // - Node 8: /1/5/8/ 306 | // 3. Builds WHERE clause with OR conditions: 307 | // 308 | // SELECT students.* 309 | // FROM students 310 | // INNER JOIN organization_nodes 311 | // ON organization_nodes.model_id = students.id 312 | // WHERE organization_nodes.model_type = 'App\Models\Student' 313 | // AND ( 314 | // organization_nodes.path LIKE '/1/5/7/%' -- Mathematics Dept descendants 315 | // OR organization_nodes.path = '/1/5/7/' -- Mathematics Dept itself 316 | // OR organization_nodes.path LIKE '/1/5/8/%' -- Science Dept descendants 317 | // OR organization_nodes.path = '/1/5/8/' -- Science Dept itself 318 | // ) 319 | // 320 | // Result: Returns ALL students from BOTH departments 321 | // - John (ID: 9) from Mathematics 322 | // - Jane (ID: 10) from Mathematics 323 | // - Bob (ID: 11) from Science 324 | 325 | @endverbatim 326 | 327 | **Same Organization Scope, Multiple Nodes:** 328 | 329 | This is particularly useful when a user needs access to multiple branches within the same organizational level: 330 | 331 | @verbatim 332 | 333 | // Organization Structure (all under same scope): 334 | // School District (scope level: 1) 335 | // ├── High School A (node ID: 5, path: /1/5/) 336 | // │ ├── Mathematics Dept (node ID: 7, path: /1/5/7/) 337 | // │ └── Science Dept (node ID: 8, path: /1/5/8/) 338 | // └── High School B (node ID: 6, path: /1/6/) 339 | // └── Mathematics Dept (node ID: 12, path: /1/6/12/) 340 | 341 | // Teacher assigned to Mathematics departments in BOTH schools: 342 | $rolePermissionService->attachOrganizationRoleToUser(7, $teacherRole->id, $teacher->id); // HS A Math 343 | $rolePermissionService->attachOrganizationRoleToUser(12, $teacherRole->id, $teacher->id); // HS B Math 344 | 345 | // When querying: 346 | $students = Student::all(); 347 | 348 | // AAuth builds query: 349 | // WHERE ( 350 | // organization_nodes.path LIKE '/1/5/7/%' OR organization_nodes.path = '/1/5/7/' 351 | // OR 352 | // organization_nodes.path LIKE '/1/6/12/%' OR organization_nodes.path = '/1/6/12/' 353 | // ) 354 | // 355 | // Returns students from Mathematics departments in BOTH High School A and High School B 356 | 357 | @endverbatim 358 | 359 | **Internal Implementation:** 360 | 361 | AAuth's `organizationNodes()` method automatically handles multiple node assignments: 362 | 363 | @verbatim 364 | 365 | // Inside AAuth::organizationNodes(): 366 | // 367 | // $this->organizationNodeIds = [7, 8]; // User's assigned node IDs 368 | // 369 | // foreach ($this->organizationNodeIds as $organizationNodeId) { 370 | // $rootNode = OrganizationNode::find($organizationNodeId); 371 | // // Path: /1/5/7/ for node 7 372 | // // Path: /1/5/8/ for node 8 373 | // 374 | // // Each iteration adds OR condition: 375 | // $query->orWhere('path', 'like', $rootNode->path . '/%'); 376 | // $query->orWhere('path', '=', $rootNode->path); // if includeRootNode 377 | // } 378 | // 379 | // Final WHERE clause: 380 | // WHERE ( 381 | // path LIKE '/1/5/7/%' OR path = '/1/5/7/' 382 | // OR 383 | // path LIKE '/1/5/8/%' OR path = '/1/5/8/' 384 | // ) 385 | 386 | @endverbatim 387 | 388 | **Benefits of Multiple Node Assignment:** 389 | 390 | 1. **Flexible Access Control**: Users can access multiple organizational branches without needing a higher-level role 391 | 2. **Granular Permissions**: Different permissions can be assigned per organization node 392 | 3. **Efficient Queries**: Single query returns data from all authorized nodes 393 | 4. **Automatic Combination**: No manual query building needed - AAuth handles OR logic automatically 394 | 395 | **Important Notes:** 396 | 397 | - All assigned nodes are **combined with OR**, not AND 398 | - User gets access to **all descendants** of each assigned node 399 | - Path matching is **automatic** - no manual path construction needed 400 | - Works seamlessly with **global scopes** - filtering happens transparently 401 | - **Same role** can be assigned to multiple nodes, or **different roles** can be assigned to different nodes 402 | 403 | #### Path-Based Descendant Access Logic 404 | 405 | The materialized path pattern automatically grants access to **all descendants** using simple LIKE pattern matching: 406 | 407 | @verbatim 408 | 409 | // User has role at node 7 (path: /1/5/7/) 410 | $authorizedPath = '/1/5/7/'; 411 | 412 | // AAuth uses LIKE pattern to find all descendants: 413 | // WHERE path LIKE '/1/5/7/%' 414 | // 415 | // This matches: 416 | // ✅ /1/5/7/ (node 7 itself, if includeRootNode = true) 417 | // ✅ /1/5/7/9/ (direct child) 418 | // ✅ /1/5/7/10/ (direct child) 419 | // ✅ /1/5/7/12/15/ (nested descendant, any depth) 420 | // 421 | // Does NOT match: 422 | // ❌ /1/5/8/ (sibling node - different path) 423 | // ❌ /1/6/ (different branch) 424 | // ❌ /1/5/ (parent - path doesn't start with authorized path) 425 | 426 | @endverbatim 427 | 428 | #### Real-World Scenario: School System 429 | 430 | **Complete Example:** 431 | @verbatim 432 | 433 | // Organization Structure: 434 | // School District (ID: 1, path: /1/) 435 | // ├── High School A (ID: 5, path: /1/5/) 436 | // │ ├── Mathematics Dept (ID: 7, path: /1/5/7/) 437 | // │ │ ├── Student: John (ID: 9, path: /1/5/7/9/) 438 | // │ │ └── Student: Jane (ID: 10, path: /1/5/7/10/) 439 | // │ └── Science Dept (ID: 8, path: /1/5/8/) 440 | // │ └── Student: Bob (ID: 11, path: /1/5/8/11/) 441 | // └── High School B (ID: 6, path: /1/6/) 442 | // └── Mathematics Dept (ID: 12, path: /1/6/12/) 443 | 444 | // Scenario 1: Mathematics Teacher at High School A 445 | $mathTeacher = User::find(1); 446 | $rolePermissionService->attachOrganizationRoleToUser(7, $teacherRole->id, $mathTeacher->id); 447 | 448 | // Teacher queries: 449 | $myStudents = Student::all(); 450 | // Returns: John (ID: 9), Jane (ID: 10) 451 | // Does NOT return: Bob (different dept), or students from High School B 452 | 453 | // Scenario 2: Principal at High School A 454 | $principal = User::find(2); 455 | $rolePermissionService->attachOrganizationRoleToUser(5, $principalRole->id, $principal->id); 456 | 457 | // Principal queries: 458 | $schoolStudents = Student::all(); 459 | // Returns: John, Jane, Bob (all students from High School A) 460 | // Does NOT return: students from High School B 461 | 462 | // Scenario 3: District Administrator 463 | $admin = User::find(3); 464 | $rolePermissionService->attachOrganizationRoleToUser(1, $adminRole->id, $admin->id); 465 | 466 | // Admin queries: 467 | $allStudents = Student::all(); 468 | // Returns: ALL students from entire district (all descendants of node 1) 469 | 470 | @endverbatim 471 | 472 | #### Bypassing Organization Filtering 473 | 474 | Sometimes you need to access all records (e.g., for system administrators or reports): 475 | 476 | @verbatim 477 | 478 | use AuroraWebSoftware\AAuth\Scopes\AAuthOrganizationNodeScope; 479 | 480 | // Remove only organization scope 481 | $allSchools = School::withoutGlobalScope(AAuthOrganizationNodeScope::class)->get(); 482 | 483 | // Remove all global scopes (including ABAC if applied) 484 | $allSchools = School::withoutGlobalScopes()->get(); 485 | 486 | // Use in queries 487 | $totalCount = School::withoutGlobalScope(AAuthOrganizationNodeScope::class)->count(); 488 | 489 | @endverbatim 490 | 491 | #### Performance Optimization 492 | 493 | The materialized path pattern is highly optimized: 494 | 495 | **1. Index-Friendly Queries:** 496 | @verbatim 497 | 498 | -- Recommended index for organization_nodes table: 499 | CREATE INDEX idx_organization_nodes_path ON organization_nodes(path); 500 | CREATE INDEX idx_organization_nodes_model ON organization_nodes(model_type, model_id); 501 | 502 | -- LIKE queries with leading path are index-friendly: 503 | -- WHERE path LIKE '/1/5/7/%' ✅ Uses index 504 | -- WHERE path LIKE '%/7/%' ❌ Cannot use index efficiently 505 | 506 | @endverbatim 507 | 508 | **2. Single Query Advantage:** 509 | - **Materialized Path**: One query with LIKE pattern 510 | - **Adjacency List**: Requires recursive CTE or multiple queries 511 | - **Nested Sets**: Complex queries, difficult to maintain 512 | 513 | **3. Caching Opportunities:** 514 | @verbatim 515 | 516 | // Cache user's authorized paths to avoid repeated queries 517 | $cacheKey = "user_{$userId}_role_{$roleId}_paths"; 518 | $authorizedPaths = Cache::remember($cacheKey, 3600, function () use ($userId, $roleId) { 519 | return AAuth::organizationNodes()->pluck('path')->toArray(); 520 | }); 521 | 522 | @endverbatim 523 | 524 | #### Key Takeaways 525 | 526 | 1. **Every Query is Filtered**: Global scope applies to ALL queries automatically 527 | 2. **No Manual Filtering Needed**: You write `Student::all()` and filtering happens automatically 528 | 3. **Path-Based Hierarchy**: Materialized path makes descendant queries fast and simple 529 | 4. **Transparent Operation**: Works with relationships, aggregations, and complex queries 530 | 5. **Automatic Descendant Access**: Users automatically get access to all child nodes 531 | 6. **Multiple Roles Supported**: Users with multiple roles get combined access 532 | 7. **Bypass When Needed**: Use `withoutGlobalScope()` for admin/system operations 533 | 534 | ### Creating Organization Structure 535 | 536 | Use `OrganizationService` to create organization scopes and nodes: 537 | 538 | @verbatim 539 | 540 | use AuroraWebSoftware\AAuth\Services\OrganizationService; 541 | 542 | $organizationService = new OrganizationService(); 543 | 544 | $scope = $organizationService->createOrganizationScope([ 545 | 'name' => 'School System', 546 | 'level' => 1, 547 | 'status' => 'active', 548 | ]); 549 | 550 | @endverbatim 551 | 552 | @verbatim 553 | 554 | $node = $organizationService->createOrganizationNode([ 555 | 'name' => 'High School A', 556 | 'organization_scope_id' => $scope->id, 557 | 'parent_id' => null, // Root node 558 | ]); 559 | 560 | @endverbatim 561 | 562 | ### Creating Roles and Permissions 563 | 564 | Use `RolePermissionService` to manage roles and permissions: 565 | 566 | @verbatim 567 | 568 | use AuroraWebSoftware\AAuth\Services\RolePermissionService; 569 | use AuroraWebSoftware\AAuth\Models\OrganizationScope; 570 | 571 | $rolePermissionService = new RolePermissionService(); 572 | $organizationScope = OrganizationScope::whereName('Root Scope')->first(); 573 | 574 | $role = $rolePermissionService->createRole([ 575 | 'organization_scope_id' => $organizationScope->id, 576 | 'type' => 'system', // or 'organization' 577 | 'name' => 'System Administrator', 578 | 'status' => 'active', 579 | ]); 580 | 581 | @endverbatim 582 | 583 | @verbatim 584 | 585 | // Attach single permission 586 | $rolePermissionService->attachPermissionToRole('edit_something', $role->id); 587 | 588 | // Attach multiple permissions 589 | $rolePermissionService->attachPermissionToRole([ 590 | 'create_something', 591 | 'edit_something', 592 | 'delete_something', 593 | ], $role->id); 594 | 595 | // Sync permissions (removes all existing and adds new ones) 596 | $rolePermissionService->syncPermissionsOfRole([ 597 | 'create_something', 598 | 'edit_something', 599 | ], $role->id); 600 | 601 | @endverbatim 602 | 603 | ### Assigning Roles to Users 604 | 605 | @verbatim 606 | 607 | // System roles are organization-independent 608 | $rolePermissionService->attachSystemRoleToUser($role->id, $user->id); 609 | 610 | @endverbatim 611 | 612 | @verbatim 613 | 614 | use AuroraWebSoftware\AAuth\Models\OrganizationNode; 615 | 616 | $organizationNode = OrganizationNode::whereName('High School A')->first(); 617 | 618 | // Organization roles require an organization node 619 | $rolePermissionService->attachOrganizationRoleToUser( 620 | $organizationNode->id, 621 | $role->id, 622 | $user->id 623 | ); 624 | 625 | @endverbatim 626 | 627 | ### Using AAuth Facade 628 | 629 | The AAuth facade provides methods to check permissions and access organization nodes: 630 | 631 | @verbatim 632 | 633 | use AuroraWebSoftware\AAuth\Facades\AAuth; 634 | 635 | // Check if user has permission 636 | if (AAuth::can('edit_something')) { 637 | // User has permission 638 | } 639 | 640 | // Get all permissions for current role 641 | $permissions = AAuth::permissions(); 642 | 643 | // Abort if user doesn't have permission to check inside contoller or inside services 644 | AAuth::passOrAbort('edit_something', 'You do not have permission'); 645 | 646 | @endverbatim 647 | 648 | @verbatim 649 | 650 | // Get all authorized organization nodes 651 | $nodes = AAuth::organizationNodes(); 652 | 653 | // Include root nodes 654 | $nodes = AAuth::organizationNodes(includeRootNode: true); 655 | 656 | // Filter by model type 657 | $schoolNodes = AAuth::organizationNodes(modelType: School::class); 658 | 659 | // Get query builder for custom queries to attach any query 660 | $query = AAuth::organizationNodesQuery(modelType: School::class); 661 | 662 | @endverbatim 663 | 664 | ### Creating Models with Organization Nodes 665 | 666 | When creating a model that implements `AAuthOrganizationNodeInterface`, you can create both the model and its organization node together: 667 | 668 | @verbatim 669 | 670 | $school = School::createWithAAuthOrganizationNode( 671 | ['name' => 'New School', 'address' => '123 Main St'], 672 | $parentOrganizationNodeId, 673 | $organizationScopeId 674 | ); 675 | 676 | @endverbatim 677 | 678 | ### Session-Based Role Selection 679 | 680 | AAuth uses session to track the current active role. Before using AAuth, ensure the `roleId` is set in the session: 681 | 682 | @verbatim 683 | 684 | use Illuminate\Support\Facades\Session; 685 | 686 | Session::put('roleId', $role->id); 687 | 688 | @endverbatim 689 | 690 | ### Permission Configuration 691 | 692 | Permissions are defined in `config/aauth.php`: 693 | 694 | @verbatim 695 | 696 | return [ 697 | 'permissions' => [ 698 | 'system' => [ 699 | 'edit_something_for_system' => 'aauth/system.edit_something_for_system', 700 | 'create_something_for_system' => 'aauth/system.create_something_for_system', 701 | ], 702 | 'organization' => [ 703 | 'edit_something_for_organization' => 'aauth/organization.edit_something_for_organization', 704 | 'create_something_for_organization' => 'aauth/organization.create_something_for_organization', 705 | ], 706 | ], 707 | ]; 708 | 709 | @endverbatim 710 | 711 | ### Best Practices 712 | 713 | 1. **Always set roleId in session** before using AAuth facade or service 714 | 2. **Use organization roles** for data that belongs to organizational hierarchy 715 | 3. **Use system roles** for global permissions that don't depend on organization 716 | 4. **Implement interface methods correctly** when using `AAuthOrganizationNode` trait 717 | 5. **Use `withoutGlobalScopes()`** when you need to bypass organization and abac filtering 718 | 6. **Validate organization scope levels** when creating hierarchical structures 719 | 7. **Use transactions** when creating models with organization nodes to ensure data consistency 720 | 721 | ### Important Notes 722 | 723 | - System permissions can only be assigned to system roles 724 | - Organization permissions can only be assigned to organization roles 725 | - Models using `AAuthOrganizationNode` trait automatically filter data via global scope 726 | - Organization nodes use path-based hierarchy (e.g., `/1/2/3`) 727 | - Users can have multiple roles, but only one active role per session 728 | 729 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AAuth for Laravel 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/aurorawebsoftware/aauth.svg?style=flat-square)](https://packagist.org/packages/aurorawebsoftware/aauth) 4 | [![Tests](https://github.com/aurorawebsoftware/aauth/actions/workflows/run-tests.yml/badge.svg?branch=main)](https://github.com/aurorawebsoftware/aauth/actions/workflows/run-tests.yml) 5 | [![Code Style](https://github.com/aurorawebsoftware/aauth/actions/workflows/php-cs-fixer.yml/badge.svg?branch=main)](https://github.com/aurorawebsoftware/aauth/actions/workflows/php-cs-fixer.yml) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/aurorawebsoftware/aauth.svg?style=flat-square)](https://packagist.org/packages/aurora/aauth) 7 | 8 | Organization Based (OrBAC) , Attibute Based (ABAC) , Rol-Permission (RBAC) Based Authentication Methods Combined **Laravel Auth Package** with Limitless Hierarchical Level of Organizations and Limitless Attribute Conditions 9 | 10 | # Features 11 | 12 | - Organization Based Access Controllable (OrBAC) Eloquent Models 13 | - Attribute Based Access Controllable (ABAC) Eloquent Models 14 | - Role Based Access Control (RoBAC) 15 | - Permissions Based Access Control 16 | - Lean & Non-Complex Architecture 17 | - PolyMorphic Relationships of Model & Organization Node 18 | - DB Row Level Filtering for the Role with ABAC 19 | - Built-in Blade Directives for permission control inside **Blade** files 20 | - Mysql, MariaDB, Postgres Support 21 | - Community Driven and Open Source Forever 22 | 23 | --- 24 | 25 | 26 | # Installation 27 | 28 | You can install the package via composer: 29 | 30 | ```bash 31 | composer require aurorawebsoftware/aauth 32 | ``` 33 | 34 | You must add AAuthUser Trait to the User Model and User Model must implement AAuthUserContract 35 | 36 | ```php 37 | use Illuminate\Foundation\Auth\User as Authenticatable; 38 | use AuroraWebSoftware\AAuth\Traits\AAuthUser; 39 | use AuroraWebSoftware\AAuth\Contracts\AAuthUserContract; 40 | 41 | class User extends Authenticatable implements AAuthUserContract 42 | { 43 | use AAuthUser; 44 | 45 | // ... 46 | } 47 | ``` 48 | 49 | You can publish and run the migrations with: 50 | 51 | ```bash 52 | php artisan migrate 53 | ``` 54 | 55 | You can publish the sample data seeder with: 56 | 57 | ```bash 58 | php artisan vendor:publish --tag="aauth-seeders" 59 | php artisan db:seed --class=SampleDataSeeder 60 | ``` 61 | 62 | Optionally, You can seed the sample data with: 63 | 64 | ```bash 65 | php artisan db:seed --class=SampleDataSeeder 66 | ``` 67 | 68 | You can publish the config file with: 69 | 70 | ```bash 71 | php artisan vendor:publish --tag="aauth-config" 72 | ``` 73 | 74 | This is the example contents of the published config file: 75 | 76 | ```php 77 | return [ 78 | 'permissions' => [ 79 | 'system' => [ 80 | 'edit_something_for_system' => 'aauth/system.edit_something_for_system', 81 | 'create_something_for_system' => 'aauth/system.create_something_for_system', 82 | ], 83 | 'organization' => [ 84 | 'edit_something_for_organization' => 'aauth/organization.edit_something_for_organization', 85 | 'create_something_for_organization' => 'aauth/organization.create_something_for_organization', 86 | ], 87 | ], 88 | ]; 89 | ``` 90 | 91 | # Main Philosophy of AAuth OrBAC 92 | 93 | In computer system security, there are several approaches to restrict system access to authorized users. 94 | 95 | Most used and known *access control method* is Rol Based Access Control (RoBAC). 96 | 97 | In most circumstances, it's sufficient for software projects. 98 | Basically; Roles and Permissions are assigned to the Users, The data can be accessed horizontally as single level 99 | 100 | What if your data access needs are further more than one level? 101 | and what if you need to restrict and filter the data in organizational and hierarchical manner? 102 | 103 | Let's assume we need to implement a multi-zone, multi-level school system and be our structure like this. 104 | 105 | - Türkiye 106 | - A High School 107 | - Class 1A 108 | - Class 2A 109 | - B High School 110 | - Class 1A 111 | - Germany 112 | - X High School 113 | - Class 1B 114 | - Class 2B 115 | 116 | How can you restrict A High School's data from X High School Principal and Teachers? 117 | 118 | How can you give permissions to a Class Teacher to see their students **only** ? 119 | 120 | What if we need another level of organization in the future like this? 121 | and want to give access to see students data under their responsibility only for Europe Zone Principal, Türkiye 122 | Principal dynamically *without writing one line of code?* 123 | 124 | - Europe 125 | - Türkiye 126 | - A High School 127 | - Class 1A 128 | - Class 2A 129 | - B High School 130 | - Class 1A 131 | - Germany 132 | - X High School 133 | - Class 1B 134 | - Class 2B 135 | - America 136 | - USA 137 | - .... 138 | - .... 139 | - Canada 140 | - ..... 141 | 142 | # Main Philosophy of AAuth ABAC 143 | 144 | In the context of the AAuth package, Attribute-Based Access Control (ABAC) is an advanced system for creating dynamic, attribute-based filtering scopes for your Eloquent models. It allows you to define granular access rules that operate directly on the **attributes of the models themselves**. When a user attempts to retrieve data, these rules are automatically applied to the database queries, ensuring that only records matching the specified attribute conditions for the user's current role are returned. 145 | 146 | **Core Concepts in AAuth's ABAC:** 147 | 148 | * **Model Attributes:** These are the characteristics of your Eloquent model instances, directly corresponding to the columns in your database tables (e.g., an `Order` model might have attributes like `status`, `amount`, `created_at`, `customer_id`). AAuth's ABAC rules are built around evaluating these attributes. 149 | * **Rules/Policies:** In AAuth, ABAC rules are structured statements that define conditions based on model attributes. For example, a rule might state "only allow access to `Order` models where the `status` attribute is 'completed' AND the `amount` attribute is greater than 100." These rules are stored and associated with user roles (see "Managing ABAC Rules and Associations") but the conditions within the rules operate on the attributes of the Eloquent model being queried. The syntax for these rules is detailed in "Defining ABAC Rules". 150 | * While traditional ABAC systems might also heavily feature subject attributes (e.g., user's department, security clearance) and environment conditions (e.g., time of day) as direct inputs into the rule *structure*, AAuth's primary focus for its ABAC *rule definitions* is on the attributes of the data/model itself. External factors like user properties or current time can be incorporated by dynamically constructing the rule's *values* before they are stored or used, but the rule engine itself primarily processes conditions against model attributes. 151 | 152 | **Benefits of ABAC in AAuth:** 153 | 154 | Framed by AAuth's model-centric approach, the benefits include: 155 | 156 | * **Fine-Grained Data Filtering:** ABAC enables highly specific filtering of Eloquent model records, moving beyond broad role-based permissions to control access down to individual records based on their attributes. 157 | * **Dynamic Query Modification:** Access to data can change dynamically as the attributes of the model records change, without needing to redefine roles or permissions. For instance, if an `Invoice` model's `payment_status` attribute changes to 'paid', an ABAC rule can automatically restrict access for roles that should only see unpaid invoices. 158 | * **Reduced Complexity in Roles:** Instead of creating numerous specific roles for slightly different data access needs, ABAC allows for fewer roles, with the data visibility for those roles being dynamically shaped by attribute-based rules. 159 | * **Centralized Access Logic for Data Records:** Rules pertaining to data attributes are defined and managed in a structured way, making it easier to understand and audit who can access what data. 160 | 161 | **ABAC in AAuth: How It Works** 162 | 163 | AAuth implements its ABAC capabilities by allowing you to: 164 | 1. Define rules based on the attributes of your Eloquent models (e.g., for an `Order` model, rules might use `Order->status` or `Order->amount`). 165 | 2. Associate these rules with specific user roles. 166 | 3. Automatically apply these rules as database query conditions whenever data is fetched for an ABAC-enabled model. 167 | 168 | For example, a rule might specify that users with a "Support Tier 1" role can only access `Ticket` models where the `Ticket->priority` is 'low' and `Ticket->is_open` is true. If a `Ticket`'s `priority` changes, or it's closed, it automatically falls out of scope for that user role. This powerful filtering is applied seamlessly, as detailed in the "Automatic Query Filtering (ABAC)" section. 169 | 170 | This model-attribute-focused ABAC complements the Organization-Based Access Control (OrBAC) and Role-Based Access Control (RBAC) features of AAuth, allowing for a robust, multi-layered access control strategy. 171 | 172 | 173 | --- 174 | **AAuth may be your first class assistant package.** 175 | 176 | --- 177 | > If you don't need organizational roles, **AAuth** may not be suitable for your work. 178 | --- 179 | 180 | # AAuth Terminology 181 | 182 | Before using AAuth its worth to understand the main terminology of AAuth. 183 | AAuth differs from other Auth Packages due to its organizational structure. 184 | 185 | ## What is Organization? 186 | 187 | Organization is a kind of term 188 | which refers to hierarchical arrangement of eloquent models in sequential tree. 189 | 190 | It consists of a central root organization node, and sub organization nodes, 191 | which are connected via edges. 192 | We can also say that organization tree has one root node, many sub organization nodes polymorphic-connected with one 193 | eloquent model. 194 | 195 | ## Organization Scope 196 | 197 | In Organization Tree, each node has an organization scope. 198 | Organization scope has a level property to determine the level of the organization node in the tree. 199 | 200 | ## Organization Node 201 | 202 | Each node in the organization tree means organization node. 203 | Each Organization Node is an Eloquent Model. 204 | Organization Node can be polymorphic-related with an Eloquent Model. 205 | 206 | ## Permission 207 | 208 | In This Package there are 2 types of Permissions. 209 | 210 | 1. System Permissions 211 | 2. Organization Permissions 212 | 213 | **System Permission** is plain permission non-related to the organization which is useful for system related access 214 | controls like backup_db, edit_website_logo, edit_contact_info etc.. 215 | A System permission can only be assigned to a System Role. System Permissions should be added inside `aauth.php` config 216 | file's permission['system'] array. 217 | 218 | **Organization Permission** is hierarchical controllable permission. An Organization permission can only be assigned to 219 | an Organization Role. 220 | Organization Permissions should be added inside `aauth.php` config file's permission['organization'] array. 221 | 222 | ## ABAC 223 | Attribute-Based Access Control. AAuth provides comprehensive ABAC features, allowing for fine-grained access control based on model attributes. For a detailed explanation, see the sections: 224 | - "Main Philosophy of AAuth ABAC" 225 | - "Using ABAC Interface and Trait with Eloquent Models" 226 | - "Defining ABAC Rules" 227 | - "Managing ABAC Rules and Associations" 228 | - "Automatic Query Filtering (ABAC)" 229 | 230 | ## Role 231 | 232 | Roles are assigned to users. Each User can have multiple roles. 233 | 234 | In This Package there are 2 types of Roles. 235 | 236 | 1. System Roles 237 | 2. Organization Roles 238 | 239 | **System Role** is plain role for non-related to the organization which is useful for system related users like system 240 | admin, super admin etc.. 241 | 242 | **Organization Role** is hierarchical position of a User in Organization Tree. 243 | An Organization Role can be assigned to a user with 3 parameters. 244 | 245 | - user_id (related user's id) 246 | - role_id 247 | - organization_node_id (id of the organization node which defines the position of the user's role on the organization 248 | Tree) 249 | 250 | > ! it can be a little overwhelming at the first, but it is not complex lol. :) 251 | 252 | ## User 253 | 254 | Just a usual Laravel User. 255 | AAuthUser trait must be added to Default User Model. 256 | 257 | ## Permission Config File 258 | 259 | Permissions are stored inside `config/aauth.php` which is published after installing. 260 | 261 | ### Model - Organization Node Relations 262 | 263 | Each Organization Node can have a polymorphic relationship with an Eloquent Model. By doing this, an Eloquent Model can 264 | be an organization node and can be access controllable. 265 | 266 | It means that; Only Authorized User Role can be access the relating model, or in other words, Each role only can access 267 | the models which is on Authenticated Sub-Organization Tree of User's Role. 268 | 269 | ### Defining ABAC Rules 270 | 271 | Attribute-Based Access Control (ABAC) rules in AAuth determine whether a user has access to a specific Eloquent model instance based on its attributes. These rules are defined as a PHP array, which can also be represented as a JSON string that decodes into the equivalent array structure. This allows for dynamic rule creation and storage. 272 | 273 | The rules are typically defined within your ABAC-enabled Eloquent model by implementing the `getABACRules(): array` method from the `AAuthABACModelInterface`. These rules are then automatically applied by the `AAuthABACModelScope` when querying the model. 274 | 275 | **Overall Structure:** 276 | 277 | An ABAC rule set is fundamentally a nested array structure that always starts with a top-level logical operator, either `&&` (AND) or `||` (OR). This operator dictates how the conditions or condition groups directly under it are evaluated. 278 | 279 | * If the top-level operator is `&&`, all direct child conditions/groups must evaluate to true. 280 | * If the top-level operator is `||`, at least one direct child condition/group must evaluate to true. 281 | 282 | Each element within the top-level operator's array is either a single condition or another nested group of conditions (which itself starts with a logical operator). 283 | 284 | **Example of basic structure:** 285 | 286 | ```php 287 | [ 288 | // Top-level logical operator 289 | '&&' => [ 290 | // Condition 1 291 | ['=' => ['attribute' => 'status', 'value' => 'active']], 292 | // Condition 2 293 | ['>' => ['attribute' => 'amount', 'value' => 100]], 294 | // Nested group of conditions 295 | ['||' => [ 296 | ['=' => ['attribute' => 'category', 'value' => 'electronics']], 297 | ['=' => ['attribute' => 'category', 'value' => 'books']] 298 | ]] 299 | ] 300 | ] 301 | ``` 302 | 303 | In this example: 304 | Access is granted if (`status` is 'active' **AND** `amount` is greater than 100) **AND** (`category` is 'electronics' **OR** `category` is 'books'). 305 | 306 | **Logical Operators:** 307 | 308 | Logical operators define how multiple conditions are combined. 309 | 310 | * **`&&` (AND):** 311 | All conditions or nested groups within this block must be true for this part of the rule to be satisfied. 312 | ```php 313 | [ 314 | '&&' => [ 315 | // Condition A 316 | // Condition B 317 | ] 318 | ] 319 | // Both Condition A AND Condition B must be true. 320 | ``` 321 | 322 | * **`||` (OR):** 323 | At least one of the conditions or nested groups within this block must be true for this part of the rule to be satisfied. 324 | ```php 325 | [ 326 | '||' => [ 327 | // Condition X 328 | // Condition Y 329 | ] 330 | ] 331 | // Either Condition X OR Condition Y (or both) must be true. 332 | ``` 333 | 334 | **Nesting Logical Operators:** 335 | You can nest these operators to create complex logic: 336 | 337 | ```php 338 | [ 339 | '&&' => [ // Outer AND 340 | ['=' => ['attribute' => 'is_published', 'value' => true]], // Condition 1 341 | ['||' => [ // Inner OR 342 | ['=' => ['attribute' => 'visibility', 'value' => 'public']], // Condition 2a 343 | ['=' => ['attribute' => 'owner_id', 'value' => '$USER_ID']] // Condition 2b (assuming $USER_ID is a placeholder you'd replace) 344 | ]] 345 | ] 346 | ] 347 | // Access if: is_published is true AND (visibility is 'public' OR owner_id matches the user's ID) 348 | ``` 349 | 350 | **Conditional Operators:** 351 | 352 | Conditional operators are used to compare a model's attribute with a specific value. The `AAuthABACModelScope` leverages Laravel's underlying database query builder, so standard SQL comparison operators are generally available. Common ones include: 353 | 354 | * `=` : Equal to. 355 | * `!=` or `<>` : Not equal to. 356 | * `>` : Greater than. 357 | * `<` : Less than. 358 | * `>=` : Greater than or equal to. 359 | * `<=` : Less than or equal to. 360 | * `LIKE` : Simple string matching (e.g., `value` can be `'%' . $searchTerm . '%'`). 361 | * `NOT LIKE` : Negated string matching. 362 | * `IN` : Value is within a given array. The `value` for an `IN` condition should be an array. 363 | * `NOT IN` : Value is not within a given array. The `value` for a `NOT IN` condition should be an array. 364 | 365 | **Condition Structure:** 366 | 367 | Each individual condition is an array where the key is the conditional operator, and the value is another array containing `attribute` and `value` keys. 368 | 369 | ```php 370 | [ 371 | // Conditional Operator (e.g., '=') 372 | '=' => [ 373 | 'attribute' => 'model_column_name', // The column name on your Eloquent model 374 | 'value' => 'the_value_to_compare_against' 375 | ] 376 | ] 377 | ``` 378 | 379 | * `attribute`: A string representing the name of the attribute (database column) on the Eloquent model being queried. 380 | * `value`: The value to compare the attribute against. This can be a string, number, boolean, or an array (especially for `IN` and `NOT IN` operators). 381 | 382 | **Practical Examples:** 383 | 384 | 1. **Allow if `status` is 'active':** 385 | ```php 386 | [ 387 | '&&' => [ 388 | ['=' => ['attribute' => 'status', 'value' => 'active']] 389 | ] 390 | ] 391 | ``` 392 | *(Note: Even for a single condition, it must be wrapped in a top-level logical operator.)* 393 | 394 | 2. **Allow if `order_value` is greater than or equal to 1000:** 395 | ```php 396 | [ 397 | '&&' => [ 398 | ['>=' => ['attribute' => 'order_value', 'value' => 1000]] 399 | ] 400 | ] 401 | ``` 402 | 403 | 3. **Allow if `is_urgent` is true AND `priority` is greater than 5:** 404 | ```php 405 | [ 406 | '&&' => [ 407 | ['=' => ['attribute' => 'is_urgent', 'value' => true]], 408 | ['>' => ['attribute' => 'priority', 'value' => 5]] 409 | ] 410 | ] 411 | ``` 412 | 413 | 4. **Allow if `department` is 'sales' OR `department` is 'support':** 414 | This can be written in two ways: 415 | Using `||`: 416 | ```php 417 | [ 418 | '||' => [ 419 | ['=' => ['attribute' => 'department', 'value' => 'sales']], 420 | ['=' => ['attribute' => 'department', 'value' => 'support']] 421 | ] 422 | ] 423 | ``` 424 | Using `IN`: 425 | ```php 426 | [ 427 | '&&' => [ // Top level can be && if this is the only group 428 | ['IN' => ['attribute' => 'department', 'value' => ['sales', 'support']]] 429 | ] 430 | ] 431 | ``` 432 | 433 | 5. **Complex: Allow if (`type` is 'document' AND `file_format` is 'pdf') OR (`type` is 'image' AND `resolution` is 'high'):** 434 | ```php 435 | [ 436 | '||' => [ // Top-level OR 437 | [ // First group: document conditions 438 | '&&' => [ 439 | ['=' => ['attribute' => 'type', 'value' => 'document']], 440 | ['=' => ['attribute' => 'file_format', 'value' => 'pdf']] 441 | ] 442 | ], 443 | [ // Second group: image conditions 444 | '&&' => [ 445 | ['=' => ['attribute' => 'type', 'value' => 'image']], 446 | ['=' => ['attribute' => 'resolution', 'value' => 'high']] 447 | ] 448 | ] 449 | ] 450 | ] 451 | ``` 452 | 453 | 6. **Example adapted from `README-abac.md` (made concrete):** 454 | Original abstract idea: 455 | ```json 456 | // { 457 | // "&&": [ 458 | // { "==": [ "$attribute", "asd" ] }, 459 | // { "==": [ "$attribute", "asd" ] }, 460 | // { "||": [ 461 | // { "==": [ "$attribute", "asd" ] }, 462 | // { "==": [ "$attribute", "asd" ] } 463 | // ] 464 | // } 465 | // ] 466 | // } 467 | ``` 468 | Concrete AAuth PHP array: 469 | ```php 470 | [ 471 | '&&' => [ 472 | ['=' => ['attribute' => 'product_category', 'value' => 'electronics']], 473 | ['=' => ['attribute' => 'brand', 'value' => 'AwesomeBrand']], 474 | [ 475 | '||' => [ 476 | ['=' => ['attribute' => 'region', 'value' => 'EU']], 477 | ['=' => ['attribute' => 'region', 'value' => 'US']] 478 | ] 479 | ] 480 | ] 481 | ] 482 | // Access if: category is 'electronics' AND brand is 'AwesomeBrand' AND (region is 'EU' OR region is 'US') 483 | ``` 484 | 485 | By defining these rules in the `getABACRules()` method of your ABAC-enabled models, AAuth will automatically filter database queries to ensure users can only access records that meet the specified attribute conditions for their role. 486 | 487 | 488 | # Usage 489 | 490 | Before using this, please make sure that you published the config files. 491 | 492 | ## AAuth Services, Service Provider and `roleId` Session and Facade 493 | 494 | AAuth Services are initialized inside AAuthService Provider. 495 | 496 | roleId session must be set before initializing **AAuth** Service. 497 | `AAuthServiceProvider.php` 498 | 499 | ```php 500 | $this->app->singleton('aauth', function ($app) { 501 | return new AAuth( 502 | Auth::user(), 503 | Session::get('roleId') 504 | ); 505 | }); 506 | ``` 507 | 508 | there is also a AAuth Facade to access AAuth Service class statically. 509 | Example; 510 | 511 | ```php 512 | AAuth::can(); 513 | ``` 514 | 515 | ## OrganizationService 516 | 517 | Organization Service is used for organization related jobs. 518 | The service can be initialized as 519 | 520 | ```php 521 | $organizationService = new OrganizationService() 522 | ``` 523 | 524 | or via dependency injecting 525 | 526 | ```php 527 | public function index(OrganizationService $organizationService) 528 | { 529 | ..... 530 | } 531 | ``` 532 | 533 | ### Creating an Organization Scope 534 | ```php 535 | $data = [ 536 | 'name' => 'Org Scope1', 537 | 'level' => 5, 538 | 'status' => 'active', 539 | ]; 540 | 541 | $organizationService->createOrganizationScope($data); 542 | ``` 543 | 544 | ### Updating an Organization Scope 545 | // todo help wanted 546 | 547 | ### Deleting an Organization Scope 548 | // todo help wanted 549 | 550 | 551 | ### Creating an Organization Node without Model Relationship 552 | ```php 553 | 554 | $orgScope = OrganizationScope::first(); 555 | 556 | $data = [ 557 | 'name' => 'Created Org Node 1', 558 | 'organization_scope_id' => $orgScope->id, 559 | 'parent_id' => 1, 560 | ]; 561 | 562 | $organizationService->createOrganizationNode($data); 563 | ``` 564 | 565 | ### Updating an Organization Node 566 | // todo help wanted 567 | 568 | ### Deleting an Organization Node 569 | // todo help wanted 570 | 571 | ## Role Permission Service 572 | 573 | This Service is used for role related jobs. 574 | The service can be initialized as 575 | 576 | ```php 577 | $rolePermissionService = new RolePermissionService() 578 | ``` 579 | 580 | or via dependency injecting 581 | 582 | ```php 583 | public function index(RolePermissionService $rolePermissionService) 584 | { 585 | ..... 586 | } 587 | ``` 588 | ### Creating a Role 589 | ```php 590 | $organizationScope = OrganizationScope::whereName('Root Scope')->first(); 591 | 592 | $data = [ 593 | 'organization_scope_id' => $organizationScope->id, 594 | 'type' => 'system', 595 | 'name' => 'Created System Role 1', 596 | 'status' => 'active', 597 | ]; 598 | 599 | $createdRole = $rolePermissionService->createRole($data); 600 | ``` 601 | 602 | ### Updating a Role 603 | // todo help wanted 604 | 605 | ### Deleting a Role 606 | // todo help wanted 607 | 608 | ### Attaching a Role to a User 609 | ```php 610 | $role = Role::whereName('System Role 1')->first(); 611 | $permissionName = 'test_permission1'; 612 | 613 | $rolePermissionService->attachPermissionToRole($permissionName, $role->id); 614 | ``` 615 | 616 | ### Syncing All Permissions for a Role 617 | ```php 618 | $role = Role::whereName('System Role 1')->first(); 619 | $permissionName1 = 'test_permission1'; 620 | $permissionName2 = 'test_permission2'; 621 | $permissionName3 = 'test_permission3'; 622 | 623 | $rolePermissionService->syncPermissionsOfRole( 624 | compact('permissionName1', 'permissionName2', 'permissionName3'), 625 | $role->id 626 | ); 627 | ``` 628 | 629 | ### Detaching Permission from a Role 630 | ```php 631 | $rolePermissionService->detachSystemRoleFromUser($role->id, $user->id); 632 | ``` 633 | 634 | ### Creating an Organization Role and Attaching to a User 635 | ```php 636 | $organizationScope = OrganizationScope::whereName('Root Scope')->first(); 637 | $organizationNode = OrganizationNode::whereName('Root Node')->first(); 638 | 639 | $data = [ 640 | 'organization_scope_id' => $organizationScope->id, 641 | 'type' => 'organization', 642 | 'name' => 'Created Organization Role 1 for Attaching', 643 | 'status' => 'active', 644 | ]; 645 | 646 | $createdRole = $rolePermissionService->createRole($data); 647 | $rolePermissionService->attachOrganizationRoleToUser($organizationNode->id, $createdRole->id, $user->id); 648 | ``` 649 | 650 | ### Creating a System Role and Attaching to a User 651 | // todo help wanted 652 | 653 | 654 | ## Using AAuth Interface and Trait with Eloquent Models 655 | To turn an Eloquent Model into an AAuth Organization Node; Model must implement `AAuthOrganizationNodeInterface` and use `AAuthOrganizationNode` Trait. 656 | After adding `AAuthOrganizationNode` trait, you will be able to use AAuth methods within the model 657 | 658 | ```php 659 | namespace App\Models\ExampleModel; 660 | 661 | use AuroraWebSoftware\AAuth\Interfaces\AAuthOrganizationNodeInterface; 662 | use AuroraWebSoftware\AAuth\Traits\AAuthOrganizationNode; 663 | use Illuminate\Database\Eloquent\Model; 664 | 665 | class ExampleModel extends Model implements AAuthOrganizationNodeInterface 666 | { 667 | use AAuthOrganizationNode; 668 | 669 | // implementation 670 | } 671 | ``` 672 | 673 | ## Using ABAC Interface and Trait with Eloquent Models 674 | 675 | To make your Eloquent models controllable via Attribute-Based Access Control (ABAC) with AAuth, you need to implement an interface and use a specific trait. This allows AAuth to understand how to apply attribute-based rules to your models. 676 | 677 | **Requirements:** 678 | 679 | 1. Your Eloquent model **must** implement the `AuroraWebSoftware\AAuth\Contracts\AAuthABACModelInterface`. 680 | 2. Your Eloquent model **must** use the `AuroraWebSoftware\AAuth\Traits\AAuthABACModel` trait. 681 | 682 | **Implementation Example:** 683 | 684 | Here's an example of how to set up an Eloquent model (e.g., `Order`) for ABAC: 685 | 686 | ```php 687 | namespace App\Models; 688 | 689 | use Illuminate\Database\Eloquent\Model; 690 | use AuroraWebSoftware\AAuth\Contracts\AAuthABACModelInterface; 691 | use AuroraWebSoftware\AAuth\Traits\AAuthABACModel; 692 | 693 | class Order extends Model implements AAuthABACModelInterface 694 | { 695 | use AAuthABACModel; 696 | 697 | // Your model's other properties and methods... 698 | 699 | /** 700 | * Get the type of the model for ABAC. 701 | * This is typically a string that identifies your model. 702 | * 703 | * @return string 704 | */ 705 | public static function getModelType(): string 706 | { 707 | return 'order'; // Or any unique string identifier for this model type 708 | } 709 | 710 | /** 711 | * Define the ABAC rules for this model. 712 | * These rules determine how access is granted based on attributes. 713 | * The detailed structure and syntax for these rules are covered in the "Defining ABAC Rules" section. 714 | * This method can serve as a fallback or default if no specific rule is found for the current user's role 715 | * via the `RoleModelAbacRule` model, or it can be the primary source if role-specific ABAC rules are not used. 716 | * 717 | * @return array 718 | */ 719 | public static function getABACRules(): array 720 | { 721 | // Example: Return an empty array as a placeholder, or define default/fallback rules here. 722 | // For instance, to allow access if 'status' is 'active' by default: 723 | // return [ 724 | // '&&' => [ 725 | // ['=' => ['attribute' => 'status', 'value' => 'active']] 726 | // ] 727 | // ]; 728 | // If no role-specific rule is found via RoleModelAbacRule, these rules (if any) might be applied. 729 | // If you exclusively use RoleModelAbacRule for all ABAC logic, this method can safely return an empty array. 730 | return []; 731 | } 732 | } 733 | ``` 734 | 735 | This setup prepares your model to have ABAC rules applied to it. The `getModelType()` method provides a string identifier for your model type, which can be used in rule definitions. The `getABACRules()` method is where you can define default or fallback attribute conditions for accessing instances of this model. While the primary mechanism for applying role-specific rules is via the `RoleModelAbacRule` model (see "Managing ABAC Rules and Associations"), these model-defined rules can act as a base or default. The detailed format for the rule syntax is covered in the "Defining ABAC Rules" section. 736 | 737 | ## AAuth Service and Facade Methods 738 | // todo 739 | 740 | ### Current Roles All Permissions 741 | current user's selected roles permissions with **AAuth Facade** 742 | ```php 743 | $permissions = AAuth::permissions(); 744 | ``` 745 | 746 | ### Check allowed permission with can() method 747 | ```php 748 | AAuth::can('create_something_for_organization'); 749 | ``` 750 | ```php 751 | if (AAuth::can('create_something_for_organization')) { 752 | // codes here 753 | } 754 | ``` 755 | 756 | ### Check permission and abort if not user and current allowed 757 | ```php 758 | AAuth::passOrAbort('create_something_for_organization'); 759 | ``` 760 | 761 | ### Get all permitted organization nodes 762 | it will return OrganizationNode collection. 763 | 764 | organizationNodes(bool $includeRootNode = false, ?string $modelType = null): \Illuminate\Support\Collection 765 | 766 | ```php 767 | $organizationNodes = AAuth::organizationNodes(); 768 | ``` 769 | 770 | ### Get one specified organization node 771 | // todo help wanted 772 | 773 | ### Descendant nodes can be checked 774 | with this method you can check is a organization node is descendant of another organization node. 775 | in other words, checks if node is sub-node of specified node. 776 | 777 | ```php 778 | $isDescendant = AAuth::descendant(1, 3); 779 | ``` 780 | 781 | ### Creating an Organization Node-able Model and Related Org. Node 782 | with this method, you can create a model and organization node with relationship together. 783 | ```php 784 | $data = ['name' => 'Test Organization Node-able Example']; 785 | 786 | $createdModel = ExampleModel::createWithAAuthOrganizationNode($data, 1, 2); 787 | ``` 788 | 789 | ### Getting Related Organization Node of Model 790 | ```php 791 | $exampleModel = ExampleModel::find(1); 792 | $relatedOrganizationModel = $exampleModel->relatedAAuthOrganizationNode() 793 | ``` 794 | 795 | ## Getting authorized Models only. (OrBAC) 796 | 797 | after adding `AAuthOrganizationNode` trait to your model, you are adding a global scope which filters the permitted data. 798 | 799 | Thus, you can simply use any eloquent model method without adding anything 800 | 801 | ```php 802 | ExampleModel::all(); 803 | ``` 804 | 805 | ## Managing ABAC Rules and Associations 806 | 807 | While the "Defining ABAC Rules" section explains the *structure* of ABAC rules, this section details how those rules are associated with specific roles and managed within the AAuth system. AAuth uses a dedicated Eloquent model to store these associations, allowing for dynamic and granular control over which rules apply to which roles for different types of models. 808 | 809 | **1. Association with Roles: The `RoleModelAbacRule` Model** 810 | 811 | ABAC rules are not globally applied or solely defined in the model's `getABACRules()` method (though that method can serve as a default or fallback). Instead, for role-specific ABAC, rules are linked to roles via the `AuroraWebSoftware\AAuth\Models\RoleModelAbacRule` Eloquent model. 812 | 813 | This model has the following key fields: 814 | 815 | * `role_id`: The ID of the `Role` to which this specific ABAC rule applies. 816 | * `model_type`: A string that identifies the ABAC-enabled Eloquent model. This string **must** match the value returned by the static `getModelType()` method on your ABAC-enabled model (e.g., `'order'`, `'post'`, `'product'`). This ensures the rules are applied to the correct model. 817 | * `rules_json`: A JSON field where the actual ABAC rule array (as documented in "Defining ABAC Rules") is stored. When creating or updating, you can typically provide a PHP array, and Eloquent will handle the JSON conversion if the model's casts are set up appropriately (which is common for JSON fields). 818 | 819 | **2. Creating and Assigning Rules** 820 | 821 | To apply a specific set of ABAC rules to a role for a particular model, you create an instance of `RoleModelAbacRule`. 822 | 823 | **Example:** 824 | 825 | Let's say you have an `Order` model that is ABAC-enabled (implements `AAuthABACModelInterface` and uses `AAuthABACModel` trait, with `Order::getModelType()` returning `'order'`). You want to create a rule for a specific role (e.g., "Regional Manager") that only allows them to see 'approved' orders with an amount greater than or equal to 100. 826 | 827 | ```php 828 | use AuroraWebSoftware\AAuth\Models\Role; 829 | use AuroraWebSoftware\AAuth\Models\RoleModelAbacRule; 830 | use App\Models\Order; // Your ABAC-enabled Order model 831 | 832 | // Assume $regionalManagerRole is an existing Role instance 833 | $regionalManagerRole = Role::where('name', 'Regional Manager')->first(); 834 | 835 | if ($regionalManagerRole) { 836 | // Define the ABAC rules for the 'Order' model specifically for this role 837 | $orderRulesForRole = [ 838 | '&&' => [ 839 | ['=' => ['attribute' => 'status', 'value' => 'approved']], 840 | ['>=' => ['attribute' => 'amount', 'value' => 100]] 841 | ] 842 | ]; 843 | 844 | // Create or update the rule for this role and model type 845 | RoleModelAbacRule::updateOrCreate( 846 | [ 847 | 'role_id' => $regionalManagerRole->id, 848 | 'model_type' => Order::getModelType(), // This will return 'order' 849 | ], 850 | [ 851 | 'rules_json' => $orderRulesForRole // Provide the array directly 852 | ] 853 | ); 854 | 855 | echo "ABAC rules for Orders assigned to Regional Manager role.\n"; 856 | } 857 | ``` 858 | In this example, `updateOrCreate` is used to either create a new rule association or update an existing one if a rule for that specific `role_id` and `model_type` already exists. 859 | 860 | **3. Viewing, Modifying, and Deleting Rules** 861 | 862 | Since `RoleModelAbacRule` is a standard Eloquent model, you can manage these rule associations using familiar Eloquent methods: 863 | 864 | * **Viewing:** 865 | ```php 866 | // Get all rules for a specific role 867 | $rulesForRole = RoleModelAbacRule::where('role_id', $role->id)->get(); 868 | 869 | // Get the rule for a specific role and model 870 | $specificRule = RoleModelAbacRule::where('role_id', $role->id) 871 | ->where('model_type', Order::getModelType()) 872 | ->first(); 873 | if ($specificRule) { 874 | $rulesArray = $specificRule->rules_json; // Accesses the casted array 875 | } 876 | ``` 877 | 878 | * **Modifying:** 879 | ```php 880 | $ruleToUpdate = RoleModelAbacRule::find(1); // Or fetch by role_id and model_type 881 | if ($ruleToUpdate) { 882 | $newRules = [ /* ... new rule definition ... */ ]; 883 | $ruleToUpdate->update(['rules_json' => $newRules]); 884 | } 885 | ``` 886 | 887 | * **Deleting:** 888 | ```php 889 | $ruleToDelete = RoleModelAbacRule::find(1); 890 | if ($ruleToDelete) { 891 | $ruleToDelete->delete(); 892 | } 893 | 894 | // Or delete by role and model type 895 | RoleModelAbacRule::where('role_id', $role->id) 896 | ->where('model_type', Order::getModelType()) 897 | ->delete(); 898 | ``` 899 | 900 | **4. Facade Method for Rule Retrieval: `AAuth::ABACRules()`** 901 | 902 | AAuth provides a facade method to retrieve the applicable ABAC rules for the currently authenticated user's active role and a specific model type: 903 | 904 | `AAuth::ABACRules(string $modelType): ?array` 905 | 906 | * `$modelType`: The string identifier for the model (e.g., `'order'`, as returned by `YourModel::getModelType()`). 907 | 908 | This method is primarily used internally by the `AAuthABACModelScope`. When you query an ABAC-enabled model (e.g., `Order::all()`), the scope automatically calls `AAuth::ABACRules(Order::getModelType())`. 909 | 910 | Here's how it works: 911 | 1. AAuth identifies the current user (typically via `Auth::user()`). 912 | 2. It determines the user's currently active role. This is usually set via `Session::get('roleId')` when the AAuth service is initialized (see "AAuth Services, Service Provider and `roleId` Session and Facade"). 913 | 3. It then queries the `role_model_abac_rules` table for an entry matching the active `role_id` and the provided `$modelType`. 914 | 4. If a matching rule is found, it returns the `rules_json` content as a PHP array. 915 | 5. If no specific rule is found for that role and model type, it may return `null` (or potentially fall back to default rules defined in the model's `getABACRules()` method, depending on the full implementation logic of `AAuthABACModelScope` and `AAuth::ABACRules()`). 916 | 917 | This mechanism ensures that the ABAC rules applied are specific to the user's current operational role, providing a powerful and flexible way to manage data access. 918 | 919 | ## Automatic Query Filtering (ABAC) 920 | 921 | A key strength of AAuth's Attribute-Based Access Control (ABAC) implementation is its ability to automatically filter Eloquent queries. This ensures that users only retrieve model records that they are authorized to access based on the ABAC rules defined for their current role, without needing to manually add conditions to every query. 922 | 923 | **1. Introduction to Global Scope: `AAuthABACModelScope`** 924 | 925 | When you use the `AuroraWebSoftware\AAuth\Traits\AAuthABACModel` trait in your Eloquent model, AAuth automatically registers a global Eloquent scope called `AuroraWebSoftware\AAuth\Scopes\AAuthABACModelScope`. This scope is responsible for intercepting database queries for that model and applying the necessary ABAC rule conditions. 926 | 927 | **2. Automatic Filtering in Action** 928 | 929 | Once your model is correctly set up with the `AAuthABACModel` trait, and you have defined ABAC rules and associated them with a user's current role via the `RoleModelAbacRule` model (as described in "Managing ABAC Rules and Associations"), the filtering is seamless: 930 | 931 | * Any standard Eloquent query you execute, such as `YourModel::all()`, `YourModel::where('some_column', 'some_value')->get()`, `YourModel::find($id)`, or even queries through relationships, will automatically have the ABAC rules applied. 932 | * The `AAuthABACModelScope` fetches the relevant ABAC rules for the active user's role and the model being queried (using `AAuth::ABACRules(YourModel::getModelType())`). 933 | * These rules are then translated into `WHERE` clauses that are added to your database query. 934 | * Consequently, the query results will only include records that satisfy the conditions defined in the applicable ABAC rules. If no rules are defined for the role, or if the rules permit all access, then all records will be returned (subject to other query conditions). 935 | 936 | **3. Example Scenario** 937 | 938 | Let's illustrate with an `Order` model that is ABAC-enabled: 939 | 940 | * The `App\Models\Order` model uses the `AAuthABACModel` trait. 941 | * The user's currently active role has an ABAC rule associated with the `'order'` model type (Order::getModelType() returns 'order'). This rule is: 942 | ```php 943 | // Rule stored in RoleModelAbacRule for the user's role and 'order' model_type: 944 | // [ 945 | // '&&' => [ 946 | // ['=' => ['attribute' => 'status', 'value' => 'completed']] 947 | // ] 948 | // ] 949 | ``` 950 | * The `orders` table in your database contains orders with various statuses: 'pending', 'processing', 'completed', 'cancelled'. 951 | 952 | Now, when the user performs queries: 953 | 954 | ```php 955 | use App\Models\Order; 956 | 957 | // Fetch all orders 958 | // Despite no explicit where('status', 'completed') here, 959 | // AAuth will automatically add this condition based on the user's role's ABAC rules. 960 | // $completedOrders will only contain orders where status is 'completed'. 961 | $completedOrders = Order::all(); 962 | 963 | foreach ($completedOrders as $order) { 964 | // echo $order->status; // Will always output 'completed' 965 | } 966 | 967 | // Fetch a specific order by ID 968 | $order1 = Order::find(1); // If Order ID 1 has status 'pending', $order1 will be null. 969 | $order2 = Order::find(2); // If Order ID 2 has status 'completed', $order2 will be the Order model. 970 | 971 | // Even more complex queries are filtered: 972 | $highValueCompletedOrders = Order::where('amount', '>', 500)->get(); 973 | // This will return orders where amount > 500 AND status is 'completed'. 974 | ``` 975 | This automatic filtering significantly enhances security and simplifies development, as the access control logic is centralized and consistently applied without requiring developers to remember to add specific `WHERE` clauses for authorization in every query. 976 | 977 | **4. Bypassing the ABAC Scope** 978 | 979 | There might be rare situations (e.g., in administrative tools or specific internal processes) where you need to retrieve all records without ABAC filtering. You can bypass the global `AAuthABACModelScope` just like any other global Eloquent scope: 980 | 981 | ```php 982 | use AuroraWebSoftware\AAuth\Scopes\AAuthABACModelScope; 983 | use App\Models\Order; 984 | 985 | // Retrieve all orders, ignoring ABAC rules for this specific query 986 | $allOrdersIncludingNonCompleted = Order::withoutGlobalScope(AAuthABACModelScope::class)->get(); 987 | 988 | // Find a specific order by ID, ignoring ABAC rules 989 | $anyOrder = Order::withoutGlobalScope(AAuthABACModelScope::class)->find(1); 990 | ``` 991 | The `withoutGlobalScopes()->all()` method mentioned earlier in the "Getting All Model Collection without any access control" section also effectively bypasses this and all other global scopes. Use this capability judiciously, as it circumvents the defined access controls. 992 | 993 | ## Getting All Model Collection without any access control 994 | ```php 995 | ExampleModel::withoutGlobalScopes()->all() 996 | ``` 997 | 998 | that's all. 999 | 1000 | ## Changelog 1001 | 1002 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 1003 | 1004 | ## Contributing 1005 | 1006 | Please see [CONTRIBUTING](README-contr.md) for details. 1007 | 1008 | ## Security Vulnerabilities 1009 | 1010 | // todo ? 1011 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 1012 | 1013 | 1014 | ## Credits 1015 | 1016 | - [Aurora Web Software Team](https://github.com/AuroraWebSoftware) 1017 | - [All Contributors](../../contributors) 1018 | 1019 | ## License 1020 | 1021 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 1022 | 1023 | ## Development Environment using Dev Containers 1024 | 1025 | This project includes a [Dev Container](https://containers.dev/) configuration, which allows you to use a Docker container as a fully-featured development environment. 1026 | 1027 | ### Prerequisites 1028 | 1029 | - [Docker Desktop](https://www.docker.com/products/docker-desktop/) installed and running. 1030 | - [Visual Studio Code](https://code.visualstudio.com/) installed. 1031 | - [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code installed. 1032 | 1033 | ### Getting Started 1034 | 1035 | 1. Clone this repository to your local machine. 1036 | 2. Open the cloned repository in Visual Studio Code. 1037 | 3. When prompted with "Reopen in Container", click the button. (If you don't see the prompt, open the Command Palette (`Ctrl+Shift+P` or `Cmd+Shift+P`) and run "Dev Containers: Reopen in Container".) 1038 | 1039 | This will build the dev container and install all necessary dependencies. You can then develop and run the application from within this isolated environment. 1040 | 1041 | [![Open in Dev Containers](https://img.shields.io/static/v1?label=Dev%20Containers&message=Open&color=blue&logo=visualstudiocode)](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/AuroraWebSoftware/AAuth) 1042 | --------------------------------------------------------------------------------