diff --git a/.gitattributes b/.gitattributes
index 7742c9ae4..993e0d23c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,11 +2,17 @@
# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
# Ignore all test and documentation with "export-ignore".
+/.github export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.travis.yml export-ignore
/phpunit.xml.dist export-ignore
/.scrutinizer.yml export-ignore
+/art export-ignore
+/docs export-ignore
/tests export-ignore
/.editorconfig export-ignore
+/.php_cs.dist.php export-ignore
/.styleci.yml export-ignore
+/CHANGELOG.md export-ignore
+/CONTRIBUTING.md export-ignore
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 000000000..328f945dd
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,43 @@
+---
+name: Bug report
+about: Report or reproducable bug
+title: ''
+labels: ''
+assignees: ''
+
+---
+
+**Before creating a new bug report**
+Please check if there isn't a similar issue on [the issue tracker](https://github.com/spatie/laravel-permission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions).
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**Versions**
+You can use `composer show` to get the version numbers of:
+- spatie/laravel-permission package version:
+- illuminate/framework package
+
+PHP version:
+
+Database version:
+
+
+**To Reproduce**
+Steps to reproduce the behavior:
+
+Here is my example code and/or tests showing the problem in my app:
+
+**Example Application**
+Here is a link to my Github repo containing a minimal Laravel application which shows my problem:
+
+**Expected behavior**
+A clear and concise description of what you expected to happen.
+
+ **Additional context**
+Add any other context about the problem here.
+
+**Environment (please complete the following information, because it helps us investigate better):**
+ - OS: [e.g. macOS]
+ - Version [e.g. 22]
+
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..5940c1979
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: false
+contact_links:
+ - name: Ask a Question
+ url: https://github.com/spatie/laravel-permission/discussions/new?category=q-a
+ about: Ask the community for help
+ - name: Feature Request
+ url: https://github.com/spatie/laravel-permission/discussions/new?category=ideas
+ about: Share ideas for new features
diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml
index 84ab01ad2..b3c985609 100644
--- a/.github/workflows/php-cs-fixer.yml
+++ b/.github/workflows/php-cs-fixer.yml
@@ -13,7 +13,7 @@ jobs:
- name: Fix style
uses: docker://oskarstark/php-cs-fixer-ga
with:
- args: --config=.php_cs --allow-risky=yes
+ args: --config=.php_cs.dist.php --allow-risky=yes
- name: Extract branch name
shell: bash
diff --git a/.github/workflows/run-tests-L7.yml b/.github/workflows/run-tests-L7.yml
index 9e07c69e0..4f347b351 100644
--- a/.github/workflows/run-tests-L7.yml
+++ b/.github/workflows/run-tests-L7.yml
@@ -7,18 +7,14 @@ jobs:
runs-on: ubuntu-latest
strategy:
- fail-fast: true
+ fail-fast: false
matrix:
- php: [7.4, 7.3, 7.2]
- laravel: [7.*, 6.*, 5.8.*]
+ php: [8.0, 7.4, 7.3]
+ laravel: [7.*]
dependency-version: [prefer-lowest, prefer-stable]
include:
- laravel: 7.*
- testbench: 5.*
- - laravel: 6.*
- testbench: 4.*
- - laravel: 5.8.*
- testbench: 3.8.*
+ testbench: 5.20
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
@@ -26,12 +22,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ~/.composer/cache/files
- key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
@@ -41,7 +31,7 @@ jobs:
- name: Install dependencies
run: |
- composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Execute tests
diff --git a/.github/workflows/run-tests-L8.yml b/.github/workflows/run-tests-L8.yml
index d8844b560..11c4fcbae 100644
--- a/.github/workflows/run-tests-L8.yml
+++ b/.github/workflows/run-tests-L8.yml
@@ -7,14 +7,21 @@ jobs:
runs-on: ubuntu-latest
strategy:
- fail-fast: true
+ fail-fast: false
matrix:
- php: [8.0, 7.4, 7.3]
- laravel: [8.*]
+ php: [8.2, 8.1, 8.0, 7.4, 7.3]
+ laravel: [9.*, 8.*]
dependency-version: [prefer-lowest, prefer-stable]
include:
+ - laravel: 9.*
+ testbench: 7.*
- laravel: 8.*
- testbench: 6.*
+ testbench: 6.23
+ exclude:
+ - laravel: 9.*
+ php: 7.4
+ - laravel: 9.*
+ php: 7.3
name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
@@ -22,12 +29,6 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- - name: Cache dependencies
- uses: actions/cache@v2
- with:
- path: ~/.composer/cache/files
- key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
-
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
@@ -37,7 +38,7 @@ jobs:
- name: Install dependencies
run: |
- composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" --no-interaction --no-update
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" "nesbot/carbon:>=2.62.1" --no-interaction --no-update
composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- name: Execute tests
diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml
new file mode 100644
index 000000000..fa56639f2
--- /dev/null
+++ b/.github/workflows/update-changelog.yml
@@ -0,0 +1,28 @@
+name: "Update Changelog"
+
+on:
+ release:
+ types: [released]
+
+jobs:
+ update:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+ with:
+ ref: main
+
+ - name: Update Changelog
+ uses: stefanzweifel/changelog-updater-action@v1
+ with:
+ latest-version: ${{ github.event.release.name }}
+ release-notes: ${{ github.event.release.body }}
+
+ - name: Commit updated CHANGELOG
+ uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ branch: main
+ commit_message: Update CHANGELOG
+ file_pattern: CHANGELOG.md
diff --git a/.gitignore b/.gitignore
index f7129a1c7..da5ec4b46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,5 +4,5 @@ vendor
tests/temp
.idea
.phpunit.result.cache
-.php_cs.cache
-
+.php-cs-fixer.cache
+tests/CreatePermissionCustomTables.php
diff --git a/.php_cs b/.php_cs.dist.php
similarity index 71%
rename from .php_cs
rename to .php_cs.dist.php
index 1c4e7d562..e89f88628 100644
--- a/.php_cs
+++ b/.php_cs.dist.php
@@ -1,9 +1,6 @@
notPath('bootstrap/*')
- ->notPath('storage/*')
- ->notPath('resources/view/mail/*')
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
@@ -13,14 +10,14 @@
->ignoreDotFiles(true)
->ignoreVCS(true);
-return PhpCsFixer\Config::create()
+return (new PhpCsFixer\Config())
->setRules([
- '@PSR2' => true,
+ '@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
- 'ordered_imports' => ['sortAlgorithm' => 'alpha'],
+ 'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
- 'trailing_comma_in_multiline_array' => true,
+ 'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
@@ -29,9 +26,15 @@
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
+ 'class_attributes_separation' => [
+ 'elements' => [
+ 'method' => 'one',
+ ],
+ ],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
- ]
+ ],
+ 'single_trait_insert_per_statement' => true,
])
->setFinder($finder);
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd9f63c90..d5ee15a1e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,119 +2,472 @@
All notable changes to `laravel-permission` will be documented in this file
+## 5.6.0 - 2022-11-19
+
+### What's Changed
+
+- No longer throws an exception when checking `hasAllPermissions()` if the permission name does not exist by @mtawil in https://github.com/spatie/laravel-permission/pull/2248
+
+### Doc Updates
+
+- [Docs] Add syncPermissions() in role-permissions.md by @xorinzor in https://github.com/spatie/laravel-permission/pull/2235
+- [Docs] Fix broken Link that link to freek's blog post by @chengkangzai in https://github.com/spatie/laravel-permission/pull/2234
+
+### New Contributors
+
+- @xorinzor made their first contribution in https://github.com/spatie/laravel-permission/pull/2235
+- @chengkangzai made their first contribution in https://github.com/spatie/laravel-permission/pull/2234
+- @mtawil made their first contribution in https://github.com/spatie/laravel-permission/pull/2248
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.16...5.6.0
+
+## 5.5.16 - 2022-10-23
+
+### What's Changed
+
+- optimize `for` loop in WildcardPermission by @SubhanSh in https://github.com/spatie/laravel-permission/pull/2113
+
+### New Contributors
+
+- @SubhanSh made their first contribution in https://github.com/spatie/laravel-permission/pull/2113
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.15...5.5.16
+
+## 5.5.15 - 2022-10-23
+
+Autocomplete all Blade directives via Laravel Idea plugin
+
+### What's Changed
+
+- Autocomplete all Blade directives via Laravel Idea plugin by @maartenpaauw in https://github.com/spatie/laravel-permission/pull/2210
+- Add tests for display roles/permissions on UnauthorizedException by @erikn69 in https://github.com/spatie/laravel-permission/pull/2228
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.14...5.5.15
+
+## 5.5.14 - 2022-10-21
+
+FIXED BREAKING CHANGE. (Sorry about that!)
+
+### What's Changed
+
+- Revert "Avoid calling the config helper in the role/perm model constructor" by @drbyte in https://github.com/spatie/laravel-permission/pull/2225
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.13...5.5.14
+
+## 5.5.13 - 2022-10-21
+
+### What's Changed
+
+- fix UnauthorizedException: Wrong configuration was used in forRoles by @Sy-Dante in https://github.com/spatie/laravel-permission/pull/2224
+
+### New Contributors
+
+- @Sy-Dante made their first contribution in https://github.com/spatie/laravel-permission/pull/2224
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.12...5.5.13
+
+## 5.5.12 - 2022-10-19
+
+Fix regression introduced in `5.5.10`
+
+### What's Changed
+
+- Fix undefined index guard_name by @erikn69 in https://github.com/spatie/laravel-permission/pull/2219
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.11...5.5.12
+
+## 5.5.11 - 2022-10-19
+
+### What's Changed
+
+- Support static arrays on blade directives by @erikn69 in https://github.com/spatie/laravel-permission/pull/2168
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.10...5.5.11
+
+## 5.5.10 - 2022-10-19
+
+### What's Changed
+
+- Avoid calling the config helper in the role/perm model constructor by @adiafora in https://github.com/spatie/laravel-permission/pull/2098 as discussed in https://github.com/spatie/laravel-permission/issues/2097 regarding `DI`
+
+### New Contributors
+
+- @adiafora made their first contribution in https://github.com/spatie/laravel-permission/pull/2098
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.9...5.5.10
+
+## 5.5.9 - 2022-10-19
+
+Compatibility Bugfix
+
+### What's Changed
+
+- Prevent `MissingAttributeException` for `guard_name` by @ejunker in https://github.com/spatie/laravel-permission/pull/2216
+
+### New Contributors
+
+- @ejunker made their first contribution in https://github.com/spatie/laravel-permission/pull/2216
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.8...5.5.9
+
+## 5.5.8 - 2022-10-19
+
+`HasRoles` trait
+
+### What's Changed
+
+- Fix returning all roles instead of the assigned by @erikn69 in https://github.com/spatie/laravel-permission/pull/2194
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.7...5.5.8
+
+## 5.5.7 - 2022-10-19
+
+Optimize HasPermissions trait
+
+### What's Changed
+
+- Delegate permission collection filter to another method by @angeljqv in https://github.com/spatie/laravel-permission/pull/2182
+- Delegate permission filter to another method by @angeljqv in https://github.com/spatie/laravel-permission/pull/2183
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.6...5.5.7
+
+## 5.5.6 - 2022-10-19
+
+Just a maintenance release.
+
+### What's Changed
+
+- Actions: add PHP 8.2 Build by @erikn69 in https://github.com/spatie/laravel-permission/pull/2214
+- Docs: Fix small syntax error in teams-permissions.md by @miten5 in https://github.com/spatie/laravel-permission/pull/2171
+- Docs: Update documentation for multiple guards by @gms8994 in https://github.com/spatie/laravel-permission/pull/2169
+- Docs: Make Writing Policies link clickable by @maartenpaauw in https://github.com/spatie/laravel-permission/pull/2202
+- Docs: Add note about non-standard User models by @androidacy-user in https://github.com/spatie/laravel-permission/pull/2179
+- Docs: Fix explanation of results for hasAllDirectPermissions in role-permission.md by @drdan18 in https://github.com/spatie/laravel-permission/pull/2139
+- Docs: Add ULIDs reference by @erikn69 in https://github.com/spatie/laravel-permission/pull/2213
+
+### New Contributors
+
+- @miten5 made their first contribution in https://github.com/spatie/laravel-permission/pull/2171
+- @gms8994 made their first contribution in https://github.com/spatie/laravel-permission/pull/2169
+- @maartenpaauw made their first contribution in https://github.com/spatie/laravel-permission/pull/2202
+- @androidacy-user made their first contribution in https://github.com/spatie/laravel-permission/pull/2179
+- @drdan18 made their first contribution in https://github.com/spatie/laravel-permission/pull/2139
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.5...5.5.6
+
+## 5.5.5 - 2022-06-29
+
+### What's Changed
+
+- Custom primary keys tests(Only tests) by @erikn69 in https://github.com/spatie/laravel-permission/pull/2096
+- [PHP 8.2] Fix `${var}` string interpolation deprecation by @Ayesh in https://github.com/spatie/laravel-permission/pull/2117
+- Use `getKey`, `getKeyName` instead of `id` by @erikn69 in https://github.com/spatie/laravel-permission/pull/2116
+- On WildcardPermission class use static instead of self for extending by @erikn69 in https://github.com/spatie/laravel-permission/pull/2111
+- Clear roles array after hydrate from cache by @angeljqv in https://github.com/spatie/laravel-permission/pull/2099
+
+### New Contributors
+
+- @Ayesh made their first contribution in https://github.com/spatie/laravel-permission/pull/2117
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.4...5.5.5
+
+## 5.5.4 - 2022-05-16
+
+## What's Changed
+
+- Support custom primary key names on models by @erikn69 in https://github.com/spatie/laravel-permission/pull/2092
+- Fix UuidTrait on uuid doc page by @abhishekpaul in https://github.com/spatie/laravel-permission/pull/2094
+- Support custom fields on cache by @erikn69 in https://github.com/spatie/laravel-permission/pull/2091
+
+## New Contributors
+
+- @abhishekpaul made their first contribution in https://github.com/spatie/laravel-permission/pull/2094
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.3...5.5.4
+
+## 5.5.3 - 2022-05-05
+
+## What's Changed
+
+- Update .gitattributes by @angeljqv in https://github.com/spatie/laravel-permission/pull/2065
+- Remove double semicolon from add_teams_fields.php.stub by @morganarnel in https://github.com/spatie/laravel-permission/pull/2067
+- [V5] Allow revokePermissionTo to accept Permission[] by @erikn69 in https://github.com/spatie/laravel-permission/pull/2014
+- [V5] Improve typing in role's findById and findOrCreate method by @itsfaqih in https://github.com/spatie/laravel-permission/pull/2022
+- [V5] Cache loader improvements by @erikn69 in https://github.com/spatie/laravel-permission/pull/1912
+
+## New Contributors
+
+- @morganarnel made their first contribution in https://github.com/spatie/laravel-permission/pull/2067
+- @itsfaqih made their first contribution in https://github.com/spatie/laravel-permission/pull/2022
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.2...5.5.3
+
+## 5.5.2 - 2022-03-09
+
+## What's Changed
+
+- [Fixes BIG bug] register blade directives after resolving blade compiler by @tabacitu in https://github.com/spatie/laravel-permission/pull/2048
+
+## New Contributors
+
+- @tabacitu made their first contribution in https://github.com/spatie/laravel-permission/pull/2048
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.1...5.5.2
+
+## 5.5.1 - 2022-03-03
+
+## What's Changed
+
+- Spelling correction by @gergo85 in https://github.com/spatie/laravel-permission/pull/2024
+- update broken link to laravel exception by @kingzamzon in https://github.com/spatie/laravel-permission/pull/2023
+- Fix Blade Directives incompatibility with renderers by @erikn69 in https://github.com/spatie/laravel-permission/pull/2039
+
+## New Contributors
+
+- @gergo85 made their first contribution in https://github.com/spatie/laravel-permission/pull/2024
+- @kingzamzon made their first contribution in https://github.com/spatie/laravel-permission/pull/2023
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.5.0...5.5.1
+
+## 5.5.0 - 2022-01-11
+
+- add support for Laravel 9
+
+## 5.4.0 - 2021-11-17
+
+## What's Changed
+
+- Add support for PHP 8.1 by @freekmurze in https://github.com/spatie/laravel-permission/pull/1926
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.3.2...5.4.0
+
+## 5.3.2 - 2021-11-17
+
+## What's Changed
+
+- [V5] Support for custom key names on Role,Permission by @erikn69 in https://github.com/spatie/laravel-permission/pull/1913
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.3.1...5.3.2
+
+## 5.3.1 - 2021-11-04
+
+- Fix hints, support int on scopePermission (#1908)
+
+## 5.3.0 - 2021-10-29
+
+- Option for custom logic for checking permissions (#1891)
+
+## 5.2.0 - 2021-10-28
+
+- [V5] Fix detaching on all teams intstead of only current #1888 by @erikn69 in https://github.com/spatie/laravel-permission/pull/1890
+- [V5] Add uuid compatibility support on teams by @erikn69 in https://github.com/spatie/laravel-permission/pull/1857
+- Adds setRoleClass method to PermissionRegistrar by @timschwartz in https://github.com/spatie/laravel-permission/pull/1867
+- Load permissions for preventLazyLoading by @bahramsadin in https://github.com/spatie/laravel-permission/pull/1884
+- [V5] Doc for `Super Admin` on teams by @erikn69 in https://github.com/spatie/laravel-permission/pull/1845
+
+## 5.1.1 - 2021-09-01
+
+- Avoid Roles over-hydration #1834
+
+## 5.1.0 - 2021-08-31
+
+- No longer flush cache on User role/perm assignment changes #1832
+- NOTE: You should test your app to be sure that you don't accidentally have deep dependencies on cache resets happening automatically in these cases.
+- ALSO NOTE: If you have added custom code which depended on these flush operations, you may need to add your own cache-reset calls.
+
+## 5.0.0 - 2021-08-31
+
+- Change default-guard-lookup to prefer current user's guard (see BC note in #1817 )
+- Teams/Groups feature (see docs, or PR #1804)
+- Customized pivots instead of `role_id`,`permission_id` #1823
+
+## 4.4.1 - 2021-09-01
+
+- Avoid Roles over-hydration #1834
+
+## 4.4.0 - 2021-08-28
+
+- Avoid BC break (removed interface change) on cache change added in 4.3.0 #1826
+- Made cache even smaller #1826
+- Avoid re-sync on non-persisted objects when firing Eloquent::saved #1819
+
+## 4.3.0 - 2021-08-17
+
+- Speed up permissions cache lookups, and make cache smaller #1799
+
+## 4.2.0 - 2021-06-04
+
+- Add hasExactRoles method #1696
+
+## 4.1.0 - 2021-06-01
+
+- Refactor to resolve guard only once during middleware
+- Refactor service provider by extracting some methods
+
+## 4.0.1 - 2021-03-22
+
+- Added note in migration for field lengths on MySQL 8. (either shorten the columns to 125 or use InnoDB)
+
+## 4.0.0 - 2021-01-27
+
+- Drop support on Laravel 5.8 #1615
+- Fix bug when adding roles to a model that doesn't yet exist #1663
+- Enforce unique constraints on database level #1261
+- Changed PermissionRegistrar::initializeCache() public to allow reinitializing cache in custom situations. #1521
+- Use Eloquent\Collection instead of Support\Collection for consistency, collection merging, etc #1630
+
+This package now requires PHP 7.2.5 and Laravel 6.0 or higher.
+If you are on a PHP version below 7.2.5 or a Laravel version below 6.0 you can use an older version of this package.
+
+## 3.18.0 - 2020-11-27
+
+- Allow PHP 8.0
+
## 3.17.0 - 2020-09-16
+
- Optional `$guard` parameter may be passed to `RoleMiddleware`, `PermissionMiddleware`, and `RoleOrPermissionMiddleware`. See #1565
## 3.16.0 - 2020-08-18
+
- Added Laravel 8 support
## 3.15.0 - 2020-08-15
+
- Change `users` relationship type to BelongsToMany
## 3.14.0 - 2020-08-15
+
- Declare table relations earlier to improve guarded/fillable detection accuracy (relates to Aug 2020 Laravel security patch)
## 3.13.0 - 2020-05-19
+
- Provide migration error text to stop caching local config when installing packages.
## 3.12.0 - 2020-05-14
+
- Add missing config setting for `display_role_in_exception`
- Ensure artisan `permission:show` command uses configured models
## 3.11.0 - 2020-03-03
+
- Allow guardName() as a function with priority over $guard_name property #1395
## 3.10.1 - 2020-03-03
+
- Update patch to handle intermittent error in #1370
## 3.10.0 - 2020-03-02
+
- Ugly patch to handle intermittent error: `Trying to access array offset on value of type null` in #1370
## 3.9.0 - 2020-02-26
+
- Add Wildcard Permissions feature #1381 (see PR or docs for details)
## 3.8.0 - 2020-02-18
+
- Clear in-memory permissions on boot, for benefit of long running processes like Swoole. #1378
## 3.7.2 - 2020-02-17
+
- Refine test for Lumen dependency. Ref #1371, Fixes #1372.
## 3.7.1 - 2020-02-15
+
- Internal refactoring of scopes to use whereIn instead of orWhere #1334, #1335
- Internal refactoring to flatten collection on splat #1341
## 3.7.0 - 2020-02-15
+
- Added methods to check any/all when querying direct permissions #1245
- Removed older Lumen dependencies #1371
## 3.6.0 - 2020-01-17
+
- Added Laravel 7.0 support
- Allow splat operator for passing roles to `hasAnyRole()`
## 3.5.0 - 2020-01-07
+
- Added missing `guardName` to Exception `PermissionDoesNotExist` #1316
## 3.4.1 - 2019-12-28
+
- Fix 3.4.0 for Lumen
## 3.4.0 - 2019-12-27
+
- Make compatible with Swoole - ie: for long-running Laravel instances
## 3.3.1 - 2019-12-24
+
- Expose Artisan commands to app layer, not just to console
## 3.3.0 - 2019-11-22
+
- Remove duplicate and unreachable code
- Remove checks for older Laravel versions
## 3.2.0 - 2019-10-16
+
- Implementation of optional guard check for hasRoles and hasAllRoles - See #1236
## 3.1.0 - 2019-10-16
+
- Use bigIncrements/bigInteger in migration - See #1224
## 3.0.0 - 2019-09-02
+
- Update dependencies to allow for Laravel 6.0
- Drop support for Laravel 5.7 and older, and PHP 7.1 and older. (They can use v2 of this package until they upgrade.)
-To be clear: v3 requires minimum Laravel 5.8 and PHP 7.2
-
+- To be clear: v3 requires minimum Laravel 5.8 and PHP 7.2
## 2.38.0 - 2019-09-02
+
- Allow support for multiple role/permission models
- Load roles relationship only when missing
- Wrap helpers in function_exists() check
## 2.37.0 - 2019-04-09
+
- Added `permission:show` CLI command to display a table of roles/permissions
- `removeRole` now returns the model, consistent with other methods
- model `$guarded` properties updated to `protected`
- README updates
## 2.36.1 - 2019-03-05
+
- reverts the changes made in 2.36.0 due to some reported breaks.
## 2.36.0 - 2019-03-04
+
- improve performance by reducing another iteration in processing query results and returning earlier
## 2.35.0 - 2019-03-01
+
- overhaul internal caching strategy for better performance and fix cache miss when permission names contained spaces
- deprecated hasUncachedPermissionTo() (use hasPermissionTo() instead)
- added getPermissionNames() method
## 2.34.0 - 2019-02-26
+
- Add explicit pivotKeys to roles/permissions BelongsToMany relationships
## 2.33.0 - 2019-02-20
+
- Laravel 5.8 compatibility
## 2.32.0 - 2019-02-13
+
- Fix duplicate permissions being created through artisan command
## 2.31.0 - 2019-02-03
+
- Add custom guard query to role scope
- Remove use of array_wrap helper function due to future deprecation
## 2.30.0 - 2019-01-28
+
- Change cache config time to DateInterval instead of integer
This is in preparation for compatibility with Laravel 5.8's cache TTL change to seconds instead of minutes.
@@ -127,321 +480,412 @@ https://laravel-news.com/cache-ttl-change-coming-to-laravel-5-8
https://github.com/laravel/framework/commit/fd6eb89b62ec09df1ffbee164831a827e83fa61d
## 2.29.0 - 2018-12-15
+
- Fix bound `saved` event from firing on all subsequent models when calling assignRole or givePermissionTo on unsaved models. However, it is preferable to save the model first, and then add roles/permissions after saving. See #971.
## 2.28.2 - 2018-12-10
+
- Use config settings for cache reset in migration stub
## 2.28.1 - 2018-12-07
+
- Remove use of Cache facade, for Lumen compatibility
## 2.28.0 - 2018-11-30
-- Rename `getCacheKey` method in HasPermissions trait to `getPermissionCacheKey` for clearer specificity.
+
+- Rename `getCacheKey` method in HasPermissions trait to `getPermissionCacheKey` for clearer specificity.
## 2.27.0 - 2018-11-21
+
- Add ability to specify a cache driver for roles/permissions caching
## 2.26.2 - 2018-11-20
+
- Added the ability to reset the permissions cache via an Artisan command:
-`php artisan permission:cache-reset`
+- `php artisan permission:cache-reset`
## 2.26.1 - 2018-11-19
+
- minor update to de-duplicate code overhead
- numerous internal updates to cache tests infrastructure
## 2.26.0 - 2018-11-19
+
- Substantial speed increase by caching the associations between models and permissions
-### NOTES: ###
+### NOTES:
+
The following changes are not "breaking", but worth making the updates to your app for consistency.
1. Config file: The `config/permission.php` file changed to move cache-related settings into a sub-array. **You should review the changes and merge the updates into your own config file.** Specifically the `expiration_time` value has moved into a sub-array entry, and the old top-level entry is no longer used.
-See the master config file here:
-https://github.com/spatie/laravel-permission/blob/master/config/permission.php
+2. See the original config file here:
+3. https://github.com/spatie/laravel-permission/blob/main/config/permission.php
+4.
+5. Cache Resets: If your `app` or `tests` are clearing the cache by specifying the cache key, **it is better to use the built-in forgetCachedPermissions() method** so that it properly handles tagged cache entries. Here is the recommended change:
+6.
-2. Cache Resets: If your `app` or `tests` are clearing the cache by specifying the cache key, **it is better to use the built-in forgetCachedPermissions() method** so that it properly handles tagged cache entries. Here is the recommended change:
```diff
- app()['cache']->forget('spatie.permission.cache');
+ $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
-```
-3. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets.
+```
+1. Also this is a good time to point out that now with v2.25.0 and v2.26.0 most permission-cache-reset scenarios may no longer be needed in your app, so it's worth reviewing those cases, as you may gain some app speed improvement by removing unnecessary cache resets.
## 2.25.0 - 2018-11-07
-- A model's `roles` and `permissions` relations (respectively) are now automatically reloaded after an Assign/Remove role or Grant/Revoke of permissions. This means there's no longer a need to call `->fresh()` on the model if the only reason is to reload the role/permission relations. (That said, you may want to call it for other reasons.)
+
+- A model's `roles` and `permissions` relations (respectively) are now automatically reloaded after an Assign/Remove role or Grant/Revoke of permissions. This means there's no longer a need to call `->fresh()` on the model if the only reason is to reload the role/permission relations. (That said, you may want to call it for other reasons.)
- Added support for passing id to HasRole()
## 2.24.0 - 2018-11-06
+
- Fix operator used on RoleOrPermissionMiddleware, and avoid throwing PermissionDoesNotExist if invalid permission passed
- Auto-reload model role relation after using AssignRole
- Avoid empty permission creation when using the CreateRole command
## 2.23.0 - 2018-10-15
+
- Avoid unnecessary queries of user roles when fetching all permissions
## 2.22.1 - 2018-10-15
+
- Fix Lumen issue with Route helper added in 2.22.0
## 2.22.0 - 2018-10-11
+
- Added `Route::role()` and `Route::permission()` middleware helper functions
- Added new `role_or_permission` middleware to allow specifying "or" combinations
## 2.21.0 - 2018-09-29
+
- Revert changes from 2.17.1 in order to support Lumen 5.7
## 2.20.0 - 2018-09-19
-- It will sync roles/permissions to models that are not persisted, by registering a `saved` callback.
-(It would previously throw an Integrity constraint violation QueryException on the pivot table insertion.)
+
+- It will sync roles/permissions to models that are not persisted, by registering a `saved` callback.
+- (It would previously throw an Integrity constraint violation QueryException on the pivot table insertion.)
## 2.19.2 - 2018-09-19
+
- add `@elserole` directive:
- Usage:
+- Usage:
+
```php
@role('roleA')
// user hasRole 'roleA'
@elserole('roleB')
// user hasRole 'roleB' but not 'roleA'
@endrole
-```
+
+```
## 2.19.1 - 2018-09-14
+
- Spark-related fix to accommodate missing guard[providers] config
## 2.19.0 - 2018-09-10
+
- Add ability to pass in IDs or mixed values to `role` scope
- Add `@unlessrole`/`@endunlessrole` Blade directives
## 2.18.0 - 2018-09-06
+
- Expanded CLI `permission:create-role` command to create optionally create-and-link permissions in one command. Also now no longer throws an error if the role already exists.
## 2.17.1 - 2018-08-28
-- Require laravel/framework instead of illuminate/* starting from ~5.4.0
+
+- Require laravel/framework instead of illuminate/* starting from ~5.4.0
- Removed old dependency for illuminate/database@~5.3.0 (Laravel 5.3 is not supported)
## 2.17.0 - 2018-08-24
+
- Laravel 5.7 compatibility
## 2.16.0 - 2018-08-20
+
- Replace static Permission::class and Role::class with dynamic value (allows custom models more easily)
- Added type checking in hasPermissionTo and hasDirectPermission
## 2.15.0 - 2018-08-15
+
- Make assigning the same role or permission twice not throw an exception
## 2.14.0 - 2018-08-13
+
- Allow using another key name than `model_id` by defining new `columns` array with `model_morph_key` key in config file. This improves UUID compatibility as discussed in #777.
## 2.13.0 - 2018-08-02
+
- Fix issue with null values passed to syncPermissions & syncRoles
## 2.12.2 - 2018-06-13
+
- added hasAllPermissions method
## 2.12.1 - 2018-04-23
-- Reverted 2.12.0. REVERTS: "Add ability to pass guard name to gate methods like can()". Requires reworking of guard handling if we're going to add this feature.
+
+- Reverted 2.12.0. REVERTS: "Add ability to pass guard name to gate methods like can()". Requires reworking of guard handling if we're going to add this feature.
## 2.12.0 - 2018-04-22
+
- Add ability to pass guard name to gate methods like can()
## 2.11.0 - 2018-04-16
+
- Improve speed of permission lookups with findByName, findById, findOrCreate
## 2.10.0 - 2018-04-15
+
- changes the type-hinted Authenticatable to Authorizable in the PermissionRegistrar.
-(Previously it was expecting models to implement the Authenticatable contract; but really that should have been Authorizable, since that's where the Gate functionality really is.)
+- (Previously it was expecting models to implement the Authenticatable contract; but really that should have been Authorizable, since that's where the Gate functionality really is.)
## 2.9.2 - 2018-03-12
+
- Now findOrCreate() exists for both Roles and Permissions
- Internal code refactoring for future dev work
## 2.9.1 - 2018-02-23
+
- Permissions now support passing integer id for sync, find, hasPermissionTo and hasDirectPermissionTo
## 2.9.0 - 2018-02-07
+
- add compatibility with Laravel 5.6
- Allow assign/sync/remove Roles from Permission model
## 2.8.2 - 2018-02-07
+
- Allow a collection containing a model to be passed to role/permission scopes
## 2.8.1 - 2018-02-03
+
- Fix compatibility with Spark v2.0 to v5.0
## 2.8.0 - 2018-01-25
+
- Support getting guard_name from extended model when using static methods
## 2.7.9 - 2018-01-23
+
Changes related to throwing UnauthorizedException:
- - When UnauthorizedException is thrown, a property is added with the expected role/permission which triggered it
- - A configuration option may be set to include the list of required roles/permissions in the message
+
+- When UnauthorizedException is thrown, a property is added with the expected role/permission which triggered it
+- A configuration option may be set to include the list of required roles/permissions in the message
## 2.7.8 - 2018-01-02
-- REVERTED: Dynamic permission_id and role_id columns according to tables name
-NOTE: This Dynamic field naming was a breaking change, so we've removed it for now.
+
+- REVERTED: Dynamic permission_id and role_id columns according to tables name
+- NOTE: This Dynamic field naming was a breaking change, so we've removed it for now.
BEST NOT TO USE v2.7.7 if you've changed tablenames in the config file.
## 2.7.7 - 2017-12-31
+
- updated `HasPermissions::getStoredPermission` to allow a collection to be returned, and to fix query when passing multiple permissions
-- Give and revoke multiple permissions
-- Dynamic permission_id and role_id columns according to tables name
-- Add findOrCreate function to Permission model
+- Give and revoke multiple permissions
+- Dynamic permission_id and role_id columns according to tables name
+- Add findOrCreate function to Permission model
- Improved Lumen support
-- Allow guard name to be null for find role by id
+- Allow guard name to be null for find role by id
## 2.7.6 - 2017-11-27
+
- added Lumen support
- updated `HasRole::assignRole` and `HasRole::syncRoles` to accept role id's in addition to role names as arguments
## 2.7.5 - 2017-10-26
+
- fixed `Gate::before` for custom gate callbacks
## 2.7.4 - 2017-10-26
+
- added cache clearing command in `up` migration for permission tables
- use config_path helper for better Lumen support
## 2.7.3 - 2017-10-21
+
- refactor middleware to throw custom `UnauthorizedException` (which raises an HttpException with 403 response)
-The 403 response is backward compatible
+- The 403 response is backward compatible
## 2.7.2 - 2017-10-18
-- refactor `PermissionRegistrar` to use `$gate->before()`
+
+- refactor `PermissionRegistrar` to use `$gate->before()`
- removed `log_registration_exception` as it is no longer relevant
## 2.7.1 - 2017-10-12
+
- fixed a bug where `Role`s and `Permission`s got detached when soft deleting a model
## 2.7.0 - 2017-09-27
+
- add support for L5.3
## 2.6.0 - 2017-09-10
+
- add `permission` scope
## 2.5.4 - 2017-09-07
+
- register the blade directives in the register method of the service provider
## 2.5.3 - 2017-09-07
+
- register the blade directives in the boot method of the service provider
## 2.5.2 - 2017-09-05
+
- let middleware use caching
## 2.5.1 - 2017-09-02
+
- add getRoleNames() method to return a collection of assigned roles
## 2.5.0 - 2017-08-30
+
- add compatibility with Laravel 5.5
## 2.4.2 - 2017-08-11
+
- automatically detach roles and permissions when a user gets deleted
## 2.4.1 - 2017-08-05
+
- fix processing of pipe symbols in `@hasanyrole` and `@hasallroles` Blade directives
## 2.4.0 -2017-08-05
+
- add `PermissionMiddleware` and `RoleMiddleware`
## 2.3.2 - 2017-07-28
+
- allow `hasAnyPermission` to take an array of permissions
## 2.3.1 - 2017-07-27
+
- fix commands not using custom models
## 2.3.0 - 2017-07-25
+
- add `create-permission` and `create-role` commands
## 2.2.0 - 2017-07-01
+
- `hasanyrole` and `hasallrole` can accept multiple roles
## 2.1.6 - 2017-06-06
+
- fixed a bug where `hasPermissionTo` wouldn't use the right guard name
## 2.1.5 - 2017-05-17
+
- fixed a bug that didn't allow you to assign a role or permission when using multiple guards
## 2.1.4 - 2017-05-10
+
- add `model_type` to the primary key of tables that use a polymorphic relationship
## 2.1.3 - 2017-04-21
+
- fixed a bug where the role()/permission() relation to user models would be saved incorrectly
- added users() relation on Permission and Role
## 2.1.2 - 2017-04-20
+
- fix a bug where the `role()`/`permission()` relation to user models would be saved incorrectly
- add `users()` relation on `Permission` and `Role`
## 2.0.2 - 2017-04-13
+
- check for duplicates when adding new roles and permissions
## 2.0.1 - 2017-04-11
+
- fix the order of the `foreignKey` and `relatedKey` in the relations
## 2.0.0 - 2017-04-10
+
- Requires minimum Laravel 5.4
- cache expiration is now configurable and set to one day by default
- roles and permissions can now be assigned to any model through the `HasRoles` trait
- removed deprecated `hasPermission` method
- renamed config file from `laravel-permission` to `permission`.
-
## 1.17.0 - 2018-08-24
+
- added support for Laravel 5.7
## 1.16.0 - 2018-02-07
+
- added support for Laravel 5.6
## 1.15 - 2017-12-08
+
- allow `hasAnyPermission` to take an array of permissions
## 1.14.1 - 2017-10-26
+
- fixed `Gate::before` for custom gate callbacks
## 1.14.0 - 2017-10-18
-- refactor `PermissionRegistrar` to use `$gate->before()`
+
+- refactor `PermissionRegistrar` to use `$gate->before()`
- removed `log_registration_exception` as it is no longer relevant
## 1.13.0 - 2017-08-31
+
- added compatibility for Laravel 5.5
## 1.12.0
+
- made foreign key name to users table configurable
## 1.11.1
+
- `hasPermissionTo` uses the cache to avoid extra queries when it is called multiple times
## 1.11.0
+
- add `getDirectPermissions`, `getPermissionsViaRoles`, `getAllPermissions`
## 1.10.0 - 2017-02-22
+
- add `hasAnyPermission`
## 1.9.0 - 2017-02-20
+
- add `log_registration_exception` in settings file
-- fix for ambiguous column name `id` when using the role scope
+- fix for ambiguous column name `id` when using the role scope
## 1.8.0 - 2017-02-09
+
- `hasDirectPermission` method is now public
## 1.7.0 - 2016-01-23
+
- added support for Laravel 5.4
## 1.6.1 - 2016-01-19
+
- make exception logging more verbose
## 1.6.0 - 2016-12-27
+
- added `Role` scope
## 1.5.3 - 2016-12-15
+
- moved some things to `boot` method in SP to solve some compatibility problems with other packages
## 1.5.2 - 2016-08-26
+
- make compatible with L5.3
## 1.5.1 - 2016-07-23
+
- fixes `givePermissionTo` and `assignRole` in Laravel 5.1
## 1.5.0 - 2016-07-23
+
** this version does not work in Laravel 5.1, please upgrade to version 1.5.1 of this package
- allowed `givePermissonTo` to accept multiple permissions
@@ -451,10 +895,12 @@ The 403 response is backward compatible
- dropped support for PHP 5.5 and HHVM
## 1.4.0 - 2016-05-08
-- added `hasPermissionTo` function to the `Role` model
+
+- added `hasPermissionTo` function to the `Role` model
## 1.3.4 - 2016-02-27
-- `hasAnyRole` can now properly process an array
+
+- `hasAnyRole` can now properly process an array
## 1.3.3 - 2016-02-24
@@ -479,25 +925,30 @@ The 403 response is backward compatible
## 1.2.0 - 2015-10-28
###Added
+
- support for custom models
## 1.1.0 - 2015-10-12
### Added
-- Blade directives
+
+- Blade directives
- `hasAllRoles()`- and `hasAnyRole()`-functions
## 1.0.2 - 2015-10-11
### Fixed
+
- Fix for running phpunit locally
## 1.0.1 - 2015-09-30
### Fixed
+
- Fixed the inconsistent naming of the `hasPermission`-method.
## 1.0.0 - 2015-09-16
### Added
+
- Everything, initial release
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
deleted file mode 100644
index 2ceb084d4..000000000
--- a/CONTRIBUTING.md
+++ /dev/null
@@ -1,32 +0,0 @@
-# Contributing
-
-Contributions are **welcome** and will be fully **credited**.
-
-We accept contributions via Pull Requests on [Github](https://github.com/spatie/laravel-permission).
-
-
-## Pull Requests
-
-- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer).
-
-- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
-
-- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
-
-- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
-
-- **Create feature branches** - Don't ask us to pull from your master branch.
-
-- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
-
-- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
-
-
-## Running Tests
-
-``` bash
-$ phpunit
-```
-
-
-**Happy coding**!
diff --git a/README.md b/README.md
index 278e3e3a8..d090b8b24 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+
+[
](https://supportukrainenow.org)
+

# Associate users with permissions and roles
@@ -18,7 +21,7 @@
## Documentation, Installation, and Usage Instructions
-See the [DOCUMENTATION](https://docs.spatie.be/laravel-permission/v3/introduction/) for detailed installation and usage instructions.
+See the [documentation](https://spatie.be/docs/laravel-permission/) for detailed installation and usage instructions.
## What It Does
This package allows you to manage user permissions and roles in a database.
@@ -61,11 +64,11 @@ Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recen
## Contributing
-Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
+Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
### Security
-If you discover any security-related issues, please email [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker.
+If you discover any security-related issues, please email [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker.
## Postcardware
@@ -77,6 +80,7 @@ We publish all received postcards [on our company website](https://spatie.be/en/
## Credits
+- [Chris Brown](https://github.com/drbyte)
- [Freek Van der Herten](https://github.com/freekmurze)
- [All Contributors](../../contributors)
diff --git a/composer.json b/composer.json
index f4ed271e8..c1a09b24d 100644
--- a/composer.json
+++ b/composer.json
@@ -1,6 +1,6 @@
{
"name": "spatie/laravel-permission",
- "description": "Permission handling for Laravel 5.8 and up",
+ "description": "Permission handling for Laravel 6.0 and up",
"keywords": [
"spatie",
"laravel",
@@ -22,15 +22,15 @@
}
],
"require": {
- "php" : "^7.2.5|^8.0",
- "illuminate/auth": "^5.8|^6.0|^7.0|^8.0",
- "illuminate/container": "^5.8|^6.0|^7.0|^8.0",
- "illuminate/contracts": "^5.8|^6.0|^7.0|^8.0",
- "illuminate/database": "^5.8|^6.0|^7.0|^8.0"
+ "php" : "^7.3|^8.0|^8.1",
+ "illuminate/auth": "^7.0|^8.0|^9.0",
+ "illuminate/container": "^7.0|^8.0|^9.0",
+ "illuminate/contracts": "^7.0|^8.0|^9.0",
+ "illuminate/database": "^7.0|^8.0|^9.0"
},
"require-dev": {
- "orchestra/testbench": "^3.8|^4.0|^5.0|^6.0",
- "phpunit/phpunit": "^8.0|^9.0",
+ "orchestra/testbench": "^5.0|^6.0|^7.0",
+ "phpunit/phpunit": "^9.4",
"predis/predis": "^1.1"
},
"autoload": {
@@ -51,6 +51,10 @@
"providers": [
"Spatie\\Permission\\PermissionServiceProvider"
]
+ },
+ "branch-alias": {
+ "dev-main": "5.x-dev",
+ "dev-master": "5.x-dev"
}
},
"scripts": {
diff --git a/config/permission.php b/config/permission.php
index 1a4207e6c..5b6e184c3 100644
--- a/config/permission.php
+++ b/config/permission.php
@@ -72,6 +72,11 @@
],
'column_names' => [
+ /*
+ * Change this if you want to name the related pivots other than defaults
+ */
+ 'role_pivot_key' => null, //default 'role_id',
+ 'permission_pivot_key' => null, //default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
@@ -82,8 +87,32 @@
*/
'model_morph_key' => 'model_id',
+
+ /*
+ * Change this if you want to use the teams feature and your related model's
+ * foreign key is other than `team_id`.
+ */
+
+ 'team_foreign_key' => 'team_id',
],
+ /*
+ * When set to true, the method for checking permissions will be registered on the gate.
+ * Set this to false, if you want to implement custom logic for checking permissions.
+ */
+
+ 'register_permission_check_method' => true,
+
+ /*
+ * When set to true the package implements teams using the 'team_foreign_key'. If you want
+ * the migrations to register the 'team_foreign_key', you must set this to true
+ * before doing the migration. If you already did the migration then you must make a new
+ * migration to also add 'team_foreign_key' to 'roles', 'model_has_roles', and
+ * 'model_has_permissions'(view the latest version of package's migration file)
+ */
+
+ 'teams' => false,
+
/*
* When set to true, the required permission names are added to the exception
* message. This could be considered an information leak in some contexts, so
@@ -121,17 +150,6 @@
'key' => 'spatie.permission.cache',
- /*
- * When checking for a permission against a model by passing a Permission
- * instance to the check, this key determines what attribute on the
- * Permissions model is used to cache against.
- *
- * Ideally, this should match your preferred way of checking permissions, eg:
- * `$user->can('view-posts')` would be 'name'.
- */
-
- 'model_key' => 'name',
-
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub
new file mode 100644
index 000000000..6abdf8d8f
--- /dev/null
+++ b/database/migrations/add_teams_fields.php.stub
@@ -0,0 +1,94 @@
+unsignedBigInteger($columnNames['team_foreign_key'])->nullable()->after('id');
+ $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
+
+ $table->dropUnique('roles_name_guard_name_unique');
+ $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
+ });
+ }
+
+ if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) {
+ Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
+ $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');
+ $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
+
+ if (DB::getDriverName() !== 'sqlite') {
+ $table->dropForeign([PermissionRegistrar::$pivotPermission]);
+ }
+ $table->dropPrimary();
+
+ $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_permissions_permission_model_type_primary');
+ if (DB::getDriverName() !== 'sqlite') {
+ $table->foreign(PermissionRegistrar::$pivotPermission)
+ ->references('id')->on($tableNames['permissions'])->onDelete('cascade');
+ }
+ });
+ }
+
+ if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) {
+ Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
+ $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
+ $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
+
+ if (DB::getDriverName() !== 'sqlite') {
+ $table->dropForeign([PermissionRegistrar::$pivotRole]);
+ }
+ $table->dropPrimary();
+
+ $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_roles_role_model_type_primary');
+ if (DB::getDriverName() !== 'sqlite') {
+ $table->foreign(PermissionRegistrar::$pivotRole)
+ ->references('id')->on($tableNames['roles'])->onDelete('cascade');
+ }
+ });
+ }
+
+ app('cache')
+ ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
+ ->forget(config('permission.cache.key'));
+ }
+
+ /**
+ * Reverse the migrations.
+ *
+ * @return void
+ */
+ public function down()
+ {
+
+ }
+}
diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub
index 3f9448d0b..04c3278b9 100644
--- a/database/migrations/create_permission_tables.php.stub
+++ b/database/migrations/create_permission_tables.php.stub
@@ -3,6 +3,7 @@
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
+use Spatie\Permission\PermissionRegistrar;
class CreatePermissionTables extends Migration
{
@@ -15,72 +16,102 @@ class CreatePermissionTables extends Migration
{
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
+ $teams = config('permission.teams');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
+ if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
+ throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
+ }
Schema::create($tableNames['permissions'], function (Blueprint $table) {
- $table->bigIncrements('id');
- $table->string('name');
- $table->string('guard_name');
+ $table->bigIncrements('id'); // permission id
+ $table->string('name'); // For MySQL 8.0 use string('name', 125);
+ $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125);
$table->timestamps();
+
+ $table->unique(['name', 'guard_name']);
});
- Schema::create($tableNames['roles'], function (Blueprint $table) {
- $table->bigIncrements('id');
- $table->string('name');
- $table->string('guard_name');
+ Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
+ $table->bigIncrements('id'); // role id
+ if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
+ $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
+ $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
+ }
+ $table->string('name'); // For MySQL 8.0 use string('name', 125);
+ $table->string('guard_name'); // For MySQL 8.0 use string('guard_name', 125);
$table->timestamps();
+ if ($teams || config('permission.testing')) {
+ $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
+ } else {
+ $table->unique(['name', 'guard_name']);
+ }
});
- Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
- $table->unsignedBigInteger('permission_id');
+ Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) {
+ $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
- $table->foreign('permission_id')
- ->references('id')
+ $table->foreign(PermissionRegistrar::$pivotPermission)
+ ->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
+ if ($teams) {
+ $table->unsignedBigInteger($columnNames['team_foreign_key']);
+ $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
- $table->primary(['permission_id', $columnNames['model_morph_key'], 'model_type'],
+ $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_permissions_permission_model_type_primary');
+ } else {
+ $table->primary([PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
+ }
+
});
- Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
- $table->unsignedBigInteger('role_id');
+ Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $teams) {
+ $table->unsignedBigInteger(PermissionRegistrar::$pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
- $table->foreign('role_id')
- ->references('id')
+ $table->foreign(PermissionRegistrar::$pivotRole)
+ ->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
+ if ($teams) {
+ $table->unsignedBigInteger($columnNames['team_foreign_key']);
+ $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
- $table->primary(['role_id', $columnNames['model_morph_key'], 'model_type'],
+ $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_roles_role_model_type_primary');
+ } else {
+ $table->primary([PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
+ }
});
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) {
- $table->unsignedBigInteger('permission_id');
- $table->unsignedBigInteger('role_id');
+ $table->unsignedBigInteger(PermissionRegistrar::$pivotPermission);
+ $table->unsignedBigInteger(PermissionRegistrar::$pivotRole);
- $table->foreign('permission_id')
- ->references('id')
+ $table->foreign(PermissionRegistrar::$pivotPermission)
+ ->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
- $table->foreign('role_id')
- ->references('id')
+ $table->foreign(PermissionRegistrar::$pivotRole)
+ ->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
- $table->primary(['permission_id', 'role_id'], 'role_has_permissions_permission_id_role_id_primary');
+ $table->primary([PermissionRegistrar::$pivotPermission, PermissionRegistrar::$pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
diff --git a/docs/_index.md b/docs/_index.md
index f6aefce38..591491f89 100644
--- a/docs/_index.md
+++ b/docs/_index.md
@@ -1,6 +1,6 @@
---
-title: v3
+title: v5
slogan: Associate users with roles and permissions
githubUrl: https://github.com/spatie/laravel-permission
-branch: master
+branch: main
---
diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md
index 492bd11b5..3b1f8160e 100644
--- a/docs/advanced-usage/cache.md
+++ b/docs/advanced-usage/cache.md
@@ -10,9 +10,6 @@ Role and Permission data are cached to speed up performance.
When you **use the built-in functions** for manipulating roles and permissions, the cache is automatically reset for you, and relations are automatically reloaded for the current model record:
```php
-$user->assignRole('writer');
-$user->removeRole('writer');
-$user->syncRoles(params);
$role->givePermissionTo('edit articles');
$role->revokePermissionTo('edit articles');
$role->syncPermissions(params);
@@ -25,6 +22,14 @@ HOWEVER, if you manipulate permission/role data directly in the database instead
Additionally, because the Role and Permission models are Eloquent models which implement the `RefreshesPermissionCache` trait, creating and deleting Roles and Permissions will automatically clear the cache. If you have created your own models which do not extend the default models then you will need to implement the trait yourself.
+**NOTE: User-specific role/permission assignments are kept in-memory since v4.4.0, so the cache-reset is no longer called since v5.1.0 when updating User-related assignments.**
+Examples:
+```php
+// These do not call a cache-reset, because the User-related assignments are in-memory.
+$user->assignRole('writer');
+$user->removeRole('writer');
+$user->syncRoles(params);
+```
### Manual cache reset
To manually reset the cache for this package, you can run the following in your app code:
@@ -71,3 +76,12 @@ In `config/permission.php` set `cache.store` to the name of any one of the `conf
Setting `'cache.store' => 'array'` in `config/permission.php` will effectively disable caching by this package between requests (it will only cache in-memory until the current request is completed processing, never persisting it).
Alternatively, in development mode you can bypass ALL of Laravel's caching between visits by setting `CACHE_DRIVER=array` in `.env`. You can see an example of this in the default `phpunit.xml` file that comes with a new Laravel install. Of course, don't do this in production though!
+
+
+## File cache driver
+
+This situation is not specific to this package, but is mentioned here due to the common question being asked.
+
+If you are using the `File` cache driver and run into problems clearing the cache, it is most likely because your filesystem's permissions are preventing the PHP CLI from altering the cache files because the PHP-FPM process is running as a different user.
+
+Work with your server administrator to fix filesystem ownership on your cache files.
diff --git a/docs/advanced-usage/custom-permission-check.md b/docs/advanced-usage/custom-permission-check.md
new file mode 100644
index 000000000..f6bfb84d8
--- /dev/null
+++ b/docs/advanced-usage/custom-permission-check.md
@@ -0,0 +1,29 @@
+---
+title: Custom Permission Check
+weight: 6
+---
+
+By default, a method is registered on [Laravel's gate](https://laravel.com/docs/authorization). This method is responsible for checking if the user has the required permission or not. Whether a user has a permission or not is determined by checking the user's permissions stored in the database.
+
+However, in some cases, you might want to implement custom logic for checking if the user has a permission or not.
+
+Let's say that your application uses access tokens for authentication and when issuing the tokens, you add a custom claim containing all the permissions the user has. In this case, if you want to check whether the user has the required permission or not based on the permissions in your custom claim in the access token, then you need to implement your own logic for handling this.
+
+You could, for example, create a `before` method to handle this:
+
+**app/Providers/AuthServiceProvider.php**
+```php
+public function boot()
+{
+ ...
+
+ Gate::before(function ($user, $ability) {
+ return $user->hasTokenPermission($ability) ?: null;
+ });
+}
+```
+Here `hasTokenPermission` is a custom method you need to implement yourself.
+
+### Register Permission Check Method
+By default, `register_permission_check_method` is set to `true`.
+Only set this to false if you want to implement custom logic for checking permissions.
\ No newline at end of file
diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md
index fcc7f9d9e..6134ed59f 100644
--- a/docs/advanced-usage/exceptions.md
+++ b/docs/advanced-usage/exceptions.md
@@ -3,24 +3,23 @@ title: Exceptions
weight: 3
---
-If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/errors#render-method).
+If you need to override exceptions thrown by this package, you can simply use normal [Laravel practices for handling exceptions](https://laravel.com/docs/8.x/errors#rendering-exceptions).
An example is shown below for your convenience, but nothing here is specific to this package other than the name of the exception.
-You can find all the exceptions added by this package in the code here: https://github.com/spatie/laravel-permission/tree/master/src/Exceptions
+You can find all the exceptions added by this package in the code here: [https://github.com/spatie/laravel-permission/tree/main/src/Exceptions](https://github.com/spatie/laravel-permission/tree/main/src/Exceptions)
**app/Exceptions/Handler.php**
```php
-public function render($request, Throwable $exception)
+
+public function register()
{
- if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) {
+ $this->renderable(function (\Spatie\Permission\Exceptions\UnauthorizedException $e, $request) {
return response()->json([
'responseMessage' => 'You do not have the required authorization.',
'responseStatus' => 403,
]);
- }
-
- return parent::render($request, $exception);
+ });
}
```
diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md
index 0396fb5b2..5fd067925 100644
--- a/docs/advanced-usage/extending.md
+++ b/docs/advanced-usage/extending.md
@@ -32,18 +32,39 @@ Note the following requirements when extending/replacing the models:
### Extending
If you need to EXTEND the existing `Role` or `Permission` models note that:
-- Your `Role` model needs to extend the `Spatie\Permission\Models\Role` model
-- Your `Permission` model needs to extend the `Spatie\Permission\Models\Permission` model
-- You need to update `config/permisison.php` to specify your namespaced model
+- Your `Role` model needs to `extend` the `Spatie\Permission\Models\Role` model
+- Your `Permission` model needs to `extend` the `Spatie\Permission\Models\Permission` model
+- You need to update `config/permission.php` to specify your namespaced model
+
+eg:
+```php
+ **Note**
+> When using Laravel Idea plugin all directives are automatically added.
+
+You may wish to extend PhpStorm to support Blade Directives of this package.
1. In PhpStorm, open Preferences, and navigate to **Languages and Frameworks -> PHP -> Blade**
(File | Settings | Languages & Frameworks | PHP | Blade)
@@ -14,11 +19,17 @@ weight: 7
**role**
- has parameter = YES
-- Prefix: `check() && auth()->user()->hasRole(`
-- Suffix: `)); ?>`
+- Prefix: ``
--
+**elserole**
+
+- has parameter = YES
+- Prefix: ``
+
**endrole**
- has parameter = NO
@@ -30,8 +41,8 @@ weight: 7
**hasrole**
- has parameter = YES
-- Prefix: `check() && auth()->user()->hasRole(`
-- Suffix: `)); ?>`
+- Prefix: ``
--
@@ -46,8 +57,8 @@ weight: 7
**hasanyrole**
- has parameter = YES
-- Prefix: `check() && auth()->user()->hasAnyRole(`
-- Suffix: `)); ?>`
+- Prefix: ``
--
@@ -62,8 +73,8 @@ weight: 7
**hasallroles**
- has parameter = YES
-- Prefix: `check() && auth()->user()->hasAllRoles(`
-- Suffix: `)); ?>`
+- Prefix: ``
--
@@ -78,8 +89,8 @@ weight: 7
**unlessrole**
- has parameter = YES
-- Prefix: `check() && !auth()->user()->hasRole(`
-- Suffix: `)); ?>`
+- Prefix: ``
--
@@ -90,3 +101,17 @@ weight: 7
- Suffix: blank
--
+
+**hasexactroles**
+
+- has parameter = YES
+- Prefix: ``
+
+--
+
+**endhasexactroles**
+
+- has parameter = NO
+- Prefix: blank
+- Suffix: blank
diff --git a/docs/advanced-usage/seeding.md b/docs/advanced-usage/seeding.md
index e8b1fa266..fcff50ce7 100644
--- a/docs/advanced-usage/seeding.md
+++ b/docs/advanced-usage/seeding.md
@@ -75,7 +75,7 @@ $permissionsByRole = [
];
$insertPermissions = fn ($role) => collect($permissionsByRole[$role])
- ->map(fn ($name) => DB::table()->insertGetId(['name' => $name]))
+ ->map(fn ($name) => DB::table('permissions')->insertGetId(['name' => $name, 'guard_name' => 'web']))
->toArray();
$permissionIdsByRole = [
diff --git a/docs/advanced-usage/testing.md b/docs/advanced-usage/testing.md
index bd9e64c50..d0cd3e7fe 100644
--- a/docs/advanced-usage/testing.md
+++ b/docs/advanced-usage/testing.md
@@ -20,6 +20,17 @@ In your tests simply add a `setUp()` instruction to re-register the permissions,
}
```
+If you are using Laravel's `LazilyRefreshDatabase` trait, you most likely want to avoid seeding permissions before every test, because that would negate the use of the `LazilyRefreshDatabase` trait. To overcome this, you should wrap your seeder in an event listener for the `MigrationsEnded` event:
+
+```php
+Event::listen(MigrationsEnded::class, function () {
+ $this->artisan('db:seed', ['--class' => RoleAndPermissionSeeder::class]);
+ $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
+});
+```
+
+Note that we call `PermissionRegistrar::forgetCachedPermissions` after seeding. This is to prevent a caching issue that can occur when the database is set up after permissions have already been registered and cached.
+
## Factories
Many applications do not require using factories to create fake roles/permissions for testing, because they use a Seeder to create specific roles and permissions that the application uses; thus tests are performed using the declared roles and permissions.
diff --git a/docs/advanced-usage/timestamps.md b/docs/advanced-usage/timestamps.md
index a19247658..7ab577ba4 100644
--- a/docs/advanced-usage/timestamps.md
+++ b/docs/advanced-usage/timestamps.md
@@ -1,6 +1,6 @@
---
title: Timestamps
-weight: 8
+weight: 10
---
### Excluding Timestamps from JSON
diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md
index 347142464..e8029ff22 100644
--- a/docs/advanced-usage/ui-options.md
+++ b/docs/advanced-usage/ui-options.md
@@ -1,6 +1,6 @@
---
title: UI Options
-weight: 10
+weight: 11
---
## Need a UI?
@@ -18,3 +18,6 @@ The package doesn't come with any screens out of the box, you should build that
- [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil)
- [Generating UI boilerplate using InfyOm](https://youtu.be/hlGu2pa1bdU) video tutorial by [Shailesh](https://github.com/shailesh-ladumor)
+
+
+- [LiveWire Base Admin Panel](https://github.com/alighasemzadeh/bap) User management by [AliGhasemzadeh](https://github.com/alighasemzadeh)
diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md
index a37d1e437..299f83c64 100644
--- a/docs/advanced-usage/uuid.md
+++ b/docs/advanced-usage/uuid.md
@@ -1,6 +1,6 @@
---
title: UUID
-weight: 6
+weight: 7
---
If you're using UUIDs or GUIDs for your User models there are a few considerations to note.
@@ -96,10 +96,8 @@ It is common to use a trait to handle the $keyType and $incrementing settings, a
trait UuidTrait
{
- protected static function bootUuidTrait()
+ public static function bootUuidTrait()
{
- parent::boot();
-
static::creating(function ($model) {
$model->keyType = 'string';
$model->incrementing = false;
diff --git a/docs/basic-usage/artisan.md b/docs/basic-usage/artisan.md
index b8c98dd53..8cf340d1b 100644
--- a/docs/basic-usage/artisan.md
+++ b/docs/basic-usage/artisan.md
@@ -31,6 +31,13 @@ When creating roles you can also create and link permissions at the same time:
php artisan permission:create-role writer web "create articles|edit articles"
```
+When creating roles with teams enabled you can set the team id by adding the `--team-id` parameter:
+
+```bash
+php artisan permission:create-role --team-id=1 writer
+php artisan permission:create-role writer api --team-id=1
+```
+
## Displaying roles and permissions in the console
There is also a `show` command to show a table of roles and permissions per guard:
diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md
index f1508ea5a..28c30efd2 100644
--- a/docs/basic-usage/basic-usage.md
+++ b/docs/basic-usage/basic-usage.md
@@ -50,7 +50,7 @@ $role->revokePermissionTo($permission);
$permission->removeRole($role);
```
-If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](../multiple-guards) section of the readme.
+If you're using multiple guards the `guard_name` attribute needs to be set as well. Read about it in the [using multiple guards](./multiple-guards) section of the readme.
The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query:
diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md
index 1c1aaaaff..a3a4b8af1 100644
--- a/docs/basic-usage/blade-directives.md
+++ b/docs/basic-usage/blade-directives.md
@@ -89,3 +89,12 @@ Alternatively, `@unlessrole` gives the reverse for checking a singular role, lik
@endunlessrole
```
+You can also determine if a user has exactly all of a given list of roles:
+
+```php
+@hasexactroles('writer|admin')
+ I am both a writer and an admin and nothing else!
+@else
+ I do not have all of these roles or have more other roles...
+@endhasexactroles
+```
diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md
index 564c232ea..27f453cb1 100644
--- a/docs/basic-usage/multiple-guards.md
+++ b/docs/basic-usage/multiple-guards.md
@@ -38,7 +38,7 @@ $user->hasPermissionTo('publish articles', 'admin');
> **Note**: When determining whether a role/permission is valid on a given model, it checks against the first matching guard in this order (it does NOT check role/permission for EACH possibility, just the first match):
- first the guardName() method if it exists on the model;
- then the `$guard_name` property if it exists on the model;
-- then the first-defined guard/provider combination in the `auth.guards` config array that matches the logged-in user's guard;
+- then the first-defined guard/provider combination in the `auth.guards` config array that matches the loaded model's guard;
- then the `auth.defaults.guard` config (which is the user's guard if they are logged in, else the default in the file).
diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md
index b35d50928..449955d9d 100644
--- a/docs/basic-usage/new-app.md
+++ b/docs/basic-usage/new-app.md
@@ -35,8 +35,8 @@ git commit -m "Add Spatie Laravel Permissions package"
php artisan migrate:fresh
# Add `HasRoles` trait to User model
-sed -i '' $'s/use Notifiable;/use Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/User.php
sed -i '' $'s/use HasFactory, Notifiable;/use HasFactory, Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/Models/User.php
+sed -i '' $'s/use HasApiTokens, HasFactory, Notifiable;/use HasApiTokens, HasFactory, Notifiable;\\\n use \\\\Spatie\\\\Permission\\\\Traits\\\\HasRoles;/' app/Models/User.php
git add . && git commit -m "Add HasRoles trait"
# Add Laravel's basic auth scaffolding
@@ -86,7 +86,7 @@ class PermissionsDemoSeeder extends Seeder
$role2->givePermissionTo('publish articles');
$role2->givePermissionTo('unpublish articles');
- $role3 = Role::create(['name' => 'super-admin']);
+ $role3 = Role::create(['name' => 'Super-Admin']);
// gets all permissions via Gate::before rule; see AuthServiceProvider
// create demo users
@@ -119,8 +119,9 @@ php artisan migrate:fresh --seed --seeder=PermissionsDemoSeeder
```
### Grant Super-Admin access
-Super-Admins are a common feature. Using the following approach allows that when your Super-Admin user is logged in, all permission-checks in your app which call `can()` or `@can()` will return true.
+Super-Admins are a common feature. The following approach allows that when your Super-Admin user is logged in, all permission-checks in your app which call `can()` or `@can()` will return true.
+- Create a role named `Super-Admin`. (Or whatever name you wish; but use it consistently just like you must with any role name.)
- Add a Gate::before check in your `AuthServiceProvider`:
```diff
@@ -130,7 +131,7 @@ Super-Admins are a common feature. Using the following approach allows that when
//
-+ // Implicitly grant "Super Admin" role all permission checks using can()
++ // Implicitly grant "Super-Admin" role all permission checks using can()
+ Gate::before(function ($user, $ability) {
+ if ($user->hasRole('Super-Admin')) {
+ return true;
@@ -155,7 +156,7 @@ To share your app on Github for easy collaboration:
```sh
git remote add origin git@github.com:YOURUSERNAME/REPONAME.git
-git push -u origin master
+git push -u origin main
```
The above only needs to be done once.
@@ -164,7 +165,7 @@ The above only needs to be done once.
```sh
git add .
git commit -m "Explain what your commit is about here"
-git push origin master
+git push origin main
```
Repeat the above process whenever you change code that you want to share.
diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md
index 8486a0d65..476610957 100644
--- a/docs/basic-usage/role-permissions.md
+++ b/docs/basic-usage/role-permissions.md
@@ -1,8 +1,10 @@
---
-title: Using permissions via roles
+title: Using Permissions via Roles
weight: 3
---
+## Assigning Roles
+
A role can be assigned to any user:
```php
@@ -27,6 +29,8 @@ Roles can also be synced:
$user->syncRoles(['writer', 'admin']);
```
+## Checking Roles
+
You can determine if a user has a certain role:
```php
@@ -50,9 +54,18 @@ You can also determine if a user has all of a given list of roles:
$user->hasAllRoles(Role::all());
```
-The `assignRole`, `hasRole`, `hasAnyRole`, `hasAllRoles` and `removeRole` functions can accept a
+You can also determine if a user has exactly all of a given list of roles:
+
+```php
+$user->hasExactRoles(Role::all());
+```
+
+The `assignRole`, `hasRole`, `hasAnyRole`, `hasAllRoles`, `hasExactRoles` and `removeRole` functions can accept a
string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object.
+
+## Assigning Permissions to Roles
+
A permission can be given to a role:
```php
@@ -71,11 +84,38 @@ A permission can be revoked from a role:
$role->revokePermissionTo('edit articles');
```
+Or revoke & add new permissions in one go:
+
+```php
+$role->syncPermissions(['edit articles', 'delete articles']);
+```
+
The `givePermissionTo` and `revokePermissionTo` functions can accept a
string or a `Spatie\Permission\Models\Permission` object.
-Permissions are inherited from roles automatically.
+**NOTE: Permissions are inherited from roles automatically.**
+
+
+### What Permissions Does A Role Have?
+
+The `permissions` property on any given role returns a collection with all the related permission objects. This collection can respond to usual Eloquent Collection operations, such as count, sort, etc.
+
+```php
+// get collection
+$role->permissions;
+
+// return only the permission names:
+$role->permissions->pluck('name');
+
+// count the number of permissions assigned to a role
+count($role->permissions);
+// or
+$role->permissions->count();
+```
+
+## Assigning Direct Permissions To A User
+
Additionally, individual permissions can be assigned to the user too.
For instance:
@@ -95,9 +135,13 @@ but `false` for `$user->hasDirectPermission('edit articles')`.
This method is useful if one builds a form for setting permissions for roles and users in an application and wants to restrict or change inherited permissions of roles of the user, i.e. allowing to change only direct permissions of the user.
-You can check if the user has All or Any of a set of permissions directly assigned:
+
+You can check if the user has a Specific or All or Any of a set of permissions directly assigned:
```php
+// Check if the user has Direct permission
+$user->hasDirectPermission('edit articles')
+
// Check if the user has All direct permissions
$user->hasAllDirectPermissions(['edit articles', 'delete articles']);
@@ -105,12 +149,12 @@ $user->hasAllDirectPermissions(['edit articles', 'delete articles']);
$user->hasAnyDirectPermission(['create articles', 'delete articles']);
```
By following the previous example, when we call `$user->hasAllDirectPermissions(['edit articles', 'delete articles'])`
-it returns `true`, because the user has all these direct permissions.
+it returns `false`, because the user does not have `edit articles` as a direct permission.
When we call
`$user->hasAnyDirectPermission('edit articles')`, it returns `true` because the user has one of the provided permissions.
-You can list all of these permissions:
+You can examine all of these permissions:
```php
// Direct permissions
@@ -125,11 +169,6 @@ $user->getAllPermissions();
All these responses are collections of `Spatie\Permission\Models\Permission` objects.
-
-
-If we follow the previous example, the first response will be a collection with the `delete article` permission and
-the second will be a collection with the `edit article` permission and the third will contain both.
-
If we follow the previous example, the first response will be a collection with the `delete article` permission and
the second will be a collection with the `edit article` permission and the third will contain both.
@@ -137,4 +176,4 @@ the second will be a collection with the `edit article` permission and the third
### NOTE about using permission names in policies
-When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at https://laravel.com/docs/authorization#writing-policies
+When calling `authorize()` for a policy method, if you have a permission named the same as one of those policy methods, your permission "name" will take precedence and not fire the policy. For this reason it may be wise to avoid naming your permissions the same as the methods in your policy. While you can define your own method names, you can read more about the defaults Laravel offers in Laravel's documentation at [Writing Policies](https://laravel.com/docs/authorization#writing-policies).
diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md
index d48ad254f..426bbc036 100644
--- a/docs/basic-usage/super-admin.md
+++ b/docs/basic-usage/super-admin.md
@@ -38,7 +38,7 @@ Jeffrey Way explains the concept of a super-admin (and a model owner, and model
Alternatively you might want to move the Super Admin check to the `Gate::after` phase instead, particularly if your Super Admin shouldn't be allowed to do things your app doesn't want "anyone" to do, such as writing more than 1 review, or bypassing unsubscribe rules, etc.
-The following code snippet is inspired from [Freek's blog article](https://murze.be/when-to-use-gateafter-in-laravel) where this topic is discussed further.
+The following code snippet is inspired from [Freek's blog article](https://freek.dev/1325-when-to-use-gateafter-in-laravel) where this topic is discussed further.
```php
// somewhere in a service provider
diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md
new file mode 100644
index 000000000..7c2bf2f54
--- /dev/null
+++ b/docs/basic-usage/teams-permissions.md
@@ -0,0 +1,98 @@
+---
+title: Teams permissions
+weight: 3
+---
+
+NOTE: Those changes must be made before performing the migration. If you have already run the migration and want to upgrade your solution, you can run the artisan console command `php artisan permission:setup-teams`, to create a new migration file named [xxxx_xx_xx_xx_add_teams_fields.php](https://github.com/spatie/laravel-permission/blob/main/database/migrations/add_teams_fields.php.stub) and then run `php artisan migrate` to upgrade your database tables.
+
+When enabled, teams permissions offers you a flexible control for a variety of scenarios. The idea behind teams permissions is inspired by the default permission implementation of [Laratrust](https://laratrust.santigarcor.me/).
+
+
+Teams permissions can be enabled in the permission config file:
+
+```php
+// config/permission.php
+'teams' => true,
+```
+
+Also, if you want to use a custom foreign key for teams you must change in the permission config file:
+```php
+// config/permission.php
+'team_foreign_key' => 'custom_team_id',
+```
+
+## Working with Teams Permissions
+
+After implements on login a solution for select a team on authentication (for example set `team_id` of the current selected team on **session**: `session(['team_id' => $team->team_id]);` ),
+we can set global `team_id` from anywhere, but works better if you create a `Middleware`, example:
+
+```php
+namespace App\Http\Middleware;
+
+class TeamsPermission{
+
+ public function handle($request, \Closure $next){
+ if(!empty(auth()->user())){
+ // session value set on login
+ setPermissionsTeamId(session('team_id'));
+ }
+ // other custom ways to get team_id
+ /*if(!empty(auth('api')->user())){
+ // `getTeamIdFromToken()` example of custom method for getting the set team_id
+ setPermissionsTeamId(auth('api')->user()->getTeamIdFromToken());
+ }*/
+
+ return $next($request);
+ }
+}
+```
+NOTE: You must add your custom `Middleware` to `$middlewarePriority` on `app/Http/Kernel.php`.
+
+## Roles Creating
+
+When creating a role you can pass the `team_id` as an optional parameter
+
+```php
+// with null team_id it creates a global role, global roles can be assigned to any team and they are unique
+Role::create(['name' => 'writer', 'team_id' => null]);
+
+// creates a role with team_id = 1, team roles can have the same name on different teams
+Role::create(['name' => 'reader', 'team_id' => 1]);
+
+// creating a role without team_id makes the role take the default global team_id
+Role::create(['name' => 'reviewer']);
+```
+
+## Roles/Permissions Assignment & Removal
+
+The role/permission assignment and removal are the same, but they take the global `team_id` set on login for sync.
+
+## Defining a Super-Admin on Teams
+
+Global roles can be assigned to different teams, `team_id` as the primary key of the relationships is always required. If you want a "Super Admin" global role for a user, when you creates a new team you must assign it to your user. Example:
+
+```php
+namespace App\Models;
+
+class YourTeamModel extends \Illuminate\Database\Eloquent\Model
+{
+ // ...
+ public static function boot()
+ {
+ parent::boot();
+
+ // here assign this team to a global user with global default role
+ self::created(function ($model) {
+ // get session team_id for restore it later
+ $session_team_id = getPermissionsTeamId();
+ // set actual new team_id to package instance
+ setPermissionsTeamId($model);
+ // get the admin user and assign roles/permissions on new team model
+ User::find('your_user_id')->assignRole('Super Admin');
+ // restore session team_id to package instance
+ setPermissionsTeamId($session_team_id);
+ });
+ }
+ // ...
+}
+```
diff --git a/docs/best-practices/roles-vs-permissions.md b/docs/best-practices/roles-vs-permissions.md
index 50be7b117..b7d99b01f 100644
--- a/docs/best-practices/roles-vs-permissions.md
+++ b/docs/best-practices/roles-vs-permissions.md
@@ -3,9 +3,9 @@ title: Roles vs Permissions
weight: 1
---
-It is generally best to code your app around `permissions` only. That way you can always use the native Laravel `@can` and `can()` directives everywhere in your app.
+It is generally best to code your app around testing against `permissions` only. (ie: when testing whether to grant access to something, in most cases it's wisest to check against a `permission`, not a `role`). That way you can always use the native Laravel `@can` and `can()` directives everywhere in your app.
-Roles can still be used to group permissions for easy assignment, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting.
+Roles can still be used to group permissions for easy assignment to a user/model, and you can still use the role-based helper methods if truly necessary. But most app-related logic can usually be best controlled using the `can` methods, which allows Laravel's Gate layer to do all the heavy lifting. Sometimes certain groups of `route` rules may make best sense to group them around a `role`, but still, whenever possible, there is less overhead used if you can check against a specific `permission` instead.
eg: `users` have `roles`, and `roles` have `permissions`, and your app always checks for `permissions`, not `roles`.
diff --git a/docs/changelog.md b/docs/changelog.md
index 27d734a9d..3280d4f29 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -3,4 +3,4 @@ title: Changelog
weight: 10
---
-All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/master/CHANGELOG.md)
+All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md)
diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md
index 616438c5f..c37db5d91 100644
--- a/docs/installation-laravel.md
+++ b/docs/installation-laravel.md
@@ -3,9 +3,13 @@ title: Installation in Laravel
weight: 4
---
-This package can be used with Laravel 5.8 or higher.
+This package can be used with Laravel 6.0 or higher.
-1. Consult the Prerequisites page for important considerations regarding your User models!
+(For Laravel 5.8, use v3.17.0)
+
+## Installing
+
+1. Consult the **Prerequisites** page for important considerations regarding your **User** models!
2. This package publishes a `config/permission.php` file. If you already have a file by that name, you must rename or remove it.
@@ -22,13 +26,14 @@ This package can be used with Laravel 5.8 or higher.
];
```
-5. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) with:
+5. You should publish [the migration](https://github.com/spatie/laravel-permission/blob/main/database/migrations/create_permission_tables.php.stub) and the [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) with:
```
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
```
6. NOTE: If you are using UUIDs, see the Advanced section of the docs on UUID steps, before you continue. It explains some changes you may want to make to the migrations and config file before continuing. It also mentions important considerations after extending this package's models for UUID capability.
+ If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`.
7. Clear your config cache. This package requires access to the `permission` config. Generally it's bad practice to do config-caching in a development environment. If you've been caching configurations locally, clear your config cache with either of these commands:
@@ -47,4 +52,4 @@ This package can be used with Laravel 5.8 or higher.
You can view the default config file contents at:
-https://github.com/spatie/laravel-permission/blob/master/config/permission.php
+[https://github.com/spatie/laravel-permission/blob/main/config/permission.php](https://github.com/spatie/laravel-permission/blob/main/config/permission.php)
diff --git a/docs/installation-lumen.md b/docs/installation-lumen.md
index b2b350b51..ff3d98455 100644
--- a/docs/installation-lumen.md
+++ b/docs/installation-lumen.md
@@ -3,9 +3,11 @@ title: Installation in Lumen
weight: 5
---
-NOTE: Lumen is not officially supported by this package. However, the following are some steps which may help get you started.
+NOTE: Lumen is **not** officially supported by this package. However, the following are some steps which may help get you started.
-First, install the package via Composer:
+Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs/main).
+
+Install the permissions package via Composer:
``` bash
composer require spatie/laravel-permission
@@ -48,7 +50,9 @@ $app->register(Spatie\Permission\PermissionServiceProvider::class);
$app->register(App\Providers\AuthServiceProvider::class);
```
-Ensure your database configuration is set in your `.env` (or `config/database.php` if you have one).
+Ensure the application's database name/credentials are set in your `.env` (or `config/database.php` if you have one), and that the database exists.
+
+NOTE: If you are going to use teams feature, you have to update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php) and set `'teams' => true,`, if you want to use a custom foreign key for teams you must change `team_foreign_key`.
Run the migrations to create the tables for this package:
@@ -64,8 +68,8 @@ NOTE: Remember that Laravel's authorization layer requires that your `User` mode
### User Table
NOTE: If you are working with a fresh install of Lumen, then you probably also need a migration file for your Users table. You can create your own, or you can copy a basic one from Laravel:
-https://github.com/laravel/laravel/blob/master/database/migrations/2014_10_12_000000_create_users_table.php
+[https://github.com/laravel/laravel/blob/main/database/migrations/2014_10_12_000000_create_users_table.php](https://github.com/laravel/laravel/blob/main/database/migrations/2014_10_12_000000_create_users_table.php)
(You will need to run `php artisan migrate` after adding this file.)
-Remember to update your ModelFactory.php to match the fields in the migration you create/copy.
+Remember to update your UserFactory.php to match the fields in the migration you create/copy.
diff --git a/docs/introduction.md b/docs/introduction.md
index c1bed6dc2..604fa8cac 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -17,7 +17,7 @@ $user->assignRole('writer');
$role->givePermissionTo('edit articles');
```
-If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](https://docs.spatie.be/laravel-permission/v3/basic-usage/multiple-guards/) section of the readme.
+If you're using multiple guards we've got you covered as well. Every guard will have its own set of permissions and roles that can be assigned to the guard's users. Read about it in the [using multiple guards](./basic-usage/multiple-guards/) section.
Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/authorization), you can check if a user has a permission with Laravel's default `can` function:
@@ -31,4 +31,4 @@ and Blade directives:
@can('edit articles')
...
@endcan
-```
\ No newline at end of file
+```
diff --git a/docs/prerequisites.md b/docs/prerequisites.md
index 57d133439..fc64ed7a8 100644
--- a/docs/prerequisites.md
+++ b/docs/prerequisites.md
@@ -3,7 +3,11 @@ title: Prerequisites
weight: 3
---
-This package can be used in Laravel 5.8 or higher.
+## Laravel Version
+
+This package can be used in Laravel 6 or higher.
+
+## User Model / Contract/Interface
This package uses Laravel's Gate layer to provide Authorization capabilities.
The Gate/authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract.
@@ -25,9 +29,25 @@ class User extends Authenticatable
}
```
+## Must not have a `role` or `roles` property, nor a `roles()` method
+
Additionally, your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database), nor a `roles()` method on it. Those will interfere with the properties and methods added by the `HasRoles` trait provided by this package, thus causing unexpected outcomes when this package's methods are used to inspect roles and permissions.
+## Must not have a `permission` or `permissions` property, nor a `permissions()` method
+
Similarly, your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database), nor a `permissions()` method on it. Those will interfere with the properties and methods added by the `HasPermissions` trait provided by this package (which is invoked via the `HasRoles` trait).
+## Config file
+
This package publishes a `config/permission.php` file. If you already have a file by that name, you must rename or remove it, as it will conflict with this package. You could optionally merge your own values with those required by this package, as long as the keys that this package expects are present. See the source file for more details.
+## Schema Limitation in MySQL
+
+MySQL 8.0 limits index keys to 1000 characters. This package publishes a migration which combines multiple columns in single index. With `utf8mb4` the 4-bytes-per-character requirement of `mb4` means the max length of the columns in the hybrid index can only be `125` characters.
+
+Thus in your AppServiceProvider you will need to set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/migrations#index-lengths-mysql-mariadb).
+
+## Note for apps using UUIDs/ULIDs/GUIDs
+
+This package expects the primary key of your `User` model to be an auto-incrementing `int`. If it is not, you may need to modify the `create_permission_tables` migration and/or modify the default configuration. See [https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/v5/advanced-usage/uuid) for more information.
+
diff --git a/ide.json b/ide.json
new file mode 100644
index 000000000..7afcd2a45
--- /dev/null
+++ b/ide.json
@@ -0,0 +1,72 @@
+{
+ "$schema": "https://laravel-ide.com/schema/laravel-ide-v2.json",
+ "blade": {
+ "directives": [
+ {
+ "name": "role",
+ "prefix": ""
+ },
+ {
+ "name": "elserole",
+ "prefix": ""
+ },
+ {
+ "name": "endrole",
+ "prefix": "",
+ "suffix": ""
+ },
+ {
+ "name": "hasrole",
+ "prefix": ""
+ },
+ {
+ "name": "endhasrole",
+ "prefix": "",
+ "suffix": ""
+ },
+ {
+ "name": "hasanyrole",
+ "prefix": ""
+ },
+ {
+ "name": "endhasanyrole",
+ "prefix": "",
+ "suffix": ""
+ },
+ {
+ "name": "hasallroles",
+ "prefix": ""
+ },
+ {
+ "name": "endhasallroles",
+ "prefix": "",
+ "suffix": ""
+ },
+ {
+ "name": "unlessrole",
+ "prefix": ""
+ },
+ {
+ "name": "endunlessrole",
+ "prefix": "",
+ "suffix": ""
+ },
+ {
+ "name": "hasexactroles",
+ "prefix": ""
+ },
+ {
+ "name": "endhasexactroles",
+ "prefix": "",
+ "suffix": ""
+ }
+ ]
+ }
+}
diff --git a/src/Commands/CreatePermission.php b/src/Commands/CreatePermission.php
index f71a63240..c3bc20693 100644
--- a/src/Commands/CreatePermission.php
+++ b/src/Commands/CreatePermission.php
@@ -19,6 +19,6 @@ public function handle()
$permission = $permissionClass::findOrCreate($this->argument('name'), $this->argument('guard'));
- $this->info("Permission `{$permission->name}` created");
+ $this->info("Permission `{$permission->name}` ".($permission->wasRecentlyCreated ? 'created' : 'already exists'));
}
}
diff --git a/src/Commands/CreateRole.php b/src/Commands/CreateRole.php
index 426d6f0c7..e4701f014 100644
--- a/src/Commands/CreateRole.php
+++ b/src/Commands/CreateRole.php
@@ -5,13 +5,15 @@
use Illuminate\Console\Command;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Contracts\Role as RoleContract;
+use Spatie\Permission\PermissionRegistrar;
class CreateRole extends Command
{
protected $signature = 'permission:create-role
{name : The name of the role}
{guard? : The name of the guard}
- {permissions? : A list of permissions to assign to the role, separated by | }';
+ {permissions? : A list of permissions to assign to the role, separated by | }
+ {--team-id=}';
protected $description = 'Create a role';
@@ -19,11 +21,26 @@ public function handle()
{
$roleClass = app(RoleContract::class);
+ $teamIdAux = getPermissionsTeamId();
+ setPermissionsTeamId($this->option('team-id') ?: null);
+
+ if (! PermissionRegistrar::$teams && $this->option('team-id')) {
+ $this->warn("Teams feature disabled, argument --team-id has no effect. Either enable it in permissions config file or remove --team-id parameter");
+
+ return;
+ }
+
$role = $roleClass::findOrCreate($this->argument('name'), $this->argument('guard'));
+ setPermissionsTeamId($teamIdAux);
+
+ $teams_key = PermissionRegistrar::$teamsKey;
+ if (PermissionRegistrar::$teams && $this->option('team-id') && is_null($role->$teams_key)) {
+ $this->warn("Role `{$role->name}` already exists on the global team; argument --team-id has no effect");
+ }
$role->givePermissionTo($this->makePermissions($this->argument('permissions')));
- $this->info("Role `{$role->name}` created");
+ $this->info("Role `{$role->name}` ".($role->wasRecentlyCreated ? 'created' : 'updated'));
}
/**
diff --git a/src/Commands/Show.php b/src/Commands/Show.php
index c0aa0e33d..fbd61967a 100644
--- a/src/Commands/Show.php
+++ b/src/Commands/Show.php
@@ -6,6 +6,7 @@
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Contracts\Role as RoleContract;
+use Symfony\Component\Console\Helper\TableCell;
class Show extends Command
{
@@ -19,6 +20,7 @@ public function handle()
{
$permissionClass = app(PermissionContract::class);
$roleClass = app(RoleContract::class);
+ $team_key = config('permission.column_names.team_foreign_key');
$style = $this->argument('style') ?? 'default';
$guard = $this->argument('guard');
@@ -32,20 +34,39 @@ public function handle()
foreach ($guards as $guard) {
$this->info("Guard: $guard");
- $roles = $roleClass::whereGuardName($guard)->orderBy('name')->get()->mapWithKeys(function ($role) {
- return [$role->name => $role->permissions->pluck('name')];
- });
+ $roles = $roleClass::whereGuardName($guard)
+ ->with('permissions')
+ ->when(config('permission.teams'), function ($q) use ($team_key) {
+ $q->orderBy($team_key);
+ })
+ ->orderBy('name')->get()->mapWithKeys(function ($role) use ($team_key) {
+ return [$role->name.'_'.($role->$team_key ?: '') => ['permissions' => $role->permissions->pluck('id'), $team_key => $role->$team_key ]];
+ });
- $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name');
+ $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', 'id');
- $body = $permissions->map(function ($permission) use ($roles) {
- return $roles->map(function (Collection $role_permissions) use ($permission) {
- return $role_permissions->contains($permission) ? ' ✔' : ' ·';
+ $body = $permissions->map(function ($permission, $id) use ($roles) {
+ return $roles->map(function (array $role_data) use ($id) {
+ return $role_data['permissions']->contains($id) ? ' ✔' : ' ·';
})->prepend($permission);
});
+ if (config('permission.teams')) {
+ $teams = $roles->groupBy($team_key)->values()->map(function ($group, $id) {
+ return new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()]);
+ });
+ }
+
$this->table(
- $roles->keys()->prepend('')->toArray(),
+ array_merge([
+ config('permission.teams') ? $teams->prepend('')->toArray() : [],
+ $roles->keys()->map(function ($val) {
+ $name = explode('_', $val);
+
+ return $name[0];
+ })
+ ->prepend('')->toArray(),
+ ]),
$body->toArray(),
$style
);
diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php
new file mode 100644
index 000000000..9923ca19d
--- /dev/null
+++ b/src/Commands/UpgradeForTeams.php
@@ -0,0 +1,127 @@
+error('Teams feature is disabled in your permission.php file.');
+ $this->warn('Please enable the teams setting in your configuration.');
+
+ return;
+ }
+
+ $this->line('');
+ $this->info("The teams feature setup is going to add a migration and a model");
+
+ $existingMigrations = $this->alreadyExistingMigrations();
+
+ if ($existingMigrations) {
+ $this->line('');
+
+ $this->warn($this->getExistingMigrationsWarning($existingMigrations));
+ }
+
+ $this->line('');
+
+ if (! $this->confirm("Proceed with the migration creation?", "yes")) {
+ return;
+ }
+
+ $this->line('');
+
+ $this->line("Creating migration");
+
+ if ($this->createMigration()) {
+ $this->info("Migration created successfully.");
+ } else {
+ $this->error(
+ "Couldn't create migration.\n".
+ "Check the write permissions within the database/migrations directory."
+ );
+ }
+
+ $this->line('');
+ }
+
+ /**
+ * Create the migration.
+ *
+ * @return bool
+ */
+ protected function createMigration()
+ {
+ try {
+ $migrationStub = __DIR__."/../../database/migrations/{$this->migrationSuffix}.stub";
+ copy($migrationStub, $this->getMigrationPath());
+
+ return true;
+ } catch (\Throwable $e) {
+ $this->error($e->getMessage());
+
+ return false;
+ }
+ }
+
+ /**
+ * Build a warning regarding possible duplication
+ * due to already existing migrations.
+ *
+ * @param array $existingMigrations
+ * @return string
+ */
+ protected function getExistingMigrationsWarning(array $existingMigrations)
+ {
+ if (count($existingMigrations) > 1) {
+ $base = "Setup teams migrations already exist.\nFollowing files were found: ";
+ } else {
+ $base = "Setup teams migration already exists.\nFollowing file was found: ";
+ }
+
+ return $base . array_reduce($existingMigrations, function ($carry, $fileName) {
+ return $carry . "\n - " . $fileName;
+ });
+ }
+
+ /**
+ * Check if there is another migration
+ * with the same suffix.
+ *
+ * @return array
+ */
+ protected function alreadyExistingMigrations()
+ {
+ $matchingFiles = glob($this->getMigrationPath('*'));
+
+ return array_map(function ($path) {
+ return basename($path);
+ }, $matchingFiles);
+ }
+
+ /**
+ * Get the migration path.
+ *
+ * The date parameter is optional for ability
+ * to provide a custom value or a wildcard.
+ *
+ * @param string|null $date
+ * @return string
+ */
+ protected function getMigrationPath($date = null)
+ {
+ $date = $date ?: date('Y_m_d_His');
+
+ return database_path("migrations/{$date}_{$this->migrationSuffix}");
+ }
+}
diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php
index 373f426cc..2a270faf8 100644
--- a/src/Exceptions/UnauthorizedException.php
+++ b/src/Exceptions/UnauthorizedException.php
@@ -14,9 +14,8 @@ public static function forRoles(array $roles): self
{
$message = 'User does not have the right roles.';
- if (config('permission.display_permission_in_exception')) {
- $permStr = implode(', ', $roles);
- $message = 'User does not have the right roles. Necessary roles are '.$permStr;
+ if (config('permission.display_role_in_exception')) {
+ $message .= ' Necessary roles are '.implode(', ', $roles);
}
$exception = new static(403, $message, null, []);
@@ -30,8 +29,7 @@ public static function forPermissions(array $permissions): self
$message = 'User does not have the right permissions.';
if (config('permission.display_permission_in_exception')) {
- $permStr = implode(', ', $permissions);
- $message = 'User does not have the right permissions. Necessary permissions are '.$permStr;
+ $message .= ' Necessary permissions are '.implode(', ', $permissions);
}
$exception = new static(403, $message, null, []);
@@ -45,8 +43,7 @@ public static function forRolesOrPermissions(array $rolesOrPermissions): self
$message = 'User does not have any of the necessary access rights.';
if (config('permission.display_permission_in_exception') && config('permission.display_role_in_exception')) {
- $permStr = implode(', ', $rolesOrPermissions);
- $message = 'User does not have the right permissions. Necessary permissions are '.$permStr;
+ $message .= ' Necessary roles or permissions are '.implode(', ', $rolesOrPermissions);
}
$exception = new static(403, $message, null, []);
diff --git a/src/Guard.php b/src/Guard.php
index df63b432e..c0630392b 100644
--- a/src/Guard.php
+++ b/src/Guard.php
@@ -2,29 +2,31 @@
namespace Spatie\Permission;
+use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
class Guard
{
/**
- * return collection of (guard_name) property if exist on class or object
- * otherwise will return collection of guards names that exists in config/auth.php.
- * @param $model
+ * Return a collection of guard names suitable for the $model,
+ * as indicated by the presence of a $guard_name property or a guardName() method on the model.
+ *
+ * @param string|Model $model model class object or name
* @return Collection
*/
public static function getNames($model): Collection
{
+ $class = is_object($model) ? get_class($model) : $model;
+
if (is_object($model)) {
if (\method_exists($model, 'guardName')) {
$guardName = $model->guardName();
} else {
- $guardName = $model->guard_name ?? null;
+ $guardName = $model->getAttributeValue('guard_name');
}
}
if (! isset($guardName)) {
- $class = is_object($model) ? get_class($model) : $model;
-
$guardName = (new \ReflectionClass($class))->getDefaultProperties()['guard_name'] ?? null;
}
@@ -32,10 +34,27 @@ public static function getNames($model): Collection
return collect($guardName);
}
+ return self::getConfigAuthGuards($class);
+ }
+
+ /**
+ * Get list of relevant guards for the $class model based on config(auth) settings.
+ *
+ * Lookup flow:
+ * - get names of models for guards defined in auth.guards where a provider is set
+ * - filter for provider models matching the model $class being checked (important for Lumen)
+ * - keys() gives just the names of the matched guards
+ * - return collection of guard names
+ *
+ * @param string $class
+ * @return Collection
+ */
+ protected static function getConfigAuthGuards(string $class): Collection
+ {
return collect(config('auth.guards'))
->map(function ($guard) {
if (! isset($guard['provider'])) {
- return;
+ return null;
}
return config("auth.providers.{$guard['provider']}.model");
@@ -46,10 +65,23 @@ public static function getNames($model): Collection
->keys();
}
+ /**
+ * Lookup a guard name relevant for the $class model and the current user.
+ *
+ * @param string|Model $class model class object or name
+ * @return string guard name
+ */
public static function getDefaultName($class): string
{
$default = config('auth.defaults.guard');
- return static::getNames($class)->first() ?: $default;
+ $possible_guards = static::getNames($class);
+
+ // return current-detected auth.defaults.guard if it matches one of those that have been checked
+ if ($possible_guards->contains($default)) {
+ return $default;
+ }
+
+ return $possible_guards->first() ?: $default;
}
}
diff --git a/src/Middlewares/PermissionMiddleware.php b/src/Middlewares/PermissionMiddleware.php
index f1eca5a9a..73dec2ef8 100644
--- a/src/Middlewares/PermissionMiddleware.php
+++ b/src/Middlewares/PermissionMiddleware.php
@@ -9,7 +9,9 @@ class PermissionMiddleware
{
public function handle($request, Closure $next, $permission, $guard = null)
{
- if (app('auth')->guard($guard)->guest()) {
+ $authGuard = app('auth')->guard($guard);
+
+ if ($authGuard->guest()) {
throw UnauthorizedException::notLoggedIn();
}
@@ -18,7 +20,7 @@ public function handle($request, Closure $next, $permission, $guard = null)
: explode('|', $permission);
foreach ($permissions as $permission) {
- if (app('auth')->guard($guard)->user()->can($permission)) {
+ if ($authGuard->user()->can($permission)) {
return $next($request);
}
}
diff --git a/src/Middlewares/RoleMiddleware.php b/src/Middlewares/RoleMiddleware.php
index 2c679d5c9..34e91e241 100644
--- a/src/Middlewares/RoleMiddleware.php
+++ b/src/Middlewares/RoleMiddleware.php
@@ -10,7 +10,9 @@ class RoleMiddleware
{
public function handle($request, Closure $next, $role, $guard = null)
{
- if (Auth::guard($guard)->guest()) {
+ $authGuard = Auth::guard($guard);
+
+ if ($authGuard->guest()) {
throw UnauthorizedException::notLoggedIn();
}
@@ -18,7 +20,7 @@ public function handle($request, Closure $next, $role, $guard = null)
? $role
: explode('|', $role);
- if (! Auth::guard($guard)->user()->hasAnyRole($roles)) {
+ if (! $authGuard->user()->hasAnyRole($roles)) {
throw UnauthorizedException::forRoles($roles);
}
diff --git a/src/Middlewares/RoleOrPermissionMiddleware.php b/src/Middlewares/RoleOrPermissionMiddleware.php
index f8d5fa3b8..b9149f2f6 100644
--- a/src/Middlewares/RoleOrPermissionMiddleware.php
+++ b/src/Middlewares/RoleOrPermissionMiddleware.php
@@ -10,7 +10,8 @@ class RoleOrPermissionMiddleware
{
public function handle($request, Closure $next, $roleOrPermission, $guard = null)
{
- if (Auth::guard($guard)->guest()) {
+ $authGuard = Auth::guard($guard);
+ if ($authGuard->guest()) {
throw UnauthorizedException::notLoggedIn();
}
@@ -18,7 +19,7 @@ public function handle($request, Closure $next, $roleOrPermission, $guard = null
? $roleOrPermission
: explode('|', $roleOrPermission);
- if (! Auth::guard($guard)->user()->hasAnyRole($rolesOrPermissions) && ! Auth::guard($guard)->user()->hasAnyPermission($rolesOrPermissions)) {
+ if (! $authGuard->user()->hasAnyRole($rolesOrPermissions) && ! $authGuard->user()->hasAnyPermission($rolesOrPermissions)) {
throw UnauthorizedException::forRolesOrPermissions($rolesOrPermissions);
}
diff --git a/src/Models/Permission.php b/src/Models/Permission.php
index 054eb2905..1a044e694 100644
--- a/src/Models/Permission.php
+++ b/src/Models/Permission.php
@@ -2,9 +2,9 @@
namespace Spatie\Permission\Models;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
-use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission as PermissionContract;
use Spatie\Permission\Exceptions\PermissionAlreadyExists;
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
@@ -18,13 +18,15 @@ class Permission extends Model implements PermissionContract
use HasRoles;
use RefreshesPermissionCache;
- protected $guarded = ['id'];
+ protected $guarded = [];
public function __construct(array $attributes = [])
{
$attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard');
parent::__construct($attributes);
+
+ $this->guarded[] = $this->primaryKey;
}
public function getTable()
@@ -36,7 +38,7 @@ public static function create(array $attributes = [])
{
$attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class);
- $permission = static::getPermissions(['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']])->first();
+ $permission = static::getPermission(['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']]);
if ($permission) {
throw PermissionAlreadyExists::create($attributes['name'], $attributes['guard_name']);
@@ -53,8 +55,8 @@ public function roles(): BelongsToMany
return $this->belongsToMany(
config('permission.models.role'),
config('permission.table_names.role_has_permissions'),
- 'permission_id',
- 'role_id'
+ PermissionRegistrar::$pivotPermission,
+ PermissionRegistrar::$pivotRole
);
}
@@ -64,10 +66,10 @@ public function roles(): BelongsToMany
public function users(): BelongsToMany
{
return $this->morphedByMany(
- getModelForGuard($this->attributes['guard_name']),
+ getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')),
'model',
config('permission.table_names.model_has_permissions'),
- 'permission_id',
+ PermissionRegistrar::$pivotPermission,
config('permission.column_names.model_morph_key')
);
}
@@ -85,7 +87,7 @@ public function users(): BelongsToMany
public static function findByName(string $name, $guardName = null): PermissionContract
{
$guardName = $guardName ?? Guard::getDefaultName(static::class);
- $permission = static::getPermissions(['name' => $name, 'guard_name' => $guardName])->first();
+ $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]);
if (! $permission) {
throw PermissionDoesNotExist::create($name, $guardName);
}
@@ -106,7 +108,7 @@ public static function findByName(string $name, $guardName = null): PermissionCo
public static function findById(int $id, $guardName = null): PermissionContract
{
$guardName = $guardName ?? Guard::getDefaultName(static::class);
- $permission = static::getPermissions(['id' => $id, 'guard_name' => $guardName])->first();
+ $permission = static::getPermission([(new static())->getKeyName() => $id, 'guard_name' => $guardName]);
if (! $permission) {
throw PermissionDoesNotExist::withId($id, $guardName);
@@ -126,7 +128,7 @@ public static function findById(int $id, $guardName = null): PermissionContract
public static function findOrCreate(string $name, $guardName = null): PermissionContract
{
$guardName = $guardName ?? Guard::getDefaultName(static::class);
- $permission = static::getPermissions(['name' => $name, 'guard_name' => $guardName])->first();
+ $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]);
if (! $permission) {
return static::query()->create(['name' => $name, 'guard_name' => $guardName]);
@@ -137,11 +139,28 @@ public static function findOrCreate(string $name, $guardName = null): Permission
/**
* Get the current cached permissions.
+ *
+ * @param array $params
+ * @param bool $onlyOne
+ *
+ * @return \Illuminate\Database\Eloquent\Collection
*/
- protected static function getPermissions(array $params = []): Collection
+ protected static function getPermissions(array $params = [], bool $onlyOne = false): Collection
{
return app(PermissionRegistrar::class)
->setPermissionClass(static::class)
- ->getPermissions($params);
+ ->getPermissions($params, $onlyOne);
+ }
+
+ /**
+ * Get the current cached first permission.
+ *
+ * @param array $params
+ *
+ * @return \Spatie\Permission\Contracts\Permission
+ */
+ protected static function getPermission(array $params = []): ?PermissionContract
+ {
+ return static::getPermissions($params, true)->first();
}
}
diff --git a/src/Models/Role.php b/src/Models/Role.php
index 5fd3177fc..bb4173d82 100644
--- a/src/Models/Role.php
+++ b/src/Models/Role.php
@@ -9,6 +9,7 @@
use Spatie\Permission\Exceptions\RoleAlreadyExists;
use Spatie\Permission\Exceptions\RoleDoesNotExist;
use Spatie\Permission\Guard;
+use Spatie\Permission\PermissionRegistrar;
use Spatie\Permission\Traits\HasPermissions;
use Spatie\Permission\Traits\RefreshesPermissionCache;
@@ -17,13 +18,15 @@ class Role extends Model implements RoleContract
use HasPermissions;
use RefreshesPermissionCache;
- protected $guarded = ['id'];
+ protected $guarded = [];
public function __construct(array $attributes = [])
{
$attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard');
parent::__construct($attributes);
+
+ $this->guarded[] = $this->primaryKey;
}
public function getTable()
@@ -35,7 +38,15 @@ public static function create(array $attributes = [])
{
$attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class);
- if (static::where('name', $attributes['name'])->where('guard_name', $attributes['guard_name'])->first()) {
+ $params = ['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']];
+ if (PermissionRegistrar::$teams) {
+ if (array_key_exists(PermissionRegistrar::$teamsKey, $attributes)) {
+ $params[PermissionRegistrar::$teamsKey] = $attributes[PermissionRegistrar::$teamsKey];
+ } else {
+ $attributes[PermissionRegistrar::$teamsKey] = getPermissionsTeamId();
+ }
+ }
+ if (static::findByParam($params)) {
throw RoleAlreadyExists::create($attributes['name'], $attributes['guard_name']);
}
@@ -50,8 +61,8 @@ public function permissions(): BelongsToMany
return $this->belongsToMany(
config('permission.models.permission'),
config('permission.table_names.role_has_permissions'),
- 'role_id',
- 'permission_id'
+ PermissionRegistrar::$pivotRole,
+ PermissionRegistrar::$pivotPermission
);
}
@@ -61,10 +72,10 @@ public function permissions(): BelongsToMany
public function users(): BelongsToMany
{
return $this->morphedByMany(
- getModelForGuard($this->attributes['guard_name']),
+ getModelForGuard($this->attributes['guard_name'] ?? config('auth.defaults.guard')),
'model',
config('permission.table_names.model_has_roles'),
- 'role_id',
+ PermissionRegistrar::$pivotRole,
config('permission.column_names.model_morph_key')
);
}
@@ -83,7 +94,7 @@ public static function findByName(string $name, $guardName = null): RoleContract
{
$guardName = $guardName ?? Guard::getDefaultName(static::class);
- $role = static::where('name', $name)->where('guard_name', $guardName)->first();
+ $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]);
if (! $role) {
throw RoleDoesNotExist::named($name);
@@ -92,11 +103,19 @@ public static function findByName(string $name, $guardName = null): RoleContract
return $role;
}
+ /**
+ * Find a role by its id (and optionally guardName).
+ *
+ * @param int $id
+ * @param string|null $guardName
+ *
+ * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role
+ */
public static function findById(int $id, $guardName = null): RoleContract
{
$guardName = $guardName ?? Guard::getDefaultName(static::class);
- $role = static::where('id', $id)->where('guard_name', $guardName)->first();
+ $role = static::findByParam([(new static())->getKeyName() => $id, 'guard_name' => $guardName]);
if (! $role) {
throw RoleDoesNotExist::withId($id);
@@ -111,21 +130,40 @@ public static function findById(int $id, $guardName = null): RoleContract
* @param string $name
* @param string|null $guardName
*
- * @return \Spatie\Permission\Contracts\Role
+ * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role
*/
public static function findOrCreate(string $name, $guardName = null): RoleContract
{
$guardName = $guardName ?? Guard::getDefaultName(static::class);
- $role = static::where('name', $name)->where('guard_name', $guardName)->first();
+ $role = static::findByParam(['name' => $name, 'guard_name' => $guardName]);
if (! $role) {
- return static::query()->create(['name' => $name, 'guard_name' => $guardName]);
+ return static::query()->create(['name' => $name, 'guard_name' => $guardName] + (PermissionRegistrar::$teams ? [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : []));
}
return $role;
}
+ protected static function findByParam(array $params = [])
+ {
+ $query = static::query();
+
+ if (PermissionRegistrar::$teams) {
+ $query->where(function ($q) use ($params) {
+ $q->whereNull(PermissionRegistrar::$teamsKey)
+ ->orWhere(PermissionRegistrar::$teamsKey, $params[PermissionRegistrar::$teamsKey] ?? getPermissionsTeamId());
+ });
+ unset($params[PermissionRegistrar::$teamsKey]);
+ }
+
+ foreach ($params as $key => $value) {
+ $query->where($key, $value);
+ }
+
+ return $query->first();
+ }
+
/**
* Determine if the user may perform the given permission.
*
@@ -155,6 +193,6 @@ public function hasPermissionTo($permission): bool
throw GuardDoesNotMatch::create($permission->guard_name, $this->getGuardNames());
}
- return $this->permissions->contains('id', $permission->id);
+ return $this->permissions->contains($permission->getKeyName(), $permission->getKey());
}
}
diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php
index acad62f26..e886c4961 100644
--- a/src/PermissionRegistrar.php
+++ b/src/PermissionRegistrar.php
@@ -5,7 +5,9 @@
use Illuminate\Cache\CacheManager;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Access\Gate;
-use Illuminate\Support\Collection;
+use Illuminate\Contracts\Cache\Repository;
+use Illuminate\Contracts\Cache\Store;
+use Illuminate\Database\Eloquent\Collection;
use Spatie\Permission\Contracts\Permission;
use Spatie\Permission\Contracts\Role;
@@ -23,17 +25,38 @@ class PermissionRegistrar
/** @var string */
protected $roleClass;
- /** @var \Illuminate\Support\Collection */
+ /** @var \Illuminate\Database\Eloquent\Collection */
protected $permissions;
+ /** @var string */
+ public static $pivotRole;
+
+ /** @var string */
+ public static $pivotPermission;
+
/** @var \DateInterval|int */
public static $cacheExpirationTime;
+ /** @var bool */
+ public static $teams;
+
/** @var string */
- public static $cacheKey;
+ public static $teamsKey;
+
+ /** @var int|string */
+ protected $teamId = null;
/** @var string */
- public static $cacheModelKey;
+ public static $cacheKey;
+
+ /** @var array */
+ private $cachedRoles = [];
+
+ /** @var array */
+ private $alias = [];
+
+ /** @var array */
+ private $except = [];
/**
* PermissionRegistrar constructor.
@@ -49,19 +72,25 @@ public function __construct(CacheManager $cacheManager)
$this->initializeCache();
}
- protected function initializeCache()
+ public function initializeCache()
{
- self::$cacheExpirationTime = config('permission.cache.expiration_time', config('permission.cache_expiration_time'));
+ self::$cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours');
+
+ self::$teams = config('permission.teams', false);
+ self::$teamsKey = config('permission.column_names.team_foreign_key');
self::$cacheKey = config('permission.cache.key');
- self::$cacheModelKey = config('permission.cache.model_key');
+
+ self::$pivotRole = config('permission.column_names.role_pivot_key') ?: 'role_id';
+ self::$pivotPermission = config('permission.column_names.permission_pivot_key') ?: 'permission_id';
$this->cache = $this->getCacheStoreFromConfig();
}
- protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Repository
+ protected function getCacheStoreFromConfig(): Repository
{
- // the 'default' fallback here is from the permission.php config file, where 'default' means to use config(cache.default)
+ // the 'default' fallback here is from the permission.php config file,
+ // where 'default' means to use config(cache.default)
$cacheDriver = config('permission.cache.store', 'default');
// when 'default' is specified, no action is required since we already have the default instance
@@ -77,6 +106,28 @@ protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Reposi
return $this->cacheManager->store($cacheDriver);
}
+ /**
+ * Set the team id for teams/groups support, this id is used when querying permissions/roles
+ *
+ * @param int|string|\Illuminate\Database\Eloquent\Model $id
+ */
+ public function setPermissionsTeamId($id)
+ {
+ if ($id instanceof \Illuminate\Database\Eloquent\Model) {
+ $id = $id->getKey();
+ }
+ $this->teamId = $id;
+ }
+
+ /**
+ *
+ * @return int|string
+ */
+ public function getPermissionsTeamId()
+ {
+ return $this->teamId;
+ }
+
/**
* Register the permission check method on the gate.
* We resolve the Gate fresh here, for benefit of long-running instances.
@@ -114,27 +165,63 @@ public function clearClassPermissions()
$this->permissions = null;
}
+ /**
+ * Load permissions from cache
+ * This get cache and turns array into \Illuminate\Database\Eloquent\Collection
+ */
+ private function loadPermissions()
+ {
+ if ($this->permissions) {
+ return;
+ }
+
+ $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () {
+ return $this->getSerializedPermissionsForCache();
+ });
+
+ // fallback for old cache method, must be removed on next mayor version
+ if (! isset($this->permissions['alias'])) {
+ $this->forgetCachedPermissions();
+ $this->loadPermissions();
+
+ return;
+ }
+
+ $this->alias = $this->permissions['alias'];
+
+ $this->hydrateRolesCache();
+
+ $this->permissions = $this->getHydratedPermissionCollection();
+
+ $this->cachedRoles = $this->alias = $this->except = [];
+ }
+
/**
* Get the permissions based on the passed params.
*
* @param array $params
+ * @param bool $onlyOne
*
- * @return \Illuminate\Support\Collection
+ * @return \Illuminate\Database\Eloquent\Collection
*/
- public function getPermissions(array $params = []): Collection
- {
- if ($this->permissions === null) {
- $this->permissions = $this->cache->remember(self::$cacheKey, self::$cacheExpirationTime, function () {
- return $this->getPermissionClass()
- ->with('roles')
- ->get();
- });
- }
+ public function getPermissions(array $params = [], bool $onlyOne = false): Collection
+ {
+ $this->loadPermissions();
+
+ $method = $onlyOne ? 'first' : 'filter';
+
+ $permissions = $this->permissions->$method(static function ($permission) use ($params) {
+ foreach ($params as $attr => $value) {
+ if ($permission->getAttribute($attr) != $value) {
+ return false;
+ }
+ }
- $permissions = clone $this->permissions;
+ return true;
+ });
- foreach ($params as $attr => $value) {
- $permissions = $permissions->where($attr, $value);
+ if ($onlyOne) {
+ $permissions = new Collection($permissions ? [$permissions] : []);
}
return $permissions;
@@ -153,6 +240,8 @@ public function getPermissionClass(): Permission
public function setPermissionClass($permissionClass)
{
$this->permissionClass = $permissionClass;
+ config()->set('permission.models.permission', $permissionClass);
+ app()->bind(Permission::class, $permissionClass);
return $this;
}
@@ -167,13 +256,129 @@ public function getRoleClass(): Role
return app($this->roleClass);
}
+ public function setRoleClass($roleClass)
+ {
+ $this->roleClass = $roleClass;
+ config()->set('permission.models.role', $roleClass);
+ app()->bind(Role::class, $roleClass);
+
+ return $this;
+ }
+
+ public function getCacheRepository(): Repository
+ {
+ return $this->cache;
+ }
+
+ public function getCacheStore(): Store
+ {
+ return $this->cache->getStore();
+ }
+
/**
- * Get the instance of the Cache Store.
+ * Changes array keys with alias
*
- * @return \Illuminate\Contracts\Cache\Store
+ * @return array
*/
- public function getCacheStore(): \Illuminate\Contracts\Cache\Store
+ private function aliasedArray($model): array
{
- return $this->cache->getStore();
+ return collect(is_array($model) ? $model : $model->getAttributes())->except($this->except)
+ ->keyBy(function ($value, $key) {
+ return $this->alias[$key] ?? $key;
+ })->all();
+ }
+
+ /**
+ * Array for cache alias
+ */
+ private function aliasModelFields($newKeys = []): void
+ {
+ $i = 0;
+ $alphas = ! count($this->alias) ? range('a', 'h') : range('j', 'p');
+
+ foreach (array_keys($newKeys->getAttributes()) as $value) {
+ if (! isset($this->alias[$value])) {
+ $this->alias[$value] = $alphas[$i++] ?? $value;
+ }
+ }
+
+ $this->alias = array_diff_key($this->alias, array_flip($this->except));
+ }
+
+ /*
+ * Make the cache smaller using an array with only required fields
+ */
+ private function getSerializedPermissionsForCache()
+ {
+ $this->except = config('permission.cache.column_names_except', ['created_at','updated_at', 'deleted_at']);
+
+ $permissions = $this->getPermissionClass()->select()->with('roles')->get()
+ ->map(function ($permission) {
+ if (! $this->alias) {
+ $this->aliasModelFields($permission);
+ }
+
+ return $this->aliasedArray($permission) + $this->getSerializedRoleRelation($permission);
+ })->all();
+ $roles = array_values($this->cachedRoles);
+ $this->cachedRoles = [];
+
+ return ['alias' => array_flip($this->alias)] + compact('permissions', 'roles');
+ }
+
+ private function getSerializedRoleRelation($permission)
+ {
+ if (! $permission->roles->count()) {
+ return [];
+ }
+
+ if (! isset($this->alias['roles'])) {
+ $this->alias['roles'] = 'r';
+ $this->aliasModelFields($permission->roles[0]);
+ }
+
+ return [
+ 'r' => $permission->roles->map(function ($role) {
+ if (! isset($this->cachedRoles[$role->getKey()])) {
+ $this->cachedRoles[$role->getKey()] = $this->aliasedArray($role);
+ }
+
+ return $role->getKey();
+ })->all(),
+ ];
+ }
+
+ private function getHydratedPermissionCollection()
+ {
+ $permissionClass = $this->getPermissionClass();
+ $permissionInstance = new $permissionClass();
+
+ return Collection::make(
+ array_map(function ($item) use ($permissionInstance) {
+ return $permissionInstance
+ ->newFromBuilder($this->aliasedArray(array_diff_key($item, ['r' => 0])))
+ ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? []));
+ }, $this->permissions['permissions'])
+ );
+ }
+
+ private function getHydratedRoleCollection(array $roles)
+ {
+ return Collection::make(array_values(
+ array_intersect_key($this->cachedRoles, array_flip($roles))
+ ));
+ }
+
+ private function hydrateRolesCache()
+ {
+ $roleClass = $this->getRoleClass();
+ $roleInstance = new $roleClass();
+
+ array_map(function ($item) use ($roleInstance) {
+ $role = $roleInstance->newFromBuilder($this->aliasedArray($item));
+ $this->cachedRoles[$role->getKey()] = $role;
+ }, $this->permissions['roles']);
+
+ $this->permissions['roles'] = [];
}
}
diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php
index e45361b18..e408064a9 100644
--- a/src/PermissionServiceProvider.php
+++ b/src/PermissionServiceProvider.php
@@ -4,6 +4,7 @@
use Illuminate\Filesystem\Filesystem;
use Illuminate\Routing\Route;
+use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\Compilers\BladeCompiler;
@@ -12,31 +13,20 @@
class PermissionServiceProvider extends ServiceProvider
{
- public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesystem)
+ public function boot(PermissionRegistrar $permissionLoader)
{
- if (function_exists('config_path')) { // function not available and 'publish' not relevant in Lumen
- $this->publishes([
- __DIR__.'/../config/permission.php' => config_path('permission.php'),
- ], 'config');
-
- $this->publishes([
- __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName($filesystem),
- ], 'migrations');
- }
+ $this->offerPublishing();
$this->registerMacroHelpers();
- $this->commands([
- Commands\CacheReset::class,
- Commands\CreateRole::class,
- Commands\CreatePermission::class,
- Commands\Show::class,
- ]);
+ $this->registerCommands();
$this->registerModelBindings();
- $permissionLoader->clearClassPermissions();
- $permissionLoader->registerPermissions();
+ if ($this->app->config['permission.register_permission_check_method']) {
+ $permissionLoader->clearClassPermissions();
+ $permissionLoader->registerPermissions();
+ }
$this->app->singleton(PermissionRegistrar::class, function ($app) use ($permissionLoader) {
return $permissionLoader;
@@ -50,7 +40,36 @@ public function register()
'permission'
);
- $this->registerBladeExtensions();
+ $this->callAfterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) {
+ $this->registerBladeExtensions($bladeCompiler);
+ });
+ }
+
+ protected function offerPublishing()
+ {
+ if (! function_exists('config_path')) {
+ // function not available and 'publish' not relevant in Lumen
+ return;
+ }
+
+ $this->publishes([
+ __DIR__.'/../config/permission.php' => config_path('permission.php'),
+ ], 'config');
+
+ $this->publishes([
+ __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName('create_permission_tables.php'),
+ ], 'migrations');
+ }
+
+ protected function registerCommands()
+ {
+ $this->commands([
+ Commands\CacheReset::class,
+ Commands\CreateRole::class,
+ Commands\CreatePermission::class,
+ Commands\Show::class,
+ Commands\UpgradeForTeams::class,
+ ]);
}
protected function registerModelBindings()
@@ -65,58 +84,56 @@ protected function registerModelBindings()
$this->app->bind(RoleContract::class, $config['role']);
}
- protected function registerBladeExtensions()
+ public static function bladeMethodWrapper($method, $role, $guard = null)
+ {
+ return auth($guard)->check() && auth($guard)->user()->{$method}($role);
+ }
+
+ protected function registerBladeExtensions($bladeCompiler)
{
- $this->app->afterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) {
- $bladeCompiler->directive('role', function ($arguments) {
- list($role, $guard) = explode(',', $arguments.',');
-
- return "check() && auth({$guard})->user()->hasRole({$role})): ?>";
- });
- $bladeCompiler->directive('elserole', function ($arguments) {
- list($role, $guard) = explode(',', $arguments.',');
-
- return "check() && auth({$guard})->user()->hasRole({$role})): ?>";
- });
- $bladeCompiler->directive('endrole', function () {
- return '';
- });
-
- $bladeCompiler->directive('hasrole', function ($arguments) {
- list($role, $guard) = explode(',', $arguments.',');
-
- return "check() && auth({$guard})->user()->hasRole({$role})): ?>";
- });
- $bladeCompiler->directive('endhasrole', function () {
- return '';
- });
-
- $bladeCompiler->directive('hasanyrole', function ($arguments) {
- list($roles, $guard) = explode(',', $arguments.',');
-
- return "check() && auth({$guard})->user()->hasAnyRole({$roles})): ?>";
- });
- $bladeCompiler->directive('endhasanyrole', function () {
- return '';
- });
-
- $bladeCompiler->directive('hasallroles', function ($arguments) {
- list($roles, $guard) = explode(',', $arguments.',');
-
- return "check() && auth({$guard})->user()->hasAllRoles({$roles})): ?>";
- });
- $bladeCompiler->directive('endhasallroles', function () {
- return '';
- });
-
- $bladeCompiler->directive('unlessrole', function ($arguments) {
- list($role, $guard) = explode(',', $arguments.',');
-
- return "check() || ! auth({$guard})->user()->hasRole({$role})): ?>";
- });
- $bladeCompiler->directive('endunlessrole', function () {
- return '';
- });
+ $bladeCompiler->directive('role', function ($arguments) {
+ return "";
+ });
+ $bladeCompiler->directive('elserole', function ($arguments) {
+ return "";
+ });
+ $bladeCompiler->directive('endrole', function () {
+ return '';
+ });
+
+ $bladeCompiler->directive('hasrole', function ($arguments) {
+ return "";
+ });
+ $bladeCompiler->directive('endhasrole', function () {
+ return '';
+ });
+
+ $bladeCompiler->directive('hasanyrole', function ($arguments) {
+ return "";
+ });
+ $bladeCompiler->directive('endhasanyrole', function () {
+ return '';
+ });
+
+ $bladeCompiler->directive('hasallroles', function ($arguments) {
+ return "";
+ });
+ $bladeCompiler->directive('endhasallroles', function () {
+ return '';
+ });
+
+ $bladeCompiler->directive('unlessrole', function ($arguments) {
+ return "";
+ });
+ $bladeCompiler->directive('endunlessrole', function () {
+ return '';
+ });
+
+ $bladeCompiler->directive('hasexactroles', function ($arguments) {
+ return "";
+ });
+ $bladeCompiler->directive('endhasexactroles', function () {
+ return '';
});
}
@@ -127,11 +144,7 @@ protected function registerMacroHelpers()
}
Route::macro('role', function ($roles = []) {
- if (! is_array($roles)) {
- $roles = [$roles];
- }
-
- $roles = implode('|', $roles);
+ $roles = implode('|', Arr::wrap($roles));
$this->middleware("role:$roles");
@@ -139,11 +152,7 @@ protected function registerMacroHelpers()
});
Route::macro('permission', function ($permissions = []) {
- if (! is_array($permissions)) {
- $permissions = [$permissions];
- }
-
- $permissions = implode('|', $permissions);
+ $permissions = implode('|', Arr::wrap($permissions));
$this->middleware("permission:$permissions");
@@ -154,17 +163,19 @@ protected function registerMacroHelpers()
/**
* Returns existing migration file if found, else uses the current timestamp.
*
- * @param Filesystem $filesystem
* @return string
*/
- protected function getMigrationFileName(Filesystem $filesystem): string
+ protected function getMigrationFileName($migrationFileName): string
{
$timestamp = date('Y_m_d_His');
+ $filesystem = $this->app->make(Filesystem::class);
+
return Collection::make($this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR)
- ->flatMap(function ($path) use ($filesystem) {
- return $filesystem->glob($path.'*_create_permission_tables.php');
- })->push($this->app->databasePath()."/migrations/{$timestamp}_create_permission_tables.php")
+ ->flatMap(function ($path) use ($filesystem, $migrationFileName) {
+ return $filesystem->glob($path.'*_'.$migrationFileName);
+ })
+ ->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}")
->first();
}
}
diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php
index 532e815ce..462493b63 100644
--- a/src/Traits/HasPermissions.php
+++ b/src/Traits/HasPermissions.php
@@ -4,8 +4,10 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Spatie\Permission\Contracts\Permission;
+use Spatie\Permission\Contracts\Role;
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument;
@@ -15,6 +17,7 @@
trait HasPermissions
{
+ /** @var string */
private $permissionClass;
public static function bootHasPermissions()
@@ -42,20 +45,26 @@ public function getPermissionClass()
*/
public function permissions(): BelongsToMany
{
- return $this->morphToMany(
+ $relation = $this->morphToMany(
config('permission.models.permission'),
'model',
config('permission.table_names.model_has_permissions'),
config('permission.column_names.model_morph_key'),
- 'permission_id'
+ PermissionRegistrar::$pivotPermission
);
+
+ if (! PermissionRegistrar::$teams) {
+ return $relation;
+ }
+
+ return $relation->wherePivot(PermissionRegistrar::$teamsKey, getPermissionsTeamId());
}
/**
* Scope the model query to certain permissions only.
*
* @param \Illuminate\Database\Eloquent\Builder $query
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
*
* @return \Illuminate\Database\Eloquent\Builder
*/
@@ -69,20 +78,25 @@ public function scopePermission(Builder $query, $permissions): Builder
return $query->where(function (Builder $query) use ($permissions, $rolesWithPermissions) {
$query->whereHas('permissions', function (Builder $subQuery) use ($permissions) {
- $subQuery->whereIn(config('permission.table_names.permissions').'.id', \array_column($permissions, 'id'));
+ $permissionClass = $this->getPermissionClass();
+ $key = (new $permissionClass())->getKeyName();
+ $subQuery->whereIn(config('permission.table_names.permissions').".$key", \array_column($permissions, $key));
});
if (count($rolesWithPermissions) > 0) {
$query->orWhereHas('roles', function (Builder $subQuery) use ($rolesWithPermissions) {
- $subQuery->whereIn(config('permission.table_names.roles').'.id', \array_column($rolesWithPermissions, 'id'));
+ $roleClass = $this->getRoleClass();
+ $key = (new $roleClass())->getKeyName();
+ $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($rolesWithPermissions, $key));
});
}
});
}
/**
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
*
* @return array
+ * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist
*/
protected function convertToPermissionModels($permissions): array
{
@@ -90,32 +104,26 @@ protected function convertToPermissionModels($permissions): array
$permissions = $permissions->all();
}
- $permissions = is_array($permissions) ? $permissions : [$permissions];
-
return array_map(function ($permission) {
if ($permission instanceof Permission) {
return $permission;
}
+ $method = is_string($permission) ? 'findByName' : 'findById';
- return $this->getPermissionClass()->findByName($permission, $this->getDefaultGuardName());
- }, $permissions);
+ return $this->getPermissionClass()->{$method}($permission, $this->getDefaultGuardName());
+ }, Arr::wrap($permissions));
}
/**
- * Determine if the model may perform the given permission.
+ * Find a permission.
*
* @param string|int|\Spatie\Permission\Contracts\Permission $permission
- * @param string|null $guardName
*
- * @return bool
+ * @return \Spatie\Permission\Contracts\Permission
* @throws PermissionDoesNotExist
*/
- public function hasPermissionTo($permission, $guardName = null): bool
+ public function filterPermission($permission, $guardName = null)
{
- if (config('permission.enable_wildcard_permission', false)) {
- return $this->hasWildcardPermission($permission, $guardName);
- }
-
$permissionClass = $this->getPermissionClass();
if (is_string($permission)) {
@@ -133,9 +141,29 @@ public function hasPermissionTo($permission, $guardName = null): bool
}
if (! $permission instanceof Permission) {
- throw new PermissionDoesNotExist;
+ throw new PermissionDoesNotExist();
+ }
+
+ return $permission;
+ }
+
+ /**
+ * Determine if the model may perform the given permission.
+ *
+ * @param string|int|\Spatie\Permission\Contracts\Permission $permission
+ * @param string|null $guardName
+ *
+ * @return bool
+ * @throws PermissionDoesNotExist
+ */
+ public function hasPermissionTo($permission, $guardName = null): bool
+ {
+ if (config('permission.enable_wildcard_permission', false)) {
+ return $this->hasWildcardPermission($permission, $guardName);
}
+ $permission = $this->filterPermission($permission, $guardName);
+
return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission);
}
@@ -178,15 +206,6 @@ protected function hasWildcardPermission($permission, $guardName = null): bool
return false;
}
- /**
- * @deprecated since 2.35.0
- * @alias of hasPermissionTo()
- */
- public function hasUncachedPermissionTo($permission, $guardName = null): bool
- {
- return $this->hasPermissionTo($permission, $guardName);
- }
-
/**
* An alias to hasPermissionTo(), but avoids throwing an exception.
*
@@ -207,10 +226,9 @@ public function checkPermissionTo($permission, $guardName = null): bool
/**
* Determine if the model has any of the given permissions.
*
- * @param array ...$permissions
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions
*
* @return bool
- * @throws \Exception
*/
public function hasAnyPermission(...$permissions): bool
{
@@ -228,17 +246,16 @@ public function hasAnyPermission(...$permissions): bool
/**
* Determine if the model has all of the given permissions.
*
- * @param array ...$permissions
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions
*
* @return bool
- * @throws \Exception
*/
public function hasAllPermissions(...$permissions): bool
{
$permissions = collect($permissions)->flatten();
foreach ($permissions as $permission) {
- if (! $this->hasPermissionTo($permission)) {
+ if (! $this->checkPermissionTo($permission)) {
return false;
}
}
@@ -268,21 +285,9 @@ protected function hasPermissionViaRole(Permission $permission): bool
*/
public function hasDirectPermission($permission): bool
{
- $permissionClass = $this->getPermissionClass();
-
- if (is_string($permission)) {
- $permission = $permissionClass->findByName($permission, $this->getDefaultGuardName());
- }
+ $permission = $this->filterPermission($permission);
- if (is_int($permission)) {
- $permission = $permissionClass->findById($permission, $this->getDefaultGuardName());
- }
-
- if (! $permission instanceof Permission) {
- throw new PermissionDoesNotExist;
- }
-
- return $this->permissions->contains('id', $permission->id);
+ return $this->permissions->contains($permission->getKeyName(), $permission->getKey());
}
/**
@@ -304,7 +309,7 @@ public function getAllPermissions(): Collection
/** @var Collection $permissions */
$permissions = $this->permissions;
- if ($this->roles) {
+ if (method_exists($this, 'roles')) {
$permissions = $permissions->merge($this->getPermissionsViaRoles());
}
@@ -312,31 +317,45 @@ public function getAllPermissions(): Collection
}
/**
- * Grant the given permission(s) to a role.
+ * Returns permissions ids as array keys
*
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
*
- * @return $this
+ * @return array
*/
- public function givePermissionTo(...$permissions)
+ public function collectPermissions(...$permissions)
{
- $permissions = collect($permissions)
+ return collect($permissions)
->flatten()
- ->map(function ($permission) {
+ ->reduce(function ($array, $permission) {
if (empty($permission)) {
- return false;
+ return $array;
+ }
+
+ $permission = $this->getStoredPermission($permission);
+ if (! $permission instanceof Permission) {
+ return $array;
}
- return $this->getStoredPermission($permission);
- })
- ->filter(function ($permission) {
- return $permission instanceof Permission;
- })
- ->each(function ($permission) {
$this->ensureModelSharesGuard($permission);
- })
- ->map->id
- ->all();
+
+ $array[$permission->getKey()] = PermissionRegistrar::$teams && ! is_a($this, Role::class) ?
+ [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : [];
+
+ return $array;
+ }, []);
+ }
+
+ /**
+ * Grant the given permission(s) to a role.
+ *
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
+ *
+ * @return $this
+ */
+ public function givePermissionTo(...$permissions)
+ {
+ $permissions = $this->collectPermissions(...$permissions);
$model = $this->getModel();
@@ -348,18 +367,18 @@ public function givePermissionTo(...$permissions)
$class::saved(
function ($object) use ($permissions, $model) {
- static $modelLastFiredOn;
- if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) {
+ if ($model->getKey() != $object->getKey()) {
return;
}
- $object->permissions()->sync($permissions, false);
- $object->load('permissions');
- $modelLastFiredOn = $object;
+ $model->permissions()->sync($permissions, false);
+ $model->load('permissions');
}
);
}
- $this->forgetCachedPermissions();
+ if (is_a($this, get_class(app(PermissionRegistrar::class)->getRoleClass()))) {
+ $this->forgetCachedPermissions();
+ }
return $this;
}
@@ -367,7 +386,7 @@ function ($object) use ($permissions, $model) {
/**
* Remove all current permissions and set the given ones.
*
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
*
* @return $this
*/
@@ -379,7 +398,7 @@ public function syncPermissions(...$permissions)
}
/**
- * Revoke the given permission.
+ * Revoke the given permission(s).
*
* @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission
*
@@ -389,7 +408,9 @@ public function revokePermissionTo($permission)
{
$this->permissions()->detach($this->getStoredPermission($permission));
- $this->forgetCachedPermissions();
+ if (is_a($this, get_class(app(PermissionRegistrar::class)->getRoleClass()))) {
+ $this->forgetCachedPermissions();
+ }
$this->load('permissions');
@@ -402,7 +423,7 @@ public function getPermissionNames(): Collection
}
/**
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
*
* @return \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|\Illuminate\Support\Collection
*/
@@ -419,6 +440,10 @@ protected function getStoredPermission($permissions)
}
if (is_array($permissions)) {
+ $permissions = array_map(function ($permission) use ($permissionClass) {
+ return is_a($permission, get_class($permissionClass)) ? $permission->name : $permission;
+ }, $permissions);
+
return $permissionClass
->whereIn('name', $permissions)
->whereIn('guard_name', $this->getGuardNames())
@@ -460,7 +485,8 @@ public function forgetCachedPermissions()
/**
* Check if the model has All of the requested Direct permissions.
- * @param array ...$permissions
+ *
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions
* @return bool
*/
public function hasAllDirectPermissions(...$permissions): bool
@@ -478,7 +504,8 @@ public function hasAllDirectPermissions(...$permissions): bool
/**
* Check if the model has Any of the requested Direct permissions.
- * @param array ...$permissions
+ *
+ * @param string|int|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection ...$permissions
* @return bool
*/
public function hasAnyDirectPermission(...$permissions): bool
diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php
index 55376d4b4..6e4d7e5fe 100644
--- a/src/Traits/HasRoles.php
+++ b/src/Traits/HasRoles.php
@@ -4,7 +4,9 @@
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
+use Spatie\Permission\Contracts\Permission;
use Spatie\Permission\Contracts\Role;
use Spatie\Permission\PermissionRegistrar;
@@ -12,6 +14,7 @@ trait HasRoles
{
use HasPermissions;
+ /** @var string */
private $roleClass;
public static function bootHasRoles()
@@ -39,20 +42,30 @@ public function getRoleClass()
*/
public function roles(): BelongsToMany
{
- return $this->morphToMany(
+ $relation = $this->morphToMany(
config('permission.models.role'),
'model',
config('permission.table_names.model_has_roles'),
config('permission.column_names.model_morph_key'),
- 'role_id'
+ PermissionRegistrar::$pivotRole
);
+
+ if (! PermissionRegistrar::$teams) {
+ return $relation;
+ }
+
+ return $relation->wherePivot(PermissionRegistrar::$teamsKey, getPermissionsTeamId())
+ ->where(function ($q) {
+ $teamField = config('permission.table_names.roles').'.'.PermissionRegistrar::$teamsKey;
+ $q->whereNull($teamField)->orWhere($teamField, getPermissionsTeamId());
+ });
}
/**
* Scope the model query to certain roles only.
*
* @param \Illuminate\Database\Eloquent\Builder $query
- * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles
+ * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles
* @param string $guard
*
* @return \Illuminate\Database\Eloquent\Builder
@@ -63,30 +76,27 @@ public function scopeRole(Builder $query, $roles, $guard = null): Builder
$roles = $roles->all();
}
- if (! is_array($roles)) {
- $roles = [$roles];
- }
-
$roles = array_map(function ($role) use ($guard) {
if ($role instanceof Role) {
return $role;
}
$method = is_numeric($role) ? 'findById' : 'findByName';
- $guard = $guard ?: $this->getDefaultGuardName();
- return $this->getRoleClass()->{$method}($role, $guard);
- }, $roles);
+ return $this->getRoleClass()->{$method}($role, $guard ?: $this->getDefaultGuardName());
+ }, Arr::wrap($roles));
return $query->whereHas('roles', function (Builder $subQuery) use ($roles) {
- $subQuery->whereIn(config('permission.table_names.roles').'.id', \array_column($roles, 'id'));
+ $roleClass = $this->getRoleClass();
+ $key = (new $roleClass())->getKeyName();
+ $subQuery->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key));
});
}
/**
* Assign the given role to the model.
*
- * @param array|string|\Spatie\Permission\Contracts\Role ...$roles
+ * @param array|string|int|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection ...$roles
*
* @return $this
*/
@@ -94,21 +104,23 @@ public function assignRole(...$roles)
{
$roles = collect($roles)
->flatten()
- ->map(function ($role) {
+ ->reduce(function ($array, $role) {
if (empty($role)) {
- return false;
+ return $array;
+ }
+
+ $role = $this->getStoredRole($role);
+ if (! $role instanceof Role) {
+ return $array;
}
- return $this->getStoredRole($role);
- })
- ->filter(function ($role) {
- return $role instanceof Role;
- })
- ->each(function ($role) {
$this->ensureModelSharesGuard($role);
- })
- ->map->id
- ->all();
+
+ $array[$role->getKey()] = PermissionRegistrar::$teams && ! is_a($this, Permission::class) ?
+ [PermissionRegistrar::$teamsKey => getPermissionsTeamId()] : [];
+
+ return $array;
+ }, []);
$model = $this->getModel();
@@ -120,18 +132,18 @@ public function assignRole(...$roles)
$class::saved(
function ($object) use ($roles, $model) {
- static $modelLastFiredOn;
- if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) {
+ if ($model->getKey() != $object->getKey()) {
return;
}
- $object->roles()->sync($roles, false);
- $object->load('roles');
- $modelLastFiredOn = $object;
+ $model->roles()->sync($roles, false);
+ $model->load('roles');
}
);
}
- $this->forgetCachedPermissions();
+ if (is_a($this, get_class($this->getPermissionClass()))) {
+ $this->forgetCachedPermissions();
+ }
return $this;
}
@@ -139,7 +151,7 @@ function ($object) use ($roles, $model) {
/**
* Revoke the given role from the model.
*
- * @param string|\Spatie\Permission\Contracts\Role $role
+ * @param string|int|\Spatie\Permission\Contracts\Role $role
*/
public function removeRole($role)
{
@@ -147,7 +159,9 @@ public function removeRole($role)
$this->load('roles');
- $this->forgetCachedPermissions();
+ if (is_a($this, get_class($this->getPermissionClass()))) {
+ $this->forgetCachedPermissions();
+ }
return $this;
}
@@ -155,7 +169,7 @@ public function removeRole($role)
/**
* Remove all current roles and set the given ones.
*
- * @param array|\Spatie\Permission\Contracts\Role|string ...$roles
+ * @param array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection|string|int ...$roles
*
* @return $this
*/
@@ -175,6 +189,8 @@ public function syncRoles(...$roles)
*/
public function hasRole($roles, string $guard = null): bool
{
+ $this->loadMissing('roles');
+
if (is_string($roles) && false !== strpos($roles, '|')) {
$roles = $this->convertPipeToArray($roles);
}
@@ -186,13 +202,16 @@ public function hasRole($roles, string $guard = null): bool
}
if (is_int($roles)) {
+ $roleClass = $this->getRoleClass();
+ $key = (new $roleClass())->getKeyName();
+
return $guard
- ? $this->roles->where('guard_name', $guard)->contains('id', $roles)
- : $this->roles->contains('id', $roles);
+ ? $this->roles->where('guard_name', $guard)->contains($key, $roles)
+ : $this->roles->contains($key, $roles);
}
if ($roles instanceof Role) {
- return $this->roles->contains('id', $roles->id);
+ return $this->roles->contains($roles->getKeyName(), $roles->getKey());
}
if (is_array($roles)) {
@@ -231,6 +250,8 @@ public function hasAnyRole(...$roles): bool
*/
public function hasAllRoles($roles, string $guard = null): bool
{
+ $this->loadMissing('roles');
+
if (is_string($roles) && false !== strpos($roles, '|')) {
$roles = $this->convertPipeToArray($roles);
}
@@ -242,7 +263,7 @@ public function hasAllRoles($roles, string $guard = null): bool
}
if ($roles instanceof Role) {
- return $this->roles->contains('id', $roles->id);
+ return $this->roles->contains($roles->getKeyName(), $roles->getKey());
}
$roles = collect()->make($roles)->map(function ($role) {
@@ -256,6 +277,36 @@ public function hasAllRoles($roles, string $guard = null): bool
) == $roles;
}
+ /**
+ * Determine if the model has exactly all of the given role(s).
+ *
+ * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles
+ * @param string|null $guard
+ * @return bool
+ */
+ public function hasExactRoles($roles, string $guard = null): bool
+ {
+ $this->loadMissing('roles');
+
+ if (is_string($roles) && false !== strpos($roles, '|')) {
+ $roles = $this->convertPipeToArray($roles);
+ }
+
+ if (is_string($roles)) {
+ $roles = [$roles];
+ }
+
+ if ($roles instanceof Role) {
+ $roles = [$roles->name];
+ }
+
+ $roles = collect()->make($roles)->map(function ($role) {
+ return $role instanceof Role ? $role->name : $role;
+ });
+
+ return $this->roles->count() == $roles->count() && $this->hasAllRoles($roles, $guard);
+ }
+
/**
* Return all permissions directly coupled to the model.
*/
@@ -266,6 +317,8 @@ public function getDirectPermissions(): Collection
public function getRoleNames(): Collection
{
+ $this->loadMissing('roles');
+
return $this->roles->pluck('name');
}
diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php
index 22d142ba0..73797a854 100644
--- a/src/WildcardPermission.php
+++ b/src/WildcardPermission.php
@@ -8,13 +8,13 @@
class WildcardPermission
{
/** @var string */
- const WILDCARD_TOKEN = '*';
+ public const WILDCARD_TOKEN = '*';
/** @var string */
- const PART_DELIMITER = '.';
+ public const PART_DELIMITER = '.';
/** @var string */
- const SUBPART_DELIMITER = ',';
+ public const SUBPART_DELIMITER = ',';
/** @var string */
protected $permission;
@@ -41,18 +41,19 @@ public function __construct(string $permission)
public function implies($permission): bool
{
if (is_string($permission)) {
- $permission = new self($permission);
+ $permission = new static($permission);
}
$otherParts = $permission->getParts();
$i = 0;
+ $partsCount = $this->getParts()->count();
foreach ($otherParts as $otherPart) {
- if ($this->getParts()->count() - 1 < $i) {
+ if ($partsCount - 1 < $i) {
return true;
}
- if (! $this->parts->get($i)->contains(self::WILDCARD_TOKEN)
+ if (! $this->parts->get($i)->contains(static::WILDCARD_TOKEN)
&& ! $this->containsAll($this->parts->get($i), $otherPart)) {
return false;
}
@@ -60,8 +61,8 @@ public function implies($permission): bool
$i++;
}
- for ($i; $i < $this->parts->count(); $i++) {
- if (! $this->parts->get($i)->contains(self::WILDCARD_TOKEN)) {
+ for ($i; $i < $partsCount; $i++) {
+ if (! $this->parts->get($i)->contains(static::WILDCARD_TOKEN)) {
return false;
}
}
@@ -105,10 +106,10 @@ protected function setParts(): void
throw WildcardPermissionNotProperlyFormatted::create($this->permission);
}
- $parts = collect(explode(self::PART_DELIMITER, $this->permission));
+ $parts = collect(explode(static::PART_DELIMITER, $this->permission));
$parts->each(function ($item, $key) {
- $subParts = collect(explode(self::SUBPART_DELIMITER, $item));
+ $subParts = collect(explode(static::SUBPART_DELIMITER, $item));
if ($subParts->isEmpty() || $subParts->contains('')) {
throw WildcardPermissionNotProperlyFormatted::create($this->permission);
diff --git a/src/helpers.php b/src/helpers.php
index 7f79e5764..25116d0ff 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -18,3 +18,24 @@ function getModelForGuard(string $guard)
})->get($guard);
}
}
+
+if (! function_exists('setPermissionsTeamId')) {
+ /**
+ * @param int|string|\Illuminate\Database\Eloquent\Model $id
+ *
+ */
+ function setPermissionsTeamId($id)
+ {
+ app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId($id);
+ }
+}
+
+if (! function_exists('getPermissionsTeamId')) {
+ /**
+ * @return int|string
+ */
+ function getPermissionsTeamId()
+ {
+ return app(\Spatie\Permission\PermissionRegistrar::class)->getPermissionsTeamId();
+ }
+}
diff --git a/tests/Admin.php b/tests/Admin.php
index ede545b9e..b942a15d4 100644
--- a/tests/Admin.php
+++ b/tests/Admin.php
@@ -11,7 +11,9 @@
class Admin extends Model implements AuthorizableContract, AuthenticatableContract
{
- use HasRoles, Authorizable, Authenticatable;
+ use HasRoles;
+ use Authorizable;
+ use Authenticatable;
protected $fillable = ['email'];
diff --git a/tests/BladeTest.php b/tests/BladeTest.php
index 8558f5092..21d66437f 100644
--- a/tests/BladeTest.php
+++ b/tests/BladeTest.php
@@ -31,9 +31,9 @@ public function all_blade_directives_will_evaluate_false_when_there_is_nobody_lo
$this->assertEquals('does not have permission', $this->renderView('can', ['permission' => $permission]));
$this->assertEquals('does not have role', $this->renderView('role', compact('role', 'elserole')));
$this->assertEquals('does not have role', $this->renderView('hasRole', compact('role', 'elserole')));
- $this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', $roles));
+ $this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', compact('roles')));
$this->assertEquals('does not have all of the given roles', $this->renderView('hasAllRoles', ['roles' => implode('|', $roles)]));
- $this->assertEquals('does not have any of the given roles', $this->renderView('hasAnyRole', $roles));
+ $this->assertEquals('does not have any of the given roles', $this->renderView('hasAnyRole', compact('roles')));
$this->assertEquals('does not have any of the given roles', $this->renderView('hasAnyRole', ['roles' => implode('|', $roles)]));
}
@@ -262,6 +262,33 @@ public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in
$this->assertEquals('does not have all of the given roles', $this->renderView('guardHasAllRolesPipe', compact('guard')));
}
+ /** @test */
+ public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles_in_array()
+ {
+ $guard = 'admin';
+
+ $admin = $this->getSuperAdmin();
+
+ $admin->assignRole('moderator');
+
+ auth('admin')->setUser($admin);
+
+ $this->assertEquals('does have all of the given roles', $this->renderView('guardHasAllRolesArray', compact('guard')));
+ }
+
+ /** @test */
+ public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in_user_doesnt_have_all_required_roles_in_array()
+ {
+ $guard = '';
+ $user = $this->getMember();
+
+ $user->assignRole('writer');
+
+ auth()->setUser($user);
+
+ $this->assertEquals('does not have all of the given roles', $this->renderView('guardHasAllRolesArray', compact('guard')));
+ }
+
protected function getWriter()
{
$this->testUser->assignRole('writer');
diff --git a/tests/CacheTest.php b/tests/CacheTest.php
index 1a1caa0ff..62123f792 100644
--- a/tests/CacheTest.php
+++ b/tests/CacheTest.php
@@ -104,7 +104,23 @@ public function it_flushes_the_cache_when_updating_a_role()
}
/** @test */
- public function it_flushes_the_cache_when_removing_a_role_from_a_user()
+ public function removing_a_permission_from_a_user_should_not_flush_the_cache()
+ {
+ $this->testUser->givePermissionTo('edit-articles');
+
+ $this->registrar->getPermissions();
+
+ $this->testUser->revokePermissionTo('edit-articles');
+
+ $this->resetQueryCount();
+
+ $this->registrar->getPermissions();
+
+ $this->assertQueryCount(0);
+ }
+
+ /** @test */
+ public function removing_a_role_from_a_user_should_not_flush_the_cache()
{
$this->testUser->assignRole('testRole');
@@ -116,6 +132,34 @@ public function it_flushes_the_cache_when_removing_a_role_from_a_user()
$this->registrar->getPermissions();
+ $this->assertQueryCount(0);
+ }
+
+ /** @test */
+ public function it_flushes_the_cache_when_removing_a_role_from_a_permission()
+ {
+ $this->testUserPermission->assignRole('testRole');
+
+ $this->registrar->getPermissions();
+
+ $this->testUserPermission->removeRole('testRole');
+
+ $this->resetQueryCount();
+
+ $this->registrar->getPermissions();
+
+ $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count);
+ }
+
+ /** @test */
+ public function it_flushes_the_cache_when_assign_a_permission_to_a_role()
+ {
+ $this->testUserRole->givePermissionTo('edit-articles');
+
+ $this->resetQueryCount();
+
+ $this->registrar->getPermissions();
+
$this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count);
}
@@ -203,6 +247,17 @@ public function get_all_permissions_should_use_the_cache()
$this->assertQueryCount(2);
}
+ /** @test */
+ public function get_all_permissions_should_not_over_hydrate_roles()
+ {
+ $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']);
+ $permissions = $this->registrar->getPermissions();
+ $roles = $permissions->flatMap->roles;
+
+ // Should have same object reference
+ $this->assertSame($roles[0], $roles[1]);
+ }
+
/** @test */
public function it_can_reset_the_cache_with_artisan_command()
{
diff --git a/tests/CommandTest.php b/tests/CommandTest.php
index b900b8162..be1541f88 100644
--- a/tests/CommandTest.php
+++ b/tests/CommandTest.php
@@ -94,11 +94,16 @@ public function it_can_show_permission_tables()
$this->assertTrue(strpos($output, 'Guard: web') !== false);
$this->assertTrue(strpos($output, 'Guard: admin') !== false);
- // | | testRole | testRole2 |
- $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output);
+ if (method_exists($this, 'assertMatchesRegularExpression')) {
+ // | | testRole | testRole2 |
+ $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output);
- // | edit-articles | · | · |
- $this->assertRegExp('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output);
+ // | edit-articles | · | · |
+ $this->assertMatchesRegularExpression('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output);
+ } else { // phpUnit 9/8
+ $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|/', $output);
+ $this->assertRegExp('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output);
+ }
Role::findByName('testRole')->givePermissionTo('edit-articles');
$this->reloadPermissions();
@@ -108,7 +113,11 @@ public function it_can_show_permission_tables()
$output = Artisan::output();
// | edit-articles | · | · |
- $this->assertRegExp('/\|\s+edit-articles\s+\|\s+✔\s+\|\s+·\s+\|/', $output);
+ if (method_exists($this, 'assertMatchesRegularExpression')) {
+ $this->assertMatchesRegularExpression('/\|\s+edit-articles\s+\|\s+✔\s+\|\s+·\s+\|/', $output);
+ } else {
+ $this->assertRegExp('/\|\s+edit-articles\s+\|\s+✔\s+\|\s+·\s+\|/', $output);
+ }
}
/** @test */
@@ -121,4 +130,54 @@ public function it_can_show_permissions_for_guard()
$this->assertTrue(strpos($output, 'Guard: web') !== false);
$this->assertTrue(strpos($output, 'Guard: admin') === false);
}
+
+ /** @test */
+ public function it_can_setup_teams_upgrade()
+ {
+ config()->set('permission.teams', true);
+
+ $this->artisan('permission:setup-teams')
+ ->expectsQuestion('Proceed with the migration creation?', 'yes')
+ ->assertExitCode(0);
+
+ $matchingFiles = glob(database_path('migrations/*_add_teams_fields.php'));
+ $this->assertTrue(count($matchingFiles) > 0);
+
+ include_once $matchingFiles[count($matchingFiles) - 1];
+ (new \AddTeamsFields())->up();
+ (new \AddTeamsFields())->up(); //test upgrade teams migration fresh
+
+ Role::create(['name' => 'new-role', 'team_test_id' => 1]);
+ $role = Role::where('name', 'new-role')->first();
+ $this->assertNotNull($role);
+ $this->assertSame(1, (int) $role->team_test_id);
+
+ // remove migration
+ foreach ($matchingFiles as $file) {
+ unlink($file);
+ }
+ }
+
+ /** @test */
+ public function it_can_show_roles_by_teams()
+ {
+ config()->set('permission.teams', true);
+ app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache();
+
+ Role::create(['name' => 'testRoleTeam', 'team_test_id' => 1]);
+ Role::create(['name' => 'testRoleTeam', 'team_test_id' => 2]); // same name different team
+ Artisan::call('permission:show');
+
+ $output = Artisan::output();
+
+ // | | Team ID: NULL | Team ID: 1 | Team ID: 2 |
+ // | | testRole | testRole2 | testRoleTeam | testRoleTeam |
+ if (method_exists($this, 'assertMatchesRegularExpression')) {
+ $this->assertMatchesRegularExpression('/\|\s+\|\s+Team ID: NULL\s+\|\s+Team ID: 1\s+\|\s+Team ID: 2\s+\|/', $output);
+ $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|\s+testRoleTeam\s+\|\s+testRoleTeam\s+\|/', $output);
+ } else { // phpUnit 9/8
+ $this->assertRegExp('/\|\s+\|\s+Team ID: NULL\s+\|\s+Team ID: 1\s+\|\s+Team ID: 2\s+\|/', $output);
+ $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole2\s+\|\s+testRoleTeam\s+\|\s+testRoleTeam\s+\|/', $output);
+ }
+ }
}
diff --git a/tests/CustomGateTest.php b/tests/CustomGateTest.php
new file mode 100644
index 000000000..74b963105
--- /dev/null
+++ b/tests/CustomGateTest.php
@@ -0,0 +1,35 @@
+set('permission.register_permission_check_method', false);
+ }
+
+ /** @test */
+ public function it_doesnt_register_the_method_for_checking_permissions_on_the_gate()
+ {
+ $this->testUser->givePermissionTo('edit-articles');
+
+ $this->assertEmpty(app(Gate::class)->abilities());
+ $this->assertFalse($this->testUser->can('edit-articles'));
+ }
+
+ /** @test */
+ public function it_can_authorize_using_custom_method_for_checking_permissions()
+ {
+ app(Gate::class)->define('edit-articles', function () {
+ return true;
+ });
+
+ $this->assertArrayHasKey('edit-articles', app(Gate::class)->abilities());
+ $this->assertTrue($this->testUser->can('edit-articles'));
+ }
+}
diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php
index bd718d0ba..40151b67e 100644
--- a/tests/HasPermissionsTest.php
+++ b/tests/HasPermissionsTest.php
@@ -2,6 +2,7 @@
namespace Spatie\Permission\Test;
+use DB;
use Spatie\Permission\Contracts\Permission;
use Spatie\Permission\Contracts\Role;
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
@@ -61,8 +62,24 @@ public function it_can_scope_users_using_a_string()
$scopedUsers1 = User::permission('edit-articles')->get();
$scopedUsers2 = User::permission(['edit-news'])->get();
- $this->assertEquals($scopedUsers1->count(), 2);
- $this->assertEquals($scopedUsers2->count(), 1);
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ }
+
+ /** @test */
+ public function it_can_scope_users_using_a_int()
+ {
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user1->givePermissionTo([1, 2]);
+ $this->testUserRole->givePermissionTo(1);
+ $user2->assignRole('testRole');
+
+ $scopedUsers1 = User::permission(1)->get();
+ $scopedUsers2 = User::permission([2])->get();
+
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
}
/** @test */
@@ -77,8 +94,8 @@ public function it_can_scope_users_using_an_array()
$scopedUsers1 = User::permission(['edit-articles', 'edit-news'])->get();
$scopedUsers2 = User::permission(['edit-news'])->get();
- $this->assertEquals($scopedUsers1->count(), 2);
- $this->assertEquals($scopedUsers2->count(), 1);
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
}
/** @test */
@@ -93,8 +110,8 @@ public function it_can_scope_users_using_a_collection()
$scopedUsers1 = User::permission(collect(['edit-articles', 'edit-news']))->get();
$scopedUsers2 = User::permission(collect(['edit-news']))->get();
- $this->assertEquals($scopedUsers1->count(), 2);
- $this->assertEquals($scopedUsers2->count(), 1);
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
}
/** @test */
@@ -107,9 +124,9 @@ public function it_can_scope_users_using_an_object()
$scopedUsers2 = User::permission([$this->testUserPermission])->get();
$scopedUsers3 = User::permission(collect([$this->testUserPermission]))->get();
- $this->assertEquals($scopedUsers1->count(), 1);
- $this->assertEquals($scopedUsers2->count(), 1);
- $this->assertEquals($scopedUsers3->count(), 1);
+ $this->assertEquals(1, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ $this->assertEquals(1, $scopedUsers3->count());
}
/** @test */
@@ -123,7 +140,7 @@ public function it_can_scope_users_without_permissions_only_role()
$scopedUsers = User::permission('edit-articles')->get();
- $this->assertEquals($scopedUsers->count(), 2);
+ $this->assertEquals(2, $scopedUsers->count());
}
/** @test */
@@ -136,7 +153,7 @@ public function it_can_scope_users_without_permissions_only_permission()
$scopedUsers = User::permission('edit-news')->get();
- $this->assertEquals($scopedUsers->count(), 2);
+ $this->assertEquals(2, $scopedUsers->count());
}
/** @test */
@@ -223,6 +240,34 @@ public function it_can_give_and_revoke_multiple_permissions()
$this->assertEquals(0, $this->testUserRole->permissions()->count());
}
+ /** @test */
+ public function it_can_give_and_revoke_permissions_models_array()
+ {
+ $models = [app(Permission::class)::where('name', 'edit-articles')->first(), app(Permission::class)::where('name', 'edit-news')->first()];
+
+ $this->testUserRole->givePermissionTo($models);
+
+ $this->assertEquals(2, $this->testUserRole->permissions()->count());
+
+ $this->testUserRole->revokePermissionTo($models);
+
+ $this->assertEquals(0, $this->testUserRole->permissions()->count());
+ }
+
+ /** @test */
+ public function it_can_give_and_revoke_permissions_models_collection()
+ {
+ $models = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])->get();
+
+ $this->testUserRole->givePermissionTo($models);
+
+ $this->assertEquals(2, $this->testUserRole->permissions()->count());
+
+ $this->testUserRole->revokePermissionTo($models);
+
+ $this->assertEquals(0, $this->testUserRole->permissions()->count());
+ }
+
/** @test */
public function it_can_determine_that_the_user_does_not_have_a_permission()
{
@@ -365,7 +410,7 @@ public function it_can_list_all_the_permissions_via_roles_of_user()
$this->assertEquals(
collect(['edit-articles', 'edit-news']),
- $this->testUser->getPermissionsViaRoles()->pluck('name')
+ $this->testUser->getPermissionsViaRoles()->pluck('name')->sort()->values()
);
}
@@ -402,7 +447,7 @@ public function it_can_sync_multiple_permissions_by_id()
{
$this->testUser->givePermissionTo('edit-news');
- $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-blog'])->pluck('id');
+ $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-blog'])->pluck($this->testUserPermission->getKeyName());
$this->testUser->syncPermissions($ids);
@@ -418,7 +463,7 @@ public function sync_permission_ignores_null_inputs()
{
$this->testUser->givePermissionTo('edit-news');
- $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-blog'])->pluck('id');
+ $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-blog'])->pluck($this->testUserPermission->getKeyName());
$ids->push(null);
@@ -474,10 +519,17 @@ public function calling_givePermissionTo_before_saving_object_doesnt_interfere_w
$user2 = new User(['email' => 'test2@user.com']);
$user2->givePermissionTo('edit-articles');
+
+ DB::enableQueryLog();
$user2->save();
+ DB::disableQueryLog();
+
+ $this->assertTrue($user->fresh()->hasPermissionTo('edit-news'));
+ $this->assertFalse($user->fresh()->hasPermissionTo('edit-articles'));
$this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles'));
$this->assertFalse($user2->fresh()->hasPermissionTo('edit-news'));
+ $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync
}
/** @test */
@@ -489,10 +541,17 @@ public function calling_syncPermissions_before_saving_object_doesnt_interfere_wi
$user2 = new User(['email' => 'test2@user.com']);
$user2->syncPermissions('edit-articles');
+
+ DB::enableQueryLog();
$user2->save();
+ DB::disableQueryLog();
+
+ $this->assertTrue($user->fresh()->hasPermissionTo('edit-news'));
+ $this->assertFalse($user->fresh()->hasPermissionTo('edit-articles'));
$this->assertTrue($user2->fresh()->hasPermissionTo('edit-articles'));
$this->assertFalse($user2->fresh()->hasPermissionTo('edit-news'));
+ $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync
}
/** @test */
@@ -500,8 +559,8 @@ public function it_can_retrieve_permission_names()
{
$this->testUser->givePermissionTo('edit-news', 'edit-articles');
$this->assertEquals(
- collect(['edit-news', 'edit-articles']),
- $this->testUser->getPermissionNames()
+ collect(['edit-articles', 'edit-news']),
+ $this->testUser->getPermissionNames()->sort()->values()
);
}
@@ -523,4 +582,40 @@ public function it_can_check_if_there_is_any_of_the_direct_permissions_given()
$this->assertTrue($this->testUser->hasAnyDirectPermission('edit-news', 'edit-blog'));
$this->assertFalse($this->testUser->hasAnyDirectPermission('edit-blog', 'Edit News', ['Edit News']));
}
+
+ /** @test */
+ public function it_can_check_permission_based_on_logged_in_user_guard()
+ {
+ $this->testUser->givePermissionTo(app(Permission::class)::create([
+ 'name' => 'do_that',
+ 'guard_name' => 'api',
+ ]));
+ $response = $this->actingAs($this->testUser, 'api')
+ ->json('GET', '/check-api-guard-permission');
+ $response->assertJson([
+ 'status' => true,
+ ]);
+ }
+
+ /** @test */
+ public function it_can_reject_permission_based_on_logged_in_user_guard()
+ {
+ $unassignedPermission = app(Permission::class)::create([
+ 'name' => 'do_that',
+ 'guard_name' => 'api',
+ ]);
+
+ $assignedPermission = app(Permission::class)::create([
+ 'name' => 'do_that',
+ 'guard_name' => 'web',
+ ]);
+
+ $this->testUser->givePermissionTo($assignedPermission);
+ $response = $this->withExceptionHandling()
+ ->actingAs($this->testUser, 'api')
+ ->json('GET', '/check-api-guard-permission');
+ $response->assertJson([
+ 'status' => false,
+ ]);
+ }
}
diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php
new file mode 100644
index 000000000..6b9fa34ca
--- /dev/null
+++ b/tests/HasPermissionsWithCustomModelsTest.php
@@ -0,0 +1,39 @@
+assertSame(get_class($this->testUserPermission), Permission::class);
+ }
+
+ /** @test */
+ public function it_can_use_custom_fields_from_cache()
+ {
+ DB::connection()->getSchemaBuilder()->table(config('permission.table_names.roles'), function ($table) {
+ $table->string('type')->default('R');
+ });
+ DB::connection()->getSchemaBuilder()->table(config('permission.table_names.permissions'), function ($table) {
+ $table->string('type')->default('P');
+ });
+
+ $this->testUserRole->givePermissionTo($this->testUserPermission);
+ app(PermissionRegistrar::class)->getPermissions();
+
+ DB::enableQueryLog();
+ $this->assertSame('P', Permission::findByName('edit-articles')->type);
+ $this->assertSame('R', Permission::findByName('edit-articles')->roles[0]->type);
+ DB::disableQueryLog();
+
+ $this->assertSame(0, count(DB::getQueryLog()));
+ }
+}
diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php
index 902d520dd..0031b58da 100644
--- a/tests/HasRolesTest.php
+++ b/tests/HasRolesTest.php
@@ -2,6 +2,7 @@
namespace Spatie\Permission\Test;
+use DB;
use Spatie\Permission\Contracts\Role;
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
use Spatie\Permission\Exceptions\RoleDoesNotExist;
@@ -22,13 +23,13 @@ public function it_can_determine_that_the_user_does_not_have_a_role()
$this->assertTrue($this->testUser->hasRole($role->name));
$this->assertTrue($this->testUser->hasRole($role->name, $role->guard_name));
$this->assertTrue($this->testUser->hasRole([$role->name, 'fakeRole'], $role->guard_name));
- $this->assertTrue($this->testUser->hasRole($role->id, $role->guard_name));
- $this->assertTrue($this->testUser->hasRole([$role->id, 'fakeRole'], $role->guard_name));
+ $this->assertTrue($this->testUser->hasRole($role->getKey(), $role->guard_name));
+ $this->assertTrue($this->testUser->hasRole([$role->getKey(), 'fakeRole'], $role->guard_name));
$this->assertFalse($this->testUser->hasRole($role->name, 'fakeGuard'));
$this->assertFalse($this->testUser->hasRole([$role->name, 'fakeRole'], 'fakeGuard'));
- $this->assertFalse($this->testUser->hasRole($role->id, 'fakeGuard'));
- $this->assertFalse($this->testUser->hasRole([$role->id, 'fakeRole'], 'fakeGuard'));
+ $this->assertFalse($this->testUser->hasRole($role->getKey(), 'fakeGuard'));
+ $this->assertFalse($this->testUser->hasRole([$role->getKey(), 'fakeRole'], 'fakeGuard'));
$role = app(Role::class)->findOrCreate('testRoleInWebGuard2', 'web');
$this->assertFalse($this->testUser->hasRole($role));
@@ -87,7 +88,7 @@ public function it_can_assign_a_role_using_an_object()
/** @test */
public function it_can_assign_a_role_using_an_id()
{
- $this->testUser->assignRole($this->testUserRole->id);
+ $this->testUser->assignRole($this->testUserRole->getKey());
$this->assertTrue($this->testUser->hasRole($this->testUserRole));
}
@@ -95,7 +96,7 @@ public function it_can_assign_a_role_using_an_id()
/** @test */
public function it_can_assign_multiple_roles_at_once()
{
- $this->testUser->assignRole($this->testUserRole->id, 'testRole2');
+ $this->testUser->assignRole($this->testUserRole->getKey(), 'testRole2');
$this->assertTrue($this->testUser->hasRole('testRole'));
@@ -105,7 +106,7 @@ public function it_can_assign_multiple_roles_at_once()
/** @test */
public function it_can_assign_multiple_roles_using_an_array()
{
- $this->testUser->assignRole([$this->testUserRole->id, 'testRole2']);
+ $this->testUser->assignRole([$this->testUserRole->getKey(), 'testRole2']);
$this->assertTrue($this->testUser->hasRole('testRole'));
@@ -115,7 +116,7 @@ public function it_can_assign_multiple_roles_using_an_array()
/** @test */
public function it_does_not_remove_already_associated_roles_when_assigning_new_roles()
{
- $this->testUser->assignRole($this->testUserRole->id);
+ $this->testUser->assignRole($this->testUserRole->getKey());
$this->testUser->assignRole('testRole2');
@@ -125,9 +126,9 @@ public function it_does_not_remove_already_associated_roles_when_assigning_new_r
/** @test */
public function it_does_not_throw_an_exception_when_assigning_a_role_that_is_already_assigned()
{
- $this->testUser->assignRole($this->testUserRole->id);
+ $this->testUser->assignRole($this->testUserRole->getKey());
- $this->testUser->assignRole($this->testUserRole->id);
+ $this->testUser->assignRole($this->testUserRole->getKey());
$this->assertTrue($this->testUser->fresh()->hasRole('testRole'));
}
@@ -245,10 +246,17 @@ public function calling_syncRoles_before_saving_object_doesnt_interfere_with_oth
$user2 = new User(['email' => 'admin@user.com']);
$user2->syncRoles('testRole2');
+
+ DB::enableQueryLog();
$user2->save();
+ DB::disableQueryLog();
+
+ $this->assertTrue($user->fresh()->hasRole('testRole'));
+ $this->assertFalse($user->fresh()->hasRole('testRole2'));
$this->assertTrue($user2->fresh()->hasRole('testRole2'));
$this->assertFalse($user2->fresh()->hasRole('testRole'));
+ $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync
}
/** @test */
@@ -260,10 +268,17 @@ public function calling_assignRole_before_saving_object_doesnt_interfere_with_ot
$admin_user = new User(['email' => 'admin@user.com']);
$admin_user->assignRole('testRole2');
+
+ DB::enableQueryLog();
$admin_user->save();
+ DB::disableQueryLog();
+
+ $this->assertTrue($user->fresh()->hasRole('testRole'));
+ $this->assertFalse($user->fresh()->hasRole('testRole2'));
$this->assertTrue($admin_user->fresh()->hasRole('testRole2'));
$this->assertFalse($admin_user->fresh()->hasRole('testRole'));
+ $this->assertSame(4, count(DB::getQueryLog())); //avoid unnecessary sync
}
/** @test */
@@ -305,7 +320,7 @@ public function it_can_scope_users_using_a_string()
$scopedUsers = User::role('testRole')->get();
- $this->assertEquals($scopedUsers->count(), 1);
+ $this->assertEquals(1, $scopedUsers->count());
}
/** @test */
@@ -320,8 +335,8 @@ public function it_can_scope_users_using_an_array()
$scopedUsers2 = User::role(['testRole', 'testRole2'])->get();
- $this->assertEquals($scopedUsers1->count(), 1);
- $this->assertEquals($scopedUsers2->count(), 2);
+ $this->assertEquals(1, $scopedUsers1->count());
+ $this->assertEquals(2, $scopedUsers2->count());
}
/** @test */
@@ -336,11 +351,11 @@ public function it_can_scope_users_using_an_array_of_ids_and_names()
$roleName = $this->testUserRole->name;
- $otherRoleId = app(Role::class)->find(2)->id;
+ $otherRoleId = app(Role::class)->findByName('testRole2')->getKey();
$scopedUsers = User::role([$roleName, $otherRoleId])->get();
- $this->assertEquals($scopedUsers->count(), 2);
+ $this->assertEquals(2, $scopedUsers->count());
}
/** @test */
@@ -354,8 +369,8 @@ public function it_can_scope_users_using_a_collection()
$scopedUsers1 = User::role([$this->testUserRole])->get();
$scopedUsers2 = User::role(collect(['testRole', 'testRole2']))->get();
- $this->assertEquals($scopedUsers1->count(), 1);
- $this->assertEquals($scopedUsers2->count(), 2);
+ $this->assertEquals(1, $scopedUsers1->count());
+ $this->assertEquals(2, $scopedUsers2->count());
}
/** @test */
@@ -370,9 +385,9 @@ public function it_can_scope_users_using_an_object()
$scopedUsers2 = User::role([$this->testUserRole])->get();
$scopedUsers3 = User::role(collect([$this->testUserRole]))->get();
- $this->assertEquals($scopedUsers1->count(), 1);
- $this->assertEquals($scopedUsers2->count(), 1);
- $this->assertEquals($scopedUsers3->count(), 1);
+ $this->assertEquals(1, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ $this->assertEquals(1, $scopedUsers3->count());
}
/** @test */
@@ -385,7 +400,7 @@ public function it_can_scope_against_a_specific_guard()
$scopedUsers1 = User::role('testRole', 'web')->get();
- $this->assertEquals($scopedUsers1->count(), 1);
+ $this->assertEquals(1, $scopedUsers1->count());
$user3 = Admin::create(['email' => 'user1@test.com']);
$user4 = Admin::create(['email' => 'user1@test.com']);
@@ -397,8 +412,8 @@ public function it_can_scope_against_a_specific_guard()
$scopedUsers2 = Admin::role('testAdminRole', 'admin')->get();
$scopedUsers3 = Admin::role('testAdminRole2', 'admin')->get();
- $this->assertEquals($scopedUsers2->count(), 2);
- $this->assertEquals($scopedUsers3->count(), 1);
+ $this->assertEquals(2, $scopedUsers2->count());
+ $this->assertEquals(1, $scopedUsers3->count());
}
/** @test */
@@ -478,6 +493,45 @@ public function it_can_determine_that_a_user_has_all_of_the_given_roles()
$this->assertFalse($this->testUser->hasAllRoles(['testRole', 'second role'], 'fakeGuard'));
}
+ /** @test */
+ public function it_can_determine_that_a_user_has_exact_all_of_the_given_roles()
+ {
+ $roleModel = app(Role::class);
+
+ $this->assertFalse($this->testUser->hasExactRoles($roleModel->first()));
+
+ $this->assertFalse($this->testUser->hasExactRoles('testRole'));
+
+ $this->assertFalse($this->testUser->hasExactRoles($roleModel->all()));
+
+ $roleModel->create(['name' => 'second role']);
+
+ $this->testUser->assignRole($this->testUserRole);
+
+ $this->assertTrue($this->testUser->hasExactRoles('testRole'));
+ $this->assertTrue($this->testUser->hasExactRoles('testRole', 'web'));
+ $this->assertFalse($this->testUser->hasExactRoles('testRole', 'fakeGuard'));
+
+ $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role']));
+ $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'], 'web'));
+
+ $this->testUser->assignRole('second role');
+
+ $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'second role']));
+ $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'second role'], 'web'));
+ $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'], 'fakeGuard'));
+
+ $roleModel->create(['name' => 'third role']);
+ $this->testUser->assignRole('third role');
+
+ $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role']));
+ $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'], 'web'));
+ $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role'], 'fakeGuard'));
+ $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'second role', 'third role']));
+ $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'second role', 'third role'], 'web'));
+ $this->assertFalse($this->testUser->hasExactRoles(['testRole', 'second role', 'third role'], 'fakeGuard'));
+ }
+
/** @test */
public function it_can_determine_that_a_user_does_not_have_a_role_from_another_guard()
{
@@ -513,7 +567,7 @@ public function it_can_retrieve_role_names()
$this->assertEquals(
collect(['testRole', 'testRole2']),
- $this->testUser->getRoleNames()
+ $this->testUser->getRoleNames()->sort()->values()
);
}
diff --git a/tests/HasRolesWithCustomModelsTest.php b/tests/HasRolesWithCustomModelsTest.php
new file mode 100644
index 000000000..d39739205
--- /dev/null
+++ b/tests/HasRolesWithCustomModelsTest.php
@@ -0,0 +1,15 @@
+assertSame(get_class($this->testUserRole), Role::class);
+ }
+}
diff --git a/tests/Manager.php b/tests/Manager.php
index 9bd21928d..000150913 100644
--- a/tests/Manager.php
+++ b/tests/Manager.php
@@ -11,7 +11,9 @@
class Manager extends Model implements AuthorizableContract, AuthenticatableContract
{
- use HasRoles, Authorizable, Authenticatable;
+ use HasRoles;
+ use Authorizable;
+ use Authenticatable;
protected $fillable = ['email'];
@@ -23,7 +25,7 @@ class Manager extends Model implements AuthorizableContract, AuthenticatableCont
// When present, it takes precedence over the $guard_name property.
public function guardName()
{
- return 'api';
+ return 'jwt';
}
// intentionally different property value for the sake of unit tests
diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php
deleted file mode 100644
index 0787922a5..000000000
--- a/tests/MiddlewareTest.php
+++ /dev/null
@@ -1,536 +0,0 @@
-roleMiddleware = new RoleMiddleware();
-
- $this->permissionMiddleware = new PermissionMiddleware();
-
- $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware();
- }
-
- /** @test */
- public function a_guest_cannot_access_a_route_protected_by_the_role_or_permission_middleware()
- {
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleOrPermissionMiddleware,
- 'testRole'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_guest_cannot_access_a_route_protected_by_rolemiddleware()
- {
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- 'testRole'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_user_cannot_access_a_route_protected_by_role_middleware_of_another_guard()
- {
- Auth::login($this->testUser);
-
- $this->testUser->assignRole('testRole');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- 'testAdminRole'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_user_can_access_a_route_protected_by_role_middleware_if_have_this_role()
- {
- Auth::login($this->testUser);
-
- $this->testUser->assignRole('testRole');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- 'testRole'
- ),
- 200
- );
- }
-
- /** @test */
- public function a_user_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles()
- {
- Auth::login($this->testUser);
-
- $this->testUser->assignRole('testRole');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- 'testRole|testRole2'
- ),
- 200
- );
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- ['testRole2', 'testRole']
- ),
- 200
- );
- }
-
- /** @test */
- public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role()
- {
- Auth::login($this->testUser);
-
- $this->testUser->assignRole(['testRole']);
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- 'testRole2'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_user_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles()
- {
- Auth::login($this->testUser);
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- 'testRole|testRole2'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_user_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined()
- {
- Auth::login($this->testUser);
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- ''
- ),
- 403
- );
- }
-
- /** @test */
- public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware()
- {
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'edit-articles'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard()
- {
- // These permissions are created fresh here in reverse order of guard being applied, so they are not "found first" in the db lookup when matching
- app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'web']);
- app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'admin']);
- app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'admin']);
- app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'web']);
-
- Auth::login($this->testAdmin);
-
- $this->testAdmin->givePermissionTo('admin-permission2');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'admin-permission2'
- ),
- 200
- );
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'edit-articles2'
- ),
- 403
- );
-
- Auth::login($this->testUser);
-
- $this->testUser->givePermissionTo('edit-articles2');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'edit-articles2'
- ),
- 200
- );
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'admin-permission2'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission()
- {
- Auth::login($this->testUser);
-
- $this->testUser->givePermissionTo('edit-articles');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'edit-articles'
- ),
- 200
- );
- }
-
- /** @test */
- public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions()
- {
- Auth::login($this->testUser);
-
- $this->testUser->givePermissionTo('edit-articles');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'edit-news|edit-articles'
- ),
- 200
- );
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- ['edit-news', 'edit-articles']
- ),
- 200
- );
- }
-
- /** @test */
- public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission()
- {
- Auth::login($this->testUser);
-
- $this->testUser->givePermissionTo('edit-articles');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'edit-news'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_user_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions()
- {
- Auth::login($this->testUser);
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'edit-articles|edit-news'
- ),
- 403
- );
- }
-
- /** @test */
- public function a_user_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role()
- {
- Auth::login($this->testUser);
-
- $this->testUser->assignRole('testRole');
- $this->testUser->givePermissionTo('edit-articles');
-
- $this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-news|edit-articles'),
- 200
- );
-
- $this->testUser->removeRole('testRole');
-
- $this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles'),
- 200
- );
-
- $this->testUser->revokePermissionTo('edit-articles');
- $this->testUser->assignRole('testRole');
-
- $this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles'),
- 200
- );
-
- $this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'edit-articles']),
- 200
- );
- }
-
- /** @test */
- public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role()
- {
- Auth::login($this->testUser);
-
- $this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles'),
- 403
- );
-
- $this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission'),
- 403
- );
- }
-
- /** @test */
- public function the_required_roles_can_be_fetched_from_the_exception()
- {
- Auth::login($this->testUser);
-
- $requiredRoles = [];
-
- try {
- $this->roleMiddleware->handle(new Request(), function () {
- return (new Response())->setContent('');
- }, 'some-role');
- } catch (UnauthorizedException $e) {
- $requiredRoles = $e->getRequiredRoles();
- }
-
- $this->assertEquals(['some-role'], $requiredRoles);
- }
-
- /** @test */
- public function the_required_permissions_can_be_fetched_from_the_exception()
- {
- Auth::login($this->testUser);
-
- $requiredPermissions = [];
-
- try {
- $this->permissionMiddleware->handle(new Request(), function () {
- return (new Response())->setContent('');
- }, 'some-permission');
- } catch (UnauthorizedException $e) {
- $requiredPermissions = $e->getRequiredPermissions();
- }
-
- $this->assertEquals(['some-permission'], $requiredPermissions);
- }
-
- /** @test */
- public function use_not_existing_custom_guard_in_role_or_permission()
- {
- $class = null;
-
- try {
- $this->roleOrPermissionMiddleware->handle(new Request(), function () {
- return (new Response())->setContent('');
- }, 'testRole', 'xxx');
- } catch (InvalidArgumentException $e) {
- $class = get_class($e);
- }
-
- $this->assertEquals(InvalidArgumentException::class, $class);
- }
-
- /** @test */
- public function use_not_existing_custom_guard_in_permission()
- {
- $class = null;
-
- try {
- $this->permissionMiddleware->handle(new Request(), function () {
- return (new Response())->setContent('');
- }, 'edit-articles', 'xxx');
- } catch (InvalidArgumentException $e) {
- $class = get_class($e);
- }
-
- $this->assertEquals(InvalidArgumentException::class, $class);
- }
-
- /** @test */
- public function use_not_existing_custom_guard_in_role()
- {
- $class = null;
-
- try {
- $this->roleMiddleware->handle(new Request(), function () {
- return (new Response())->setContent('');
- }, 'testRole', 'xxx');
- } catch (InvalidArgumentException $e) {
- $class = get_class($e);
- }
-
- $this->assertEquals(InvalidArgumentException::class, $class);
- }
-
- /** @test */
- public function user_can_not_access_role_with_guard_admin_while_using_default_guard()
- {
- Auth::login($this->testUser);
-
- $this->testUser->assignRole('testRole');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- 'testRole',
- 'admin'
- ),
- 403
- );
- }
-
- /** @test */
- public function user_can_access_role_with_guard_admin_while_using_default_guard()
- {
- Auth::guard('admin')->login($this->testAdmin);
-
- $this->testAdmin->assignRole('testAdminRole');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware,
- 'testAdminRole',
- 'admin'
- ),
- 200
- );
- }
-
- /** @test */
- public function user_can_not_access_permission_with_guard_admin_while_using_default_guard()
- {
- Auth::login($this->testUser);
-
- $this->testUser->givePermissionTo('edit-articles');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'edit-articles',
- 'admin'
- ),
- 403
- );
- }
-
- /** @test */
- public function user_can_access_permission_with_guard_admin_while_using_default_guard()
- {
- Auth::guard('admin')->login($this->testAdmin);
-
- $this->testAdmin->givePermissionTo('admin-permission');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'admin-permission',
- 'admin'
- ),
- 200
- );
- }
-
- /** @test */
- public function user_can_not_access_permission_or_role_with_guard_admin_while_using_default_guard()
- {
- Auth::login($this->testUser);
-
- $this->testUser->assignRole('testRole');
- $this->testUser->givePermissionTo('edit-articles');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleOrPermissionMiddleware,
- 'edit-articles|testRole',
- 'admin'
- ),
- 403
- );
- }
-
- /** @test */
- public function user_can_access_permission_or_role_with_guard_admin_while_using_default_guard()
- {
- Auth::guard('admin')->login($this->testAdmin);
-
- $this->testAdmin->assignRole('testAdminRole');
- $this->testAdmin->givePermissionTo('admin-permission');
-
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleOrPermissionMiddleware,
- 'admin-permission|testAdminRole',
- 'admin'
- ),
- 200
- );
- }
-
- protected function runMiddleware($middleware, $parameter, $guard = null)
- {
- try {
- return $middleware->handle(new Request(), function () {
- return (new Response())->setContent('');
- }, $parameter, $guard)->status();
- } catch (UnauthorizedException $e) {
- return $e->getStatusCode();
- }
- }
-}
diff --git a/tests/MultipleGuardsTest.php b/tests/MultipleGuardsTest.php
index 1bcc0de2d..066d0acf0 100644
--- a/tests/MultipleGuardsTest.php
+++ b/tests/MultipleGuardsTest.php
@@ -2,52 +2,70 @@
namespace Spatie\Permission\Test;
-use Spatie\Permission\Models\Permission;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Route;
+use Spatie\Permission\Contracts\Permission;
class MultipleGuardsTest extends TestCase
{
+ protected function getEnvironmentSetUp($app)
+ {
+ parent::getEnvironmentSetUp($app);
+
+ $app['config']->set('auth.guards', [
+ 'web' => ['driver' => 'session', 'provider' => 'users'],
+ 'api' => ['driver' => 'token', 'provider' => 'users'],
+ 'jwt' => ['driver' => 'token', 'provider' => 'users'],
+ 'abc' => ['driver' => 'abc'],
+ ]);
+
+ $this->setUpRoutes();
+ }
+
+ /**
+ * Create routes to test authentication with guards.
+ */
+ public function setUpRoutes(): void
+ {
+ Route::middleware('auth:api')->get('/check-api-guard-permission', function (Request $request) {
+ return [
+ 'status' => $request->user()->checkPermissionTo('use_api_guard'),
+ ];
+ });
+ }
+
/** @test */
public function it_can_give_a_permission_to_a_model_that_is_used_by_multiple_guards()
{
- $this->testUser->givePermissionTo(Permission::create([
+ $this->testUser->givePermissionTo(app(Permission::class)::create([
'name' => 'do_this',
'guard_name' => 'web',
]));
- $this->testUser->givePermissionTo(Permission::create([
+ $this->testUser->givePermissionTo(app(Permission::class)::create([
'name' => 'do_that',
'guard_name' => 'api',
]));
- $this->assertTrue($this->testUser->hasPermissionTo('do_this', 'web'));
- $this->assertTrue($this->testUser->hasPermissionTo('do_that', 'api'));
- }
-
- protected function getEnvironmentSetUp($app)
- {
- parent::getEnvironmentSetUp($app);
-
- $app['config']->set('auth.guards', [
- 'web' => ['driver' => 'session', 'provider' => 'users'],
- 'api' => ['driver' => 'jwt', 'provider' => 'users'],
- 'abc' => ['driver' => 'abc'],
- ]);
+ $this->assertTrue($this->testUser->checkPermissionTo('do_this', 'web'));
+ $this->assertTrue($this->testUser->checkPermissionTo('do_that', 'api'));
+ $this->assertFalse($this->testUser->checkPermissionTo('do_that', 'web'));
}
/** @test */
public function it_can_honour_guardName_function_on_model_for_overriding_guard_name_property()
{
$user = Manager::create(['email' => 'manager@test.com']);
- $user->givePermissionTo(Permission::create([
- 'name' => 'do_that',
- 'guard_name' => 'api',
+ $user->givePermissionTo(app(Permission::class)::create([
+ 'name' => 'do_jwt',
+ 'guard_name' => 'jwt',
]));
- // Manager test user has the guardName override method, which returns 'api'
- $this->assertTrue($user->checkPermissionTo('do_that', 'api'));
- $this->assertTrue($user->hasPermissionTo('do_that', 'api'));
+ // Manager test user has the guardName override method, which returns 'jwt'
+ $this->assertTrue($user->checkPermissionTo('do_jwt', 'jwt'));
+ $this->assertTrue($user->hasPermissionTo('do_jwt', 'jwt'));
// Manager test user has the $guard_name property set to 'web'
- $this->assertFalse($user->checkPermissionTo('do_that', 'web'));
+ $this->assertFalse($user->checkPermissionTo('do_jwt', 'web'));
}
}
diff --git a/tests/Permission.php b/tests/Permission.php
new file mode 100644
index 000000000..d7529ed69
--- /dev/null
+++ b/tests/Permission.php
@@ -0,0 +1,13 @@
+permissionMiddleware = new PermissionMiddleware();
+ }
+
+ /** @test */
+ public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware()
+ {
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles')
+ );
+ }
+
+ /** @test */
+ public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard()
+ {
+ // These permissions are created fresh here in reverse order of guard being applied, so they are not "found first" in the db lookup when matching
+ app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'web']);
+ $p1 = app(Permission::class)->create(['name' => 'admin-permission2', 'guard_name' => 'admin']);
+ app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'admin']);
+ $p2 = app(Permission::class)->create(['name' => 'edit-articles2', 'guard_name' => 'web']);
+
+ Auth::guard('admin')->login($this->testAdmin);
+
+ $this->testAdmin->givePermissionTo($p1);
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'admin-permission2', 'admin')
+ );
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles2', 'admin')
+ );
+
+ Auth::login($this->testUser);
+
+ $this->testUser->givePermissionTo($p2);
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles2', 'web')
+ );
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'admin-permission2', 'web')
+ );
+ }
+
+ /** @test */
+ public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->givePermissionTo('edit-articles');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles')
+ );
+ }
+
+ /** @test */
+ public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->givePermissionTo('edit-articles');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-news|edit-articles')
+ );
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, ['edit-news', 'edit-articles'])
+ );
+ }
+
+ /** @test */
+ public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->givePermissionTo('edit-articles');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-news')
+ );
+ }
+
+ /** @test */
+ public function a_user_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions()
+ {
+ Auth::login($this->testUser);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles|edit-news')
+ );
+ }
+
+ /** @test */
+ public function a_user_can_access_a_route_protected_by_permission_middleware_if_has_permission_via_role()
+ {
+ Auth::login($this->testUser);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles')
+ );
+
+ $this->testUserRole->givePermissionTo('edit-articles');
+ $this->testUser->assignRole('testRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles')
+ );
+ }
+
+ /** @test */
+ public function the_required_permissions_can_be_fetched_from_the_exception()
+ {
+ Auth::login($this->testUser);
+
+ $message = null;
+ $requiredPermissions = [];
+
+ try {
+ $this->permissionMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'some-permission');
+ } catch (UnauthorizedException $e) {
+ $message = $e->getMessage();
+ $requiredPermissions = $e->getRequiredPermissions();
+ }
+
+ $this->assertEquals('User does not have the right permissions.', $message);
+ $this->assertEquals(['some-permission'], $requiredPermissions);
+ }
+
+ /** @test */
+ public function the_required_permissions_can_be_displayed_in_the_exception()
+ {
+ Auth::login($this->testUser);
+ Config::set(['permission.display_permission_in_exception' => true]);
+
+ $message = null;
+
+ try {
+ $this->permissionMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'some-permission');
+ } catch (UnauthorizedException $e) {
+ $message = $e->getMessage();
+ }
+
+ $this->assertStringEndsWith('Necessary permissions are some-permission', $message);
+ }
+
+ /** @test */
+ public function use_not_existing_custom_guard_in_permission()
+ {
+ $class = null;
+
+ try {
+ $this->permissionMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'edit-articles', 'xxx');
+ } catch (InvalidArgumentException $e) {
+ $class = get_class($e);
+ }
+
+ $this->assertEquals(InvalidArgumentException::class, $class);
+ }
+
+ /** @test */
+ public function user_can_not_access_permission_with_guard_admin_while_login_using_default_guard()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->givePermissionTo('edit-articles');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles', 'admin')
+ );
+ }
+
+ /** @test */
+ public function user_can_access_permission_with_guard_admin_while_login_using_admin_guard()
+ {
+ Auth::guard('admin')->login($this->testAdmin);
+
+ $this->testAdmin->givePermissionTo('admin-permission');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'admin-permission', 'admin')
+ );
+ }
+
+ protected function runMiddleware($middleware, $permission, $guard = null)
+ {
+ try {
+ return $middleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, $permission, $guard)->status();
+ } catch (UnauthorizedException $e) {
+ return $e->getStatusCode();
+ }
+ }
+}
diff --git a/tests/PermissionTest.php b/tests/PermissionTest.php
index 35f02d0b0..b157d247d 100644
--- a/tests/PermissionTest.php
+++ b/tests/PermissionTest.php
@@ -7,6 +7,20 @@
class PermissionTest extends TestCase
{
+ /** @test */
+ public function it_get_user_models_using_with()
+ {
+ $this->testUser->givePermissionTo($this->testUserPermission);
+
+ $permission = app(Permission::class)::with('users')
+ ->where($this->testUserPermission->getKeyName(), $this->testUserPermission->getKey())
+ ->first();
+
+ $this->assertEquals($permission->getKey(), $this->testUserPermission->getKey());
+ $this->assertCount(1, $permission->users);
+ $this->assertEquals($permission->users[0]->id, $this->testUser->id);
+ }
+
/** @test */
public function it_throws_an_exception_when_the_permission_already_exists()
{
diff --git a/tests/Role.php b/tests/Role.php
new file mode 100644
index 000000000..bbe13ece7
--- /dev/null
+++ b/tests/Role.php
@@ -0,0 +1,13 @@
+roleMiddleware = new RoleMiddleware();
+ }
+
+ /** @test */
+ public function a_guest_cannot_access_a_route_protected_by_rolemiddleware()
+ {
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testRole')
+ );
+ }
+
+ /** @test */
+ public function a_user_cannot_access_a_route_protected_by_role_middleware_of_another_guard()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->assignRole('testRole');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testAdminRole')
+ );
+ }
+
+ /** @test */
+ public function a_user_can_access_a_route_protected_by_role_middleware_if_have_this_role()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->assignRole('testRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, 'testRole')
+ );
+ }
+
+ /** @test */
+ public function a_user_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->assignRole('testRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2')
+ );
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, ['testRole2', 'testRole'])
+ );
+ }
+
+ /** @test */
+ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->assignRole(['testRole']);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testRole2')
+ );
+ }
+
+ /** @test */
+ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles()
+ {
+ Auth::login($this->testUser);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2')
+ );
+ }
+
+ /** @test */
+ public function a_user_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined()
+ {
+ Auth::login($this->testUser);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, '')
+ );
+ }
+
+ /** @test */
+ public function the_required_roles_can_be_fetched_from_the_exception()
+ {
+ Auth::login($this->testUser);
+
+ $message = null;
+ $requiredRoles = [];
+
+ try {
+ $this->roleMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'some-role');
+ } catch (UnauthorizedException $e) {
+ $message = $e->getMessage();
+ $requiredRoles = $e->getRequiredRoles();
+ }
+
+ $this->assertEquals('User does not have the right roles.', $message);
+ $this->assertEquals(['some-role'], $requiredRoles);
+ }
+
+ /** @test */
+ public function the_required_roles_can_be_displayed_in_the_exception()
+ {
+ Auth::login($this->testUser);
+ Config::set(['permission.display_role_in_exception' => true]);
+
+ $message = null;
+
+ try {
+ $this->roleMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'some-role');
+ } catch (UnauthorizedException $e) {
+ $message = $e->getMessage();
+ }
+
+ $this->assertStringEndsWith('Necessary roles are some-role', $message);
+ }
+
+ /** @test */
+ public function use_not_existing_custom_guard_in_role()
+ {
+ $class = null;
+
+ try {
+ $this->roleMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'testRole', 'xxx');
+ } catch (InvalidArgumentException $e) {
+ $class = get_class($e);
+ }
+
+ $this->assertEquals(InvalidArgumentException::class, $class);
+ }
+
+ /** @test */
+ public function user_can_not_access_role_with_guard_admin_while_login_using_default_guard()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->assignRole('testRole');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testRole', 'admin')
+ );
+ }
+
+ /** @test */
+ public function user_can_access_role_with_guard_admin_while_login_using_admin_guard()
+ {
+ Auth::guard('admin')->login($this->testAdmin);
+
+ $this->testAdmin->assignRole('testAdminRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, 'testAdminRole', 'admin')
+ );
+ }
+
+ protected function runMiddleware($middleware, $roleName, $guard = null)
+ {
+ try {
+ return $middleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, $roleName, $guard)->status();
+ } catch (UnauthorizedException $e) {
+ return $e->getStatusCode();
+ }
+ }
+}
diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php
new file mode 100644
index 000000000..345f0209a
--- /dev/null
+++ b/tests/RoleOrPermissionMiddlewareTest.php
@@ -0,0 +1,178 @@
+roleOrPermissionMiddleware = new RoleOrPermissionMiddleware();
+ }
+
+ /** @test */
+ public function a_guest_cannot_access_a_route_protected_by_the_role_or_permission_middleware()
+ {
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole')
+ );
+ }
+
+ /** @test */
+ public function a_user_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->assignRole('testRole');
+ $this->testUser->givePermissionTo('edit-articles');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-news|edit-articles')
+ );
+
+ $this->testUser->removeRole('testRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles')
+ );
+
+ $this->testUser->revokePermissionTo('edit-articles');
+ $this->testUser->assignRole('testRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles')
+ );
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'edit-articles'])
+ );
+ }
+
+ /** @test */
+ public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role()
+ {
+ Auth::login($this->testUser);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles')
+ );
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission')
+ );
+ }
+
+ /** @test */
+ public function use_not_existing_custom_guard_in_role_or_permission()
+ {
+ $class = null;
+
+ try {
+ $this->roleOrPermissionMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'testRole', 'xxx');
+ } catch (InvalidArgumentException $e) {
+ $class = get_class($e);
+ }
+
+ $this->assertEquals(InvalidArgumentException::class, $class);
+ }
+
+ /** @test */
+ public function user_can_not_access_permission_or_role_with_guard_admin_while_login_using_default_guard()
+ {
+ Auth::login($this->testUser);
+
+ $this->testUser->assignRole('testRole');
+ $this->testUser->givePermissionTo('edit-articles');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'edit-articles|testRole', 'admin')
+ );
+ }
+
+ /** @test */
+ public function user_can_access_permission_or_role_with_guard_admin_while_login_using_admin_guard()
+ {
+ Auth::guard('admin')->login($this->testAdmin);
+
+ $this->testAdmin->assignRole('testAdminRole');
+ $this->testAdmin->givePermissionTo('admin-permission');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'admin-permission|testAdminRole', 'admin')
+ );
+ }
+
+ /** @test */
+ public function the_required_permissions_or_roles_can_be_fetched_from_the_exception()
+ {
+ Auth::login($this->testUser);
+
+ $message = null;
+ $requiredRolesOrPermissions = [];
+
+ try {
+ $this->roleOrPermissionMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'some-permission|some-role');
+ } catch (UnauthorizedException $e) {
+ $message = $e->getMessage();
+ $requiredRolesOrPermissions = $e->getRequiredPermissions();
+ }
+
+ $this->assertEquals('User does not have any of the necessary access rights.', $message);
+ $this->assertEquals(['some-permission', 'some-role'], $requiredRolesOrPermissions);
+ }
+
+ /** @test */
+ public function the_required_permissions_or_roles_can_be_displayed_in_the_exception()
+ {
+ Auth::login($this->testUser);
+ Config::set(['permission.display_permission_in_exception' => true]);
+ Config::set(['permission.display_role_in_exception' => true]);
+
+ $message = null;
+
+ try {
+ $this->roleOrPermissionMiddleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, 'some-permission|some-role');
+ } catch (UnauthorizedException $e) {
+ $message = $e->getMessage();
+ }
+
+ $this->assertStringEndsWith('Necessary roles or permissions are some-permission, some-role', $message);
+ }
+
+ protected function runMiddleware($middleware, $name, $guard = null)
+ {
+ try {
+ return $middleware->handle(new Request(), function () {
+ return (new Response())->setContent('');
+ }, $name, $guard)->status();
+ } catch (UnauthorizedException $e) {
+ return $e->getStatusCode();
+ }
+ }
+}
diff --git a/tests/RoleTest.php b/tests/RoleTest.php
index 89d51804a..35e287d50 100644
--- a/tests/RoleTest.php
+++ b/tests/RoleTest.php
@@ -8,6 +8,7 @@
use Spatie\Permission\Exceptions\RoleAlreadyExists;
use Spatie\Permission\Exceptions\RoleDoesNotExist;
use Spatie\Permission\Models\Permission;
+use Spatie\Permission\PermissionRegistrar;
class RoleTest extends TestCase
{
@@ -20,6 +21,19 @@ public function setUp(): void
Permission::create(['name' => 'wrong-guard-permission', 'guard_name' => 'admin']);
}
+ /** @test */
+ public function it_get_user_models_using_with()
+ {
+ $this->testUser->assignRole($this->testUserRole);
+
+ $role = app(Role::class)::with('users')
+ ->where($this->testUserRole->getKeyName(), $this->testUserRole->getKey())->first();
+
+ $this->assertEquals($role->getKey(), $this->testUserRole->getKey());
+ $this->assertCount(1, $role->users);
+ $this->assertEquals($role->users[0]->id, $this->testUser->id);
+ }
+
/** @test */
public function it_has_user_models_of_the_right_class()
{
@@ -239,4 +253,29 @@ public function it_belongs_to_the_default_guard_by_default()
$this->testUserRole->guard_name
);
}
+
+ /** @test */
+ public function it_can_change_role_class_on_runtime()
+ {
+ $role = app(Role::class)->create(['name' => 'test-role-old']);
+ $this->assertNotInstanceOf(RuntimeRole::class, $role);
+
+ $role->givePermissionTo('edit-articles');
+
+ app('config')->set('permission.models.role', RuntimeRole::class);
+ app()->bind(Role::class, RuntimeRole::class);
+ app(PermissionRegistrar::class)->setRoleClass(RuntimeRole::class);
+
+ $permission = app(Permission::class)->findByName('edit-articles');
+ $this->assertInstanceOf(RuntimeRole::class, $permission->roles[0]);
+ $this->assertSame('test-role-old', $permission->roles[0]->name);
+
+ $role = app(Role::class)->create(['name' => 'test-role']);
+ $this->assertInstanceOf(RuntimeRole::class, $role);
+
+ $this->testUser->assignRole('test-role');
+ $this->assertTrue($this->testUser->hasRole('test-role'));
+ $this->assertInstanceOf(RuntimeRole::class, $this->testUser->roles[0]);
+ $this->assertSame('test-role', $this->testUser->roles[0]->name);
+ }
}
diff --git a/tests/RuntimeRole.php b/tests/RuntimeRole.php
new file mode 100644
index 000000000..82f24a09f
--- /dev/null
+++ b/tests/RuntimeRole.php
@@ -0,0 +1,11 @@
+testUser->load('permissions');
+ $this->testUser->givePermissionTo('edit-articles', 'edit-news');
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('permissions');
+ $this->testUser->givePermissionTo('edit-articles', 'edit-blog');
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('permissions');
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-news']),
+ $this->testUser->getPermissionNames()->sort()->values()
+ );
+ $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news']));
+ $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-blog']));
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('permissions');
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-blog']),
+ $this->testUser->getPermissionNames()->sort()->values()
+ );
+ $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-blog']));
+ $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news']));
+ }
+
+ /** @test */
+ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_roles_on_same_user_on_different_teams()
+ {
+ $this->testUserRole->givePermissionTo('edit-articles');
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('permissions');
+ $this->testUser->assignRole('testRole');
+ $this->testUser->givePermissionTo('edit-news');
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('permissions');
+ $this->testUser->assignRole('testRole');
+ $this->testUser->givePermissionTo('edit-blog');
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('roles');
+ $this->testUser->load('permissions');
+
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-news']),
+ $this->testUser->getAllPermissions()->pluck('name')->sort()->values()
+ );
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('roles');
+ $this->testUser->load('permissions');
+
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-blog']),
+ $this->testUser->getAllPermissions()->pluck('name')->sort()->values()
+ );
+ }
+
+ /** @test */
+ public function it_can_sync_or_remove_permission_without_detach_on_different_teams()
+ {
+ setPermissionsTeamId(1);
+ $this->testUser->load('permissions');
+ $this->testUser->syncPermissions('edit-articles', 'edit-news');
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('permissions');
+ $this->testUser->syncPermissions('edit-articles', 'edit-blog');
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('permissions');
+
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-news']),
+ $this->testUser->getPermissionNames()->sort()->values()
+ );
+
+ $this->testUser->revokePermissionTo('edit-articles');
+ $this->assertEquals(
+ collect(['edit-news']),
+ $this->testUser->getPermissionNames()->sort()->values()
+ );
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('permissions');
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-blog']),
+ $this->testUser->getPermissionNames()->sort()->values()
+ );
+ }
+
+ /** @test */
+ public function it_can_scope_users_on_different_teams()
+ {
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+
+ setPermissionsTeamId(2);
+ $user1->givePermissionTo(['edit-articles', 'edit-news']);
+ $this->testUserRole->givePermissionTo('edit-articles');
+ $user2->assignRole('testRole');
+
+ setPermissionsTeamId(1);
+ $user1->givePermissionTo(['edit-articles']);
+
+ setPermissionsTeamId(2);
+ $scopedUsers1Team2 = User::permission(['edit-articles', 'edit-news'])->get();
+ $scopedUsers2Team2 = User::permission('edit-news')->get();
+
+ $this->assertEquals(2, $scopedUsers1Team2->count());
+ $this->assertEquals(1, $scopedUsers2Team2->count());
+
+ setPermissionsTeamId(1);
+ $scopedUsers1Team1 = User::permission(['edit-articles', 'edit-news'])->get();
+ $scopedUsers2Team1 = User::permission('edit-news')->get();
+
+ $this->assertEquals(1, $scopedUsers1Team1->count());
+ $this->assertEquals(0, $scopedUsers2Team1->count());
+ }
+}
diff --git a/tests/TeamHasRolesTest.php b/tests/TeamHasRolesTest.php
new file mode 100644
index 000000000..05393194a
--- /dev/null
+++ b/tests/TeamHasRolesTest.php
@@ -0,0 +1,124 @@
+create(['name' => 'testRole3']); //team_test_id = 1 by main class
+ app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]);
+ app(Role::class)->create(['name' => 'testRole4', 'team_test_id' => null]); //global role
+
+ $testRole3Team1 = app(Role::class)->where(['name' => 'testRole3', 'team_test_id' => 1])->first();
+ $testRole3Team2 = app(Role::class)->where(['name' => 'testRole3', 'team_test_id' => 2])->first();
+ $testRole4NoTeam = app(Role::class)->where(['name' => 'testRole4', 'team_test_id' => null])->first();
+ $this->assertNotNull($testRole3Team1);
+ $this->assertNotNull($testRole4NoTeam);
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('roles');
+ $this->testUser->assignRole('testRole', 'testRole2');
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('roles');
+ $this->testUser->assignRole('testRole', 'testRole3');
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('roles');
+
+ $this->assertEquals(
+ collect(['testRole', 'testRole2']),
+ $this->testUser->getRoleNames()->sort()->values()
+ );
+ $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole2']));
+
+ $this->testUser->assignRole('testRole3', 'testRole4');
+ $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole2', 'testRole3', 'testRole4']));
+ $this->assertTrue($this->testUser->hasRole($testRole3Team1)); //testRole3 team=1
+ $this->assertTrue($this->testUser->hasRole($testRole4NoTeam)); // global role team=null
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('roles');
+
+ $this->assertEquals(
+ collect(['testRole', 'testRole3']),
+ $this->testUser->getRoleNames()->sort()->values()
+ );
+ $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole3']));
+ $this->assertTrue($this->testUser->hasRole($testRole3Team2)); //testRole3 team=2
+ $this->testUser->assignRole('testRole4');
+ $this->assertTrue($this->testUser->hasExactRoles(['testRole', 'testRole3', 'testRole4']));
+ $this->assertTrue($this->testUser->hasRole($testRole4NoTeam)); // global role team=null
+ }
+
+ /** @test */
+ public function it_can_sync_or_remove_roles_without_detach_on_different_teams()
+ {
+ app(Role::class)->create(['name' => 'testRole3', 'team_test_id' => 2]);
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('roles');
+ $this->testUser->syncRoles('testRole', 'testRole2');
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('roles');
+ $this->testUser->syncRoles('testRole', 'testRole3');
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('roles');
+
+ $this->assertEquals(
+ collect(['testRole', 'testRole2']),
+ $this->testUser->getRoleNames()->sort()->values()
+ );
+
+ $this->testUser->removeRole('testRole');
+ $this->assertEquals(
+ collect(['testRole2']),
+ $this->testUser->getRoleNames()->sort()->values()
+ );
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('roles');
+
+ $this->assertEquals(
+ collect(['testRole', 'testRole3']),
+ $this->testUser->getRoleNames()->sort()->values()
+ );
+ }
+
+ /** @test */
+ public function it_can_scope_users_on_different_teams()
+ {
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+
+ setPermissionsTeamId(2);
+ $user1->assignRole($this->testUserRole);
+ $user2->assignRole('testRole2');
+
+ setPermissionsTeamId(1);
+ $user1->assignRole('testRole');
+
+ setPermissionsTeamId(2);
+ $scopedUsers1Team1 = User::role($this->testUserRole)->get();
+ $scopedUsers2Team1 = User::role(['testRole', 'testRole2'])->get();
+
+ $this->assertEquals(1, $scopedUsers1Team1->count());
+ $this->assertEquals(2, $scopedUsers2Team1->count());
+
+ setPermissionsTeamId(1);
+ $scopedUsers1Team2 = User::role($this->testUserRole)->get();
+ $scopedUsers2Team2 = User::role('testRole2')->get();
+
+ $this->assertEquals(1, $scopedUsers1Team2->count());
+ $this->assertEquals(0, $scopedUsers2Team2->count());
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index a789cc882..8f913c53e 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -3,7 +3,9 @@
namespace Spatie\Permission\Test;
use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Schema;
use Orchestra\Testbench\TestCase as Orchestra;
use Spatie\Permission\Contracts\Permission;
@@ -31,20 +33,30 @@ abstract class TestCase extends Orchestra
/** @var \Spatie\Permission\Models\Permission */
protected $testAdminPermission;
+ /** @var bool */
+ protected $useCustomModels = false;
+
+ /** @var bool */
+ protected $hasTeams = false;
+
+ protected static $migration;
+ protected static $customMigration;
+
public function setUp(): void
{
parent::setUp();
+ if (! self::$migration) {
+ $this->prepareMigration();
+ }
+
// Note: this also flushes the cache from within the migration
$this->setUpDatabase($this->app);
+ if ($this->hasTeams) {
+ setPermissionsTeamId(1);
+ }
- $this->testUser = User::first();
- $this->testUserRole = app(Role::class)->find(1);
- $this->testUserPermission = app(Permission::class)->find(1);
-
- $this->testAdmin = Admin::first();
- $this->testAdminRole = app(Role::class)->find(3);
- $this->testAdminPermission = app(Permission::class)->find(4);
+ $this->setUpRoutes();
}
/**
@@ -66,19 +78,31 @@ protected function getPackageProviders($app)
*/
protected function getEnvironmentSetUp($app)
{
+ $app['config']->set('permission.register_permission_check_method', true);
+ $app['config']->set('permission.teams', $this->hasTeams);
+ $app['config']->set('permission.testing', true); //fix sqlite
+ $app['config']->set('permission.column_names.model_morph_key', 'model_test_id');
+ $app['config']->set('permission.column_names.team_foreign_key', 'team_test_id');
$app['config']->set('database.default', 'sqlite');
$app['config']->set('database.connections.sqlite', [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
]);
-
+ $app['config']->set('permission.column_names.role_pivot_key', 'role_test_id');
+ $app['config']->set('permission.column_names.permission_pivot_key', 'permission_test_id');
$app['config']->set('view.paths', [__DIR__.'/resources/views']);
+ // ensure api guard exists (required since Laravel 8.55)
+ $app['config']->set('auth.guards.api', ['driver' => 'session', 'provider' => 'users']);
+
// Set-up admin guard
$app['config']->set('auth.guards.admin', ['driver' => 'session', 'provider' => 'admins']);
$app['config']->set('auth.providers.admins', ['driver' => 'eloquent', 'model' => Admin::class]);
-
+ if ($this->useCustomModels) {
+ $app['config']->set('permission.models.permission', \Spatie\Permission\Test\Permission::class);
+ $app['config']->set('permission.models.role', \Spatie\Permission\Test\Role::class);
+ }
// Use test User model for users provider
$app['config']->set('auth.providers.users.model', User::class);
@@ -92,8 +116,6 @@ protected function getEnvironmentSetUp($app)
*/
protected function setUpDatabase($app)
{
- $app['config']->set('permission.column_names.model_morph_key', 'model_test_id');
-
$app['db']->connection()->getSchemaBuilder()->create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('email');
@@ -110,25 +132,53 @@ protected function setUpDatabase($app)
$this->createCacheTable();
}
- include_once __DIR__.'/../database/migrations/create_permission_tables.php.stub';
-
- (new \CreatePermissionTables())->up();
+ if (! $this->useCustomModels) {
+ self::$migration->up();
+ } else {
+ self::$customMigration->up();
+ }
- User::create(['email' => 'test@user.com']);
- Admin::create(['email' => 'admin@user.com']);
- $app[Role::class]->create(['name' => 'testRole']);
+ $this->testUser = User::create(['email' => 'test@user.com']);
+ $this->testAdmin = Admin::create(['email' => 'admin@user.com']);
+ $this->testUserRole = $app[Role::class]->create(['name' => 'testRole']);
$app[Role::class]->create(['name' => 'testRole2']);
- $app[Role::class]->create(['name' => 'testAdminRole', 'guard_name' => 'admin']);
- $app[Permission::class]->create(['name' => 'edit-articles']);
+ $this->testAdminRole = $app[Role::class]->create(['name' => 'testAdminRole', 'guard_name' => 'admin']);
+ $this->testUserPermission = $app[Permission::class]->create(['name' => 'edit-articles']);
$app[Permission::class]->create(['name' => 'edit-news']);
$app[Permission::class]->create(['name' => 'edit-blog']);
- $app[Permission::class]->create(['name' => 'admin-permission', 'guard_name' => 'admin']);
+ $this->testAdminPermission = $app[Permission::class]->create(['name' => 'admin-permission', 'guard_name' => 'admin']);
$app[Permission::class]->create(['name' => 'Edit News']);
}
- /**
- * Reload the permissions.
- */
+ private function prepareMigration()
+ {
+ $migration = str_replace(
+ [
+ 'CreatePermissionTables',
+ '(\'id\'); // permission id',
+ '(\'id\'); // role id',
+ 'references(\'id\') // permission id',
+ 'references(\'id\') // role id',
+ ],
+ [
+ 'CreatePermissionCustomTables',
+ '(\'permission_test_id\');',
+ '(\'role_test_id\');',
+ 'references(\'permission_test_id\')',
+ 'references(\'role_test_id\')',
+ ],
+ file_get_contents(__DIR__.'/../database/migrations/create_permission_tables.php.stub')
+ );
+
+ file_put_contents(__DIR__.'/CreatePermissionCustomTables.php', $migration);
+
+ include_once __DIR__.'/../database/migrations/create_permission_tables.php.stub';
+ self::$migration = new \CreatePermissionTables();
+
+ include_once __DIR__.'/CreatePermissionCustomTables.php';
+ self::$customMigration = new \CreatePermissionCustomTables();
+ }
+
protected function reloadPermissions()
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
@@ -142,4 +192,16 @@ public function createCacheTable()
$table->integer('expiration');
});
}
+
+ /**
+ * Create routes to test authentication with guards.
+ */
+ public function setUpRoutes(): void
+ {
+ Route::middleware('auth:api')->get('/check-api-guard-permission', function (Request $request) {
+ return [
+ 'status' => $request->user()->hasPermissionTo('do_that'),
+ ];
+ });
+ }
}
diff --git a/tests/User.php b/tests/User.php
index dc8e2c568..886fd8fd2 100644
--- a/tests/User.php
+++ b/tests/User.php
@@ -11,7 +11,9 @@
class User extends Model implements AuthorizableContract, AuthenticatableContract
{
- use HasRoles, Authorizable, Authenticatable;
+ use HasRoles;
+ use Authorizable;
+ use Authenticatable;
protected $fillable = ['email'];
diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php
index 260223472..9fe52796d 100644
--- a/tests/WildcardMiddlewareTest.php
+++ b/tests/WildcardMiddlewareTest.php
@@ -34,11 +34,8 @@ public function setUp(): void
public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware()
{
$this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'articles.edit'
- ),
- 403
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'articles.edit')
);
}
@@ -52,11 +49,8 @@ public function a_user_can_access_a_route_protected_by_permission_middleware_if_
$this->testUser->givePermissionTo('articles');
$this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'articles.edit'
- ),
- 200
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'articles.edit')
);
}
@@ -70,19 +64,13 @@ public function a_user_can_access_a_route_protected_by_this_permission_middlewar
$this->testUser->givePermissionTo('articles.*.test');
$this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'news.edit|articles.create.test'
- ),
- 200
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'news.edit|articles.create.test')
);
$this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- ['news.edit', 'articles.create.test']
- ),
- 200
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, ['news.edit', 'articles.create.test'])
);
}
@@ -96,11 +84,8 @@ public function a_user_cannot_access_a_route_protected_by_the_permission_middlew
$this->testUser->givePermissionTo('articles.*');
$this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'news.edit'
- ),
- 403
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'news.edit')
);
}
@@ -110,11 +95,8 @@ public function a_user_cannot_access_a_route_protected_by_permission_middleware_
Auth::login($this->testUser);
$this->assertEquals(
- $this->runMiddleware(
- $this->permissionMiddleware,
- 'articles.edit|news.edit'
- ),
- 403
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'articles.edit|news.edit')
);
}
@@ -129,28 +111,28 @@ public function a_user_can_access_a_route_protected_by_permission_or_role_middle
$this->testUser->givePermissionTo('articles.*');
$this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|news.edit|articles.create'),
- 200
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|news.edit|articles.create')
);
$this->testUser->removeRole('testRole');
$this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit'),
- 200
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit')
);
$this->testUser->revokePermissionTo('articles.*');
$this->testUser->assignRole('testRole');
$this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit'),
- 200
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit')
);
$this->assertEquals(
- $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'articles.edit']),
- 200
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'articles.edit'])
);
}
diff --git a/tests/resources/views/guardHasAllRolesArray.blade.php b/tests/resources/views/guardHasAllRolesArray.blade.php
new file mode 100644
index 000000000..6ba51c1d8
--- /dev/null
+++ b/tests/resources/views/guardHasAllRolesArray.blade.php
@@ -0,0 +1,5 @@
+@hasallroles(['super-admin', 'moderator'], $guard)
+does have all of the given roles
+@else
+does not have all of the given roles
+@endhasallroles