diff --git a/.gitattributes b/.gitattributes
index 6c5391333..9da33a6cd 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,12 +2,16 @@
# 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
-/docs export-ignore
-/.styleci.yml export-ignore
+/.php_cs.dist.php export-ignore
+/phpstan* export-ignore
+/CHANGELOG.md export-ignore
+/CONTRIBUTING.md export-ignore
+
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 000000000..5ccc87cfb
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: spatie
diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yml b/.github/ISSUE_TEMPLATE/1_Bug_report.yml
new file mode 100644
index 000000000..96b2d6571
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yml
@@ -0,0 +1,55 @@
+name: Bug Report
+description: "Report a reproducible bug."
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Before creating a new Bug Report, please check that there isn't already 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).
+ Also, **many issues/questions/problems are already answered** in the [documentation](https://spatie.be/docs/laravel-permission) already. **Please be sure to check the docs** because it will save you time!
+
+ - type: textarea
+ attributes:
+ label: Description
+ description: A clear and concise description of what the bug is.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Steps To Reproduce
+ description: How do you trigger this bug? Please walk us through it step by step.
+ value: |
+ 1.
+ 2.
+ 3.
+ ...
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: Example Application
+ description: "Here is a link to my Github repo containing a minimal Laravel application which shows my problem:"
+ - type: markdown
+ attributes:
+ value: |
+ You can use `composer show` to get package version numbers:
+ - type: input
+ attributes:
+ label: "Version of spatie/laravel-permission package:"
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: "Version of laravel/framework package:"
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: "PHP version:"
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: "Database engine and version:"
+ - type: input
+ attributes:
+ label: "OS: Windows/Mac/Linux version:"
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 000000000..8c6da4eb4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,8 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Feature Request
+ url: https://github.com/spatie/laravel-permission/discussions/new?category=ideas
+ about: Share ideas for new features
+ - name: Ask a Question
+ url: https://github.com/spatie/laravel-permission/discussions/new?category=q-a
+ about: Ask the community for help
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 000000000..a76dd83f9
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,8 @@
+version: 2
+
+updates:
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml
new file mode 100644
index 000000000..c8ac6efa1
--- /dev/null
+++ b/.github/workflows/dependabot-auto-merge.yml
@@ -0,0 +1,40 @@
+name: dependabot-auto-merge
+on: pull_request_target
+
+permissions:
+ pull-requests: write
+ contents: write
+
+jobs:
+ dependabot:
+ runs-on: ubuntu-latest
+ if: ${{ github.actor == 'dependabot[bot]' }}
+ steps:
+
+ - name: Dependabot metadata
+ id: metadata
+ uses: dependabot/fetch-metadata@v2
+ with:
+ github-token: "${{ secrets.GITHUB_TOKEN }}"
+ compat-lookup: true
+
+ - name: Auto-merge Dependabot PRs for semver-minor updates
+ if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor'}}
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+
+ - name: Auto-merge Dependabot PRs for semver-patch updates
+ if: ${{steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
+
+ - name: Auto-merge Dependabot PRs for Action major versions when compatibility is higher than 90%
+ if: ${{steps.metadata.outputs.package-ecosystem == 'github_actions' && steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.compatibility-score >= 90}}
+ run: gh pr merge --auto --merge "$PR_URL"
+ env:
+ PR_URL: ${{github.event.pull_request.html_url}}
+ GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
diff --git a/.github/workflows/fix-php-code-style-issues.yml b/.github/workflows/fix-php-code-style-issues.yml
new file mode 100644
index 000000000..e3be7f171
--- /dev/null
+++ b/.github/workflows/fix-php-code-style-issues.yml
@@ -0,0 +1,24 @@
+name: Fix PHP code style issues
+
+on:
+ push:
+ paths:
+ - '**.php'
+
+jobs:
+ php-code-styling:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+ with:
+ ref: ${{ github.head_ref }}
+
+ - name: Fix PHP code style issues
+ uses: aglipanci/laravel-pint-action@v2
+
+ - name: Commit changes
+ uses: stefanzweifel/git-auto-commit-action@v7
+ with:
+ commit_message: Fix styling
diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml
new file mode 100644
index 000000000..17bbcdf3f
--- /dev/null
+++ b/.github/workflows/phpstan.yml
@@ -0,0 +1,35 @@
+name: PHPStan
+
+on:
+ push:
+ paths:
+ - '**.php'
+ - 'phpstan.neon.dist'
+ pull_request:
+ paths:
+ - '**.php'
+ - 'phpstan.neon.dist'
+
+jobs:
+ phpstan:
+ name: phpstan
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v5
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.4
+ coverage: none
+
+ - name: Install composer dependencies
+ uses: ramsey/composer-install@v3
+
+ - name: Install larastan
+ run: |
+ composer require "larastan/larastan" --no-interaction --no-update
+ composer update --prefer-dist --no-interaction
+
+ - name: Run PHPStan
+ run: ./vendor/bin/phpstan --error-format=github
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 000000000..2dd07ca39
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,71 @@
+name: "Run Tests - Current"
+
+on: [push, pull_request]
+
+jobs:
+ test:
+
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ php: [8.5, 8.4, 8.3, 8.2, 8.1, 8.0]
+ laravel: ["^12.0", "^11.0", "^10.0", "^9.0", "^8.12"]
+ dependency-version: [prefer-lowest, prefer-stable]
+ include:
+ - laravel: "^12.0"
+ testbench: 10.*
+ - laravel: "^11.0"
+ testbench: 9.*
+ - laravel: "^10.0"
+ testbench: 8.*
+ - laravel: "^9.0"
+ testbench: 7.*
+ - laravel: "^8.12"
+ testbench: "^6.23"
+ exclude:
+ - laravel: "^12.0"
+ php: 8.1
+ - laravel: "^12.0"
+ php: 8.0
+ - laravel: "^11.0"
+ php: 8.1
+ - laravel: "^11.0"
+ php: 8.0
+ - laravel: "^10.0"
+ php: 8.0
+ - laravel: "^10.0"
+ php: 8.5
+ - laravel: "^9.0"
+ php: 8.5
+ - laravel: "^8.12"
+ php: 8.3
+ - laravel: "^8.12"
+ php: 8.4
+ - laravel: "^8.12"
+ php: 8.5
+
+ name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv
+ coverage: none
+
+ - name: Install dependencies (remove passport)
+ run: composer remove --dev laravel/passport --no-interaction --no-update
+ if: matrix.laravel == '^8.12'
+
+ - name: Install dependencies
+ run: |
+ composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "symfony/console:>=4.3.4" "mockery/mockery:^1.3.2" "nesbot/carbon:>=2.72.6" --no-interaction --no-update
+ composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
+
+ - name: Execute tests
+ run: vendor/bin/phpunit
diff --git a/.github/workflows/test-cache-drivers.yml b/.github/workflows/test-cache-drivers.yml
new file mode 100644
index 000000000..3b9398c9c
--- /dev/null
+++ b/.github/workflows/test-cache-drivers.yml
@@ -0,0 +1,67 @@
+name: "Run Tests - Cache Drivers"
+
+on: [push, pull_request]
+
+jobs:
+ cache:
+
+ runs-on: ubuntu-latest
+
+ services:
+ redis:
+ image: redis
+ ports:
+ - 6379/tcp
+ options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
+
+ strategy:
+ fail-fast: false
+
+ name: Cache Drivers
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v5
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.4
+ extensions: curl, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, iconv, memcache
+ coverage: none
+
+ - name: Install dependencies
+ run: |
+ composer require "predis/predis" --no-interaction --no-update
+ composer update --prefer-stable --prefer-dist --no-interaction
+
+ - name: Execute tests - memcached cache driver
+ run: |
+ vendor/bin/phpunit
+ env:
+ CACHE_DRIVER: memcached
+
+ - name: Execute tests - redis cache driver
+ run: |
+ vendor/bin/phpunit
+ env:
+ CACHE_DRIVER: redis
+ REDIS_PORT: ${{ job.services.redis.ports['6379'] }}
+
+ - name: Execute tests - database cache driver
+ run: |
+ vendor/bin/phpunit
+ env:
+ CACHE_DRIVER: database
+
+ - name: Execute tests - file cache driver
+ run: |
+ vendor/bin/phpunit
+ env:
+ CACHE_DRIVER: file
+
+ - name: Execute tests - array cache driver
+ run: |
+ vendor/bin/phpunit
+ env:
+ CACHE_DRIVER: array
diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml
new file mode 100644
index 000000000..cb7fa9f8b
--- /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@v5
+ 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@v7
+ with:
+ branch: main
+ commit_message: Update CHANGELOG
+ file_pattern: CHANGELOG.md
diff --git a/.gitignore b/.gitignore
index 3a32e03ea..da248ba1f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,7 @@ composer.lock
vendor
tests/temp
.idea
+.phpunit.cache
+.phpunit.result.cache
+.php-cs-fixer.cache
+tests/CreatePermissionCustomTables.php
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
deleted file mode 100644
index df16b68b5..000000000
--- a/.scrutinizer.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-filter:
- excluded_paths: [tests/*]
-
-checks:
- php:
- remove_extra_empty_lines: true
- remove_php_closing_tag: true
- remove_trailing_whitespace: true
- fix_use_statements:
- remove_unused: true
- preserve_multiple: false
- preserve_blanklines: true
- order_alphabetically: true
- fix_php_opening_tag: true
- fix_linefeed: true
- fix_line_ending: true
- fix_identation_4spaces: true
- fix_doc_comments: true
-
diff --git a/.styleci.yml b/.styleci.yml
deleted file mode 100644
index 0285f1790..000000000
--- a/.styleci.yml
+++ /dev/null
@@ -1 +0,0 @@
-preset: laravel
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index b23d91dca..000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,23 +0,0 @@
-language: php
-
-php:
- - 7.1
- - 7.2
- - 7.3
- - nightly
-
-matrix:
- include:
- - php: 7.0
- env: COMPOSER_FLAGS="--prefer-lowest"
- - php: 7.1
- env: COMPOSER_FLAGS="--prefer-lowest"
- allow_failures:
- - php: nightly
-
-before_script:
- - travis_retry composer self-update
- - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source
-
-script:
- - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52a5e018f..b76054f36 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,335 +2,1477 @@
All notable changes to `laravel-permission` will be documented in this file
+## 6.21.0 - 2025-07-23
+
+### What's Changed
+
+* Allow removing multiple roles with the `removeRole` method by @TobMoeller in https://github.com/spatie/laravel-permission/pull/2859
+* [Docs] Correct middleware order for documentation example in `teams-permissions.md` by @dualklip in https://github.com/spatie/laravel-permission/pull/2863
+
+### New Contributors
+
+* @dualklip made their first contribution in https://github.com/spatie/laravel-permission/pull/2863
+* @TobMoeller made their first contribution in https://github.com/spatie/laravel-permission/pull/2859
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.20.0...6.21.0
+
+## 6.20.0 - 2025-06-14
+
+### What's Changed
+
+* Add translations support for exception messages by @nAa6666 in https://github.com/spatie/laravel-permission/pull/2852
+
+### New Contributors
+
+* @nAa6666 made their first contribution in https://github.com/spatie/laravel-permission/pull/2852
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.19.0...6.20.0
+
+## 6.19.0 - 2025-05-31
+
+### What's Changed
+
+* Revert "Remove `collectPermissions` that is not being assigned" by @erikn69 in https://github.com/spatie/laravel-permission/pull/2851
+* Fix guard_name not used to set default attribute in Role and Permission model by @Ken-vdE in https://github.com/spatie/laravel-permission/pull/2837
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.18.0...6.19.0
+
+## 6.18.0 - 2025-05-14
+
+### What's Changed
+
+* refactor exception throwing in migration file to use throw_if() by @ccaioadriano in https://github.com/spatie/laravel-permission/pull/2819
+* Fix: Example in config comment includes `permission.` prefix on `wildcard_permission` key by @jerrens in https://github.com/spatie/laravel-permission/pull/2835
+* Fix commented config key typo by @erikn69 in https://github.com/spatie/laravel-permission/pull/2844
+* Remove `collectPermissions` that is not being assigned by @JHWelch in https://github.com/spatie/laravel-permission/pull/2840
+* [Docs] Update multiple-guards.md by @Ken-vdE in https://github.com/spatie/laravel-permission/pull/2836
+* [Docs] Remove extra period by @coreyhn in https://github.com/spatie/laravel-permission/pull/2841
+* Add JetAdmin as UI Option. by @aliqasemzadeh in https://github.com/spatie/laravel-permission/pull/2814
+
+### New Contributors
+
+* @ccaioadriano made their first contribution in https://github.com/spatie/laravel-permission/pull/2819
+* @coreyhn made their first contribution in https://github.com/spatie/laravel-permission/pull/2841
+* @jerrens made their first contribution in https://github.com/spatie/laravel-permission/pull/2835
+* @Ken-vdE made their first contribution in https://github.com/spatie/laravel-permission/pull/2836
+* @JHWelch made their first contribution in https://github.com/spatie/laravel-permission/pull/2840
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.17.0...6.18.0
+
+## 6.17.0 - 2025-04-09
+
+### What's Changed
+
+* Route macro functions: add backed enum support by @Yi-pixel in https://github.com/spatie/laravel-permission/pull/2823
+
+### New Contributors
+
+* @Yi-pixel made their first contribution in https://github.com/spatie/laravel-permission/pull/2823
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.16.0...6.17.0
+
+## 6.16.0 - 2025-02-28
+
+### What's Changed
+
+* Middleware: support enums in role/permission middleware by @marklawntalk in https://github.com/spatie/laravel-permission/pull/2813
+
+### New Contributors
+
+* @marklawntalk made their first contribution in https://github.com/spatie/laravel-permission/pull/2813
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.15.0...6.16.0
+
+## 6.15.0 - 2025-02-17
+
+### What's Changed
+
+* Added 4 events for adding and removing roles or permissions by @sven-wegner in https://github.com/spatie/laravel-permission/pull/2742
+* Fixed bug of loading user roles of different teams to current team by @mohamedds-12 in https://github.com/spatie/laravel-permission/pull/2803
+
+### New Contributors
+
+* @sven-wegner made their first contribution in https://github.com/spatie/laravel-permission/pull/2742
+* @mohamedds-12 made their first contribution in https://github.com/spatie/laravel-permission/pull/2803
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.14.0...6.15.0
+
+## 6.14.0 - 2025-02-13
+
+### What's Changed
+
+* LDAP model lookup from Auth Provider by @crossplatformconsulting in https://github.com/spatie/laravel-permission/pull/2750
+
+### Internals
+
+* Add PHPUnit annotations, for future compatibility with PHPUnit 12 by @drbyte in https://github.com/spatie/laravel-permission/pull/2806
+
+### New Contributors
+
+* @crossplatformconsulting made their first contribution in https://github.com/spatie/laravel-permission/pull/2750
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.13.0...6.14.0
+
+## 6.13.0 - 2025-02-05
+
+### What's Changed
+
+* LazyLoading: Explicitly call `loadMissing('permissions')` when the relation is needed, and test with `Model::preventLazyLoading()` by @erikn69 in https://github.com/spatie/laravel-permission/pull/2776
+* [Docs] Add instructions to reinitialize cache for multi-tenancy key settings when updating multiple tenants in a single request cycle, by @sudkumar in https://github.com/spatie/laravel-permission/pull/2804
+
+### New Contributors
+
+* @sudkumar made their first contribution in https://github.com/spatie/laravel-permission/pull/2804
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.12.0...6.13.0
+
+## 6.12.0 - 2025-01-31
+
+### What's Changed
+
+* Support Laravel 12
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.11.0...6.12.0
+
+## 6.11.0 - 2025-01-30
+
+### What's Changed
+
+* Add configurable team resolver for permission team id (helpful for Jetstream, etc) by @adrenallen in https://github.com/spatie/laravel-permission/pull/2790
+
+### Internals
+
+* Replace php-cs-fixer with Laravel Pint by @bobbrodie in https://github.com/spatie/laravel-permission/pull/2780
+
+### Documentation Updates
+
+* [Docs] Include namespace in example in uuid.md by @ken-tam in https://github.com/spatie/laravel-permission/pull/2764
+* [Docs] Include Laravel 11 example in exceptions.md by @frankliniwobi in https://github.com/spatie/laravel-permission/pull/2768
+* [Docs] Fix typo in code example in passport.md by @m3skalina in https://github.com/spatie/laravel-permission/pull/2782
+* [Docs] Correct username in new-app.md by @trippodi in https://github.com/spatie/laravel-permission/pull/2785
+* [Docs] Add composer specificity by @imanghafoori1 in https://github.com/spatie/laravel-permission/pull/2772
+* [Docs] Update installation-laravel.md to fix providers.php location. by @curiousteam in https://github.com/spatie/laravel-permission/pull/2796
+
+### New Contributors
+
+* @ken-tam made their first contribution in https://github.com/spatie/laravel-permission/pull/2764
+* @frankliniwobi made their first contribution in https://github.com/spatie/laravel-permission/pull/2768
+* @bobbrodie made their first contribution in https://github.com/spatie/laravel-permission/pull/2780
+* @m3skalina made their first contribution in https://github.com/spatie/laravel-permission/pull/2782
+* @trippodi made their first contribution in https://github.com/spatie/laravel-permission/pull/2785
+* @imanghafoori1 made their first contribution in https://github.com/spatie/laravel-permission/pull/2772
+* @curiousteam made their first contribution in https://github.com/spatie/laravel-permission/pull/2796
+* @adrenallen made their first contribution in https://github.com/spatie/laravel-permission/pull/2790
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.10.1...6.11.0
+
+## 6.10.1 - 2024-11-08
+
+### What's Changed
+
+* Fix #2749 regression bug in `6.10.0` : "Can no longer delete permissions" by @erikn69 in https://github.com/spatie/laravel-permission/pull/2759
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.10.0...6.10.1
+
+## 6.10.0 - 2024-11-05
+
+### What's Changed
+
+* Fix `GuardDoesNotMatch should accept collection` by @erikn69 in https://github.com/spatie/laravel-permission/pull/2748
+* Improve performance for hydrated collections by @inserve-paul in https://github.com/spatie/laravel-permission/pull/2749
+* Only show error if `cache key exists` and `forgetCachedPermissions` fails by @erikn69 in https://github.com/spatie/laravel-permission/pull/2707
+* Remove v5 cache fallback alias by @drbyte in https://github.com/spatie/laravel-permission/pull/2754
+* Include `Larastan` in `dev` by @drbyte in https://github.com/spatie/laravel-permission/pull/2755
+
+#### Docs
+
+* [Docs example] Check for 'all' or 'any' permissions before specific permissions by @ceilidhboy in https://github.com/spatie/laravel-permission/pull/2694
+* [Docs] Fix typo in uuid.md by @levizoesch in https://github.com/spatie/laravel-permission/pull/2705
+* [Docs] Upgrade Guide - Add PR links to upgrade guide by @mraheelkhan in https://github.com/spatie/laravel-permission/pull/2716
+* [Docs] use more modern syntax for nullable return type by @galangaidilakbar in https://github.com/spatie/laravel-permission/pull/2719
+* [Docs] camelCase variable naming in example by @KamilWojtalak in https://github.com/spatie/laravel-permission/pull/2723
+* [Docs] Update using-policies.md by @marcleonhard in https://github.com/spatie/laravel-permission/pull/2741
+* [Docs] Example of pushing custom middleware before SubstituteBindings middleware by @WyattCast44 in https://github.com/spatie/laravel-permission/pull/2740
+
+#### Other
+
+* PHP 8.4 tests by @erikn69 in https://github.com/spatie/laravel-permission/pull/2747
+* Fix comment typo by @machacekmartin in https://github.com/spatie/laravel-permission/pull/2753
+
+### New Contributors
+
+* @ceilidhboy made their first contribution in https://github.com/spatie/laravel-permission/pull/2694
+* @levizoesch made their first contribution in https://github.com/spatie/laravel-permission/pull/2705
+* @galangaidilakbar made their first contribution in https://github.com/spatie/laravel-permission/pull/2719
+* @KamilWojtalak made their first contribution in https://github.com/spatie/laravel-permission/pull/2723
+* @marcleonhard made their first contribution in https://github.com/spatie/laravel-permission/pull/2741
+* @WyattCast44 made their first contribution in https://github.com/spatie/laravel-permission/pull/2740
+* @inserve-paul made their first contribution in https://github.com/spatie/laravel-permission/pull/2749
+* @machacekmartin made their first contribution in https://github.com/spatie/laravel-permission/pull/2753
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.9.0...6.10.0
+
+## 6.9.0 - 2024-06-22
+
+### What's Changed
+
+* Use `->withPivot()` for teamed relationships (allows `getPivotColumns()`) by @juliangums in https://github.com/spatie/laravel-permission/pull/2679
+* Update docblock on `$role->hasPermissionTo()` to include `BackedEnum` by @drbyte co-authored by @SanderMuller
+* [Docs] Clarify that `$guard_name` can be an array by @angelej in https://github.com/spatie/laravel-permission/pull/2659
+* Fix misc typos in changelog by @szepeviktor in https://github.com/spatie/laravel-permission/pull/2686
+
+### New Contributors
+
+* @angelej made their first contribution in https://github.com/spatie/laravel-permission/pull/2659
+* @SanderMuller made their first contribution in #2676
+* @szepeviktor made their first contribution in https://github.com/spatie/laravel-permission/pull/2686
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.8.0...6.9.0
+
+## 6.8.0 - 2024-06-21
+
+### What's Changed
+
+* Fix can't save the same model twice by @erikn69 in https://github.com/spatie/laravel-permission/pull/2658
+* Fix phpstan from #2616 by @erikn69 in https://github.com/spatie/laravel-permission/pull/2685
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.7.0...6.8.0
+
+## 6.7.0 - 2024-04-19
+
+### What's Changed
+
+- Fixed remaining Octane event contract. Update to #2656 in release `6.5.0`
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.6.0...6.7.0
+
+## 6.6.0 - 2024-04-19
+
+### What's Changed
+
+* Roles: Support for casting role names to enums by @gajosadrian in https://github.com/spatie/laravel-permission/pull/2616
+* Fix permission:show UUID error #2581 by @drbyte in https://github.com/spatie/laravel-permission/pull/2582
+* Cover WildcardPermission instance verification based on its own guard (Allow hasAllPermissions and hasAnyPermission to run on custom guard for WildcardPermission) by @AlexandreBellas in https://github.com/spatie/laravel-permission/pull/2608
+* Register Laravel "About" details by @drbyte in https://github.com/spatie/laravel-permission/pull/2584
+
+### New Contributors
+
+* @gajosadrian made their first contribution in https://github.com/spatie/laravel-permission/pull/2616
+* @AlexandreBellas made their first contribution in https://github.com/spatie/laravel-permission/pull/2608
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.5.0...6.6.0
+
+## 6.5.0 - 2024-04-18
+
+### What's Changed
+
+* Octane: Fix wrong event listener by @erikn69 in https://github.com/spatie/laravel-permission/pull/2656
+* Teams: Add nullable team_id by @Androlax2 in https://github.com/spatie/laravel-permission/pull/2607
+* Blade: simplify the definition of multiple Blade "if" directives by @alissn in https://github.com/spatie/laravel-permission/pull/2628
+* DocBlocks: Update HasPermissions::collectPermissions() docblock by @Plytas in https://github.com/spatie/laravel-permission/pull/2641
+
+#### Internals
+
+* Update role-permissions.md by @killjin in https://github.com/spatie/laravel-permission/pull/2631
+* Bump ramsey/composer-install from 2 to 3 by @dependabot in https://github.com/spatie/laravel-permission/pull/2630
+* Bump dependabot/fetch-metadata from 1 to 2 by @dependabot in https://github.com/spatie/laravel-permission/pull/2642
+
+### New Contributors
+
+* @alissn made their first contribution in https://github.com/spatie/laravel-permission/pull/2628
+* @Androlax2 made their first contribution in https://github.com/spatie/laravel-permission/pull/2607
+* @Plytas made their first contribution in https://github.com/spatie/laravel-permission/pull/2641
+* @killjin made their first contribution in https://github.com/spatie/laravel-permission/pull/2631
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.4.0...6.5.0
+
+## 6.4.0 - 2024-02-28
+
+* Laravel 11 Support
+
+### What's Changed
+
+* Add Laravel 11 to workflow run tests by @mraheelkhan in https://github.com/spatie/laravel-permission/pull/2605
+* And Passport 12
+
+### Internals
+
+* Update to use Larastan Org by @arnebr in https://github.com/spatie/laravel-permission/pull/2585
+* laravel-pint-action to major version tag by @erikn69 in https://github.com/spatie/laravel-permission/pull/2586
+
+### New Contributors
+
+* @arnebr made their first contribution in https://github.com/spatie/laravel-permission/pull/2585
+* @mraheelkhan made their first contribution in https://github.com/spatie/laravel-permission/pull/2605
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.3.0...6.4.0
+
+## 6.3.0 - 2023-12-24
+
+### What's Changed
+
+* Octane Fix: Clear wildcard permissions on Tick in https://github.com/spatie/laravel-permission/pull/2583
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.2.0...6.3.0
+
+## 6.2.0 - 2023-12-09
+
+### What's Changed
+
+* Skip duplicates on sync (was triggering Integrity Constraint Violation error) by @erikn69 in https://github.com/spatie/laravel-permission/pull/2574
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.1.0...6.2.0
+
+## 6.1.0 - 2023-11-09
+
+### What's Changed
+
+- Reset teamId on Octane by @erikn69 in https://github.com/spatie/laravel-permission/pull/2547
+ NOTE: The `\Spatie\Permission\Listeners\OctaneReloadPermissions` listener introduced in 6.0.0 is removed in 6.1.0, because the logic is directly incorporated into the ServiceProvider now.
+
+ Thanks @jameshulse for the heads-up and code-review.
+
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.0.1...6.1.0
+
+## 6.0.1 - 2023-11-06
+
+### What's Changed
+
+- Provide a default team_foreign_key value in case config file isn't upgraded yet or teams feature is unused. Fixes #2535
+- [Docs] Update unsetRelation() example in teams-permissions.md by @shdehnavi in https://github.com/spatie/laravel-permission/pull/2534
+- [Docs] Update link in direct-permissions.md by @sevannerse in https://github.com/spatie/laravel-permission/pull/2539
+
+### New Contributors
+
+- @sevannerse made their first contribution in https://github.com/spatie/laravel-permission/pull/2539
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/6.0.0...6.0.1
+
+## 6.0.0 - 2023-10-25
+
+### What's Changed
+
+- Full uuid/guid/ulid support by @erikn69 in https://github.com/spatie/laravel-permission/pull/2089
+- Refactor: Change static properties to non-static by @olivernybroe in https://github.com/spatie/laravel-permission/pull/2324
+- Fix Role::withCount if belongsToMany declared by @xenaio-daniil in https://github.com/spatie/laravel-permission/pull/2280
+- Fix: Lazily bind dependencies by @olivernybroe in https://github.com/spatie/laravel-permission/pull/2321
+- Avoid loss of all permissions/roles pivots on sync error by @erikn69 in https://github.com/spatie/laravel-permission/pull/2341
+- Fix delete permissions on Permissions Model by @erikn69 in https://github.com/spatie/laravel-permission/pull/2366
+- Detach users on role/permission physical deletion by @erikn69 in https://github.com/spatie/laravel-permission/pull/2370
+- Rename clearClassPermissions method to clearPermissionsCollection by @erikn69 in https://github.com/spatie/laravel-permission/pull/2369
+- Use anonymous migrations (for L8+) by @erikn69 in https://github.com/spatie/laravel-permission/pull/2374
+- [BC] Return string on getPermissionClass(), getRoleClass() by @erikn69 in https://github.com/spatie/laravel-permission/pull/2368
+- Only offer publishing when running in console by @erikn69 in https://github.com/spatie/laravel-permission/pull/2377
+- Don't add commands in web interface context by @angeljqv in https://github.com/spatie/laravel-permission/pull/2405
+- [BC] Fix Role->hasPermissionTo() signature to match HasPermissions trait by @erikn69 in https://github.com/spatie/laravel-permission/pull/2380
+- Force that getPermissionsViaRoles, hasPermissionViaRole must be used only by authenticable by @erikn69 in https://github.com/spatie/laravel-permission/pull/2382
+- fix BadMethodCallException: undefined methods hasAnyRole, hasAnyPermissions by @erikn69 in https://github.com/spatie/laravel-permission/pull/2381
+- Add PHPStan workflow with fixes by @erikn69 in https://github.com/spatie/laravel-permission/pull/2376
+- Add BackedEnum support by @drbyte in https://github.com/spatie/laravel-permission/pull/2391
+- Drop PHP 7.3 support by @angeljqv in https://github.com/spatie/laravel-permission/pull/2388
+- Drop PHP 7.4 support by @drbyte in https://github.com/spatie/laravel-permission/pull/2485
+- Test against PHP 8.3 by @erikn69 in https://github.com/spatie/laravel-permission/pull/2512
+- Fix call to an undefined method Role::getRoleClass by @erikn69 in https://github.com/spatie/laravel-permission/pull/2411
+- Remove force loading model relationships by @erikn69 in https://github.com/spatie/laravel-permission/pull/2412
+- Test alternate cache drivers by @erikn69 in https://github.com/spatie/laravel-permission/pull/2416
+- Use attach instead of sync on traits by @erikn69 in https://github.com/spatie/laravel-permission/pull/2420
+- Fewer sqls in syncRoles, syncPermissions by @erikn69 in https://github.com/spatie/laravel-permission/pull/2423
+- Add middleware using static method by @jnoordsij in https://github.com/spatie/laravel-permission/pull/2424
+- Update PHPDocs for IDE autocompletion by @erikn69 in https://github.com/spatie/laravel-permission/pull/2437
+- [BC] Wildcard permissions algorithm performance improvements (ALERT: Breaking Changes) by @danharrin in https://github.com/spatie/laravel-permission/pull/2445
+- Add withoutRole and withoutPermission scopes by @drbyte in https://github.com/spatie/laravel-permission/pull/2463
+- Add support for service-to-service Passport client by @SuperDJ in https://github.com/spatie/laravel-permission/pull/2467
+- Register OctaneReloadPermissions listener for Laravel Octane by @erikn69 in https://github.com/spatie/laravel-permission/pull/2403
+- Add guard name to exceptions by @drbyte in https://github.com/spatie/laravel-permission/pull/2481
+- Update contracts to allow for UUID by @drbyte in https://github.com/spatie/laravel-permission/pull/2480
+- Avoid triggering eloquent.retrieved event by @erikn69 in https://github.com/spatie/laravel-permission/pull/2498
+- [BC] Rename "Middlewares" namespace to "Middleware" by @drbyte in https://github.com/spatie/laravel-permission/pull/2499
+- `@haspermission` directive by @axlwild in https://github.com/spatie/laravel-permission/pull/2515
+- Add guard parameter to can() by @drbyte in https://github.com/spatie/laravel-permission/pull/2526
+
+### New Contributors
+
+- @xenaio-daniil made their first contribution in https://github.com/spatie/laravel-permission/pull/2280
+- @JensvandeWiel made their first contribution in https://github.com/spatie/laravel-permission/pull/2336
+- @fsamapoor made their first contribution in https://github.com/spatie/laravel-permission/pull/2361
+- @yungifez made their first contribution in https://github.com/spatie/laravel-permission/pull/2394
+- @HasanEksi made their first contribution in https://github.com/spatie/laravel-permission/pull/2418
+- @jnoordsij made their first contribution in https://github.com/spatie/laravel-permission/pull/2424
+- @danharrin made their first contribution in https://github.com/spatie/laravel-permission/pull/2445
+- @SuperDJ made their first contribution in https://github.com/spatie/laravel-permission/pull/2467
+- @ChillMouse made their first contribution in https://github.com/spatie/laravel-permission/pull/2438
+- @Okipa made their first contribution in https://github.com/spatie/laravel-permission/pull/2492
+- @edalzell made their first contribution in https://github.com/spatie/laravel-permission/pull/2494
+- @sirosfakhri made their first contribution in https://github.com/spatie/laravel-permission/pull/2501
+- @juliangums made their first contribution in https://github.com/spatie/laravel-permission/pull/2516
+- @nnnnnnnngu made their first contribution in https://github.com/spatie/laravel-permission/pull/2524
+- @axlwild made their first contribution in https://github.com/spatie/laravel-permission/pull/2515
+- @shdehnavi made their first contribution in https://github.com/spatie/laravel-permission/pull/2527
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.11.1...6.0.0
+
+## 5.11.1 - 2023-10-25
+
+No functional changes. Just several small updates to the Documentation.
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.11.0...5.11.1
+
+## 5.11.0 - 2023-08-30
+
+### What's Changed
+
+- [V5] Avoid triggering `eloquent.retrieved` event by @erikn69 in https://github.com/spatie/laravel-permission/pull/2490
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.10.2...5.11.0
+
+## 5.10.2 - 2023-07-04
+
+### What's Changed
+
+- Fix Eloquent Strictness on `permission:show` Command by @erikn69 in https://github.com/spatie/laravel-permission/pull/2457
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.10.1...5.10.2
+
+## 5.10.1 - 2023-04-12
+
+### What's Changed
+
+- [V5] Fix artisan command `permission:show` output of roles with underscores by @erikn69 in https://github.com/spatie/laravel-permission/pull/2396
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.10.0...5.10.1
+
+## 5.10.0 - 2023-03-22
+
+### What's Changed
+
+- Fix delete permissions on Permissions Model by @erikn69 in https://github.com/spatie/laravel-permission/pull/2366
+
+## 5.9.1 - 2023-02-06
+
+Apologies for the break caused by 5.9.0 !
+
+### Reverted Lazy binding of dependencies.
+
+- Revert "fix: Lazily bind dependencies", originally #2309
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.9.0...5.9.1
+
+## 5.9.0 - 2023-02-06
+
+### What's Changed
+
+- Add `permission-` prefix to publish tag names by @sedehi in https://github.com/spatie/laravel-permission/pull/2301
+- Fix detaching user models on teams feature #2220 by @erikn69 in https://github.com/spatie/laravel-permission/pull/2221
+- Hint model properties by @AJenbo in https://github.com/spatie/laravel-permission/pull/2230
+- Custom wildcard verification/separators support by @erikn69 in https://github.com/spatie/laravel-permission/pull/2252
+- fix: Lazily bind dependencies by @olivernybroe in https://github.com/spatie/laravel-permission/pull/2309
+- Extract query to `getPermissionsWithRoles` method. by @xiCO2k in https://github.com/spatie/laravel-permission/pull/2316
+- This will allow to extend the PermissionRegistrar class and change the query.
+
+### New Contributors
+
+- @sedehi made their first contribution in https://github.com/spatie/laravel-permission/pull/2301
+- @parallels999 made their first contribution in https://github.com/spatie/laravel-permission/pull/2265
+- @AJenbo made their first contribution in https://github.com/spatie/laravel-permission/pull/2230
+- @olivernybroe made their first contribution in https://github.com/spatie/laravel-permission/pull/2309
+- @xiCO2k made their first contribution in https://github.com/spatie/laravel-permission/pull/2316
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.8.0...5.9.0
+
+## 5.8.0 - 2023-01-14
+
+### What's Changed
+
+- Laravel 10.x Support by @erikn69 in https://github.com/spatie/laravel-permission/pull/2298
+
+#### Administrative
+
+- [Docs] Link updated to match name change of related tool repo by @aliqasemzadeh in https://github.com/spatie/laravel-permission/pull/2253
+- Fix tests badge by @erikn69 in https://github.com/spatie/laravel-permission/pull/2300
+- Add Laravel Pint Support by @patinthehat in https://github.com/spatie/laravel-permission/pull/2269
+- Normalize composer.json by @patinthehat in https://github.com/spatie/laravel-permission/pull/2259
+- Add Dependabot Automation by @patinthehat in https://github.com/spatie/laravel-permission/pull/2257
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.7.0...5.8.0
+
+## 5.7.0 - 2022-11-23
+
+### What's Changed
+
+- [Bugfix] Avoid checking permissions-via-roles on `Role` model (ref `Model::preventAccessingMissingAttributes()`) by @juliomotol in https://github.com/spatie/laravel-permission/pull/2227
+
+### New Contributors
+
+- @juliomotol made their first contribution in https://github.com/spatie/laravel-permission/pull/2227
+
+**Full Changelog**: https://github.com/spatie/laravel-permission/compare/5.6.0...5.7.0
+
+## 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 instead 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
+
+## 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.
+
+NOTE: If you leave your existing `config/permission.php` file alone, then with Laravel 5.8 the `60 * 24` will change from being treated as 24 hours to just 24 minutes. Depending on your app, this may or may not make a significant difference. Updating your config file to a specific DateInterval will add specificity and insulate you from the TTL change in Laravel 5.8.
+
+Refs:
+
+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
+- allowed `givePermissionTo` to accept multiple permissions
- allowed `assignRole` to accept multiple roles
- added `syncPermissions`-method
- added `syncRoles`-method
- 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
@@ -355,25 +1497,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 36921dc49..413063953 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,24 @@
-# Associate users with permissions and roles
-
-
-### Sponsor
-
-
-
-
-
If you want to quickly add authentication and authorization to Laravel projects, feel free to check Auth0's Laravel SDK and free plan at https://auth0.com/overview.
[](https://packagist.org/packages/spatie/laravel-permission)
-[](https://travis-ci.org/spatie/laravel-permission)
-[](https://styleci.io/repos/42480275)
+[](https://github.com/spatie/laravel-permission/actions?query=workflow%3ATests+branch%3Amain)
[](https://packagist.org/packages/spatie/laravel-permission)
+
+
-* [Installation](#installation)
-* [Usage](#usage)
- * [Using "direct" permissions](#using-direct-permissions-see-below-to-use-both-roles-and-permissions)
- * [Using permissions via roles](#using-permissions-via-roles)
- * [Using Blade directives](#using-blade-directives)
- * [Defining a Super-Admin](#defining-a-super-admin)
- * [Best Practices -- roles vs permissions](#best-practices----roles-vs-permissions)
- * [Using multiple guards](#using-multiple-guards)
- * [Using a middleware](#using-a-middleware)
- * [Using artisan commands](#using-artisan-commands)
-* [Unit Testing](#unit-testing)
-* [Database Seeding](#database-seeding)
-* [Extending](#extending)
-* [Cache](#cache)
+## Documentation, 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.
Once installed you can do stuff like this:
@@ -45,889 +33,27 @@ $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](#using-multiple-guards) section of the readme.
-
-Because all permissions will be registered on [Laravel's gate](https://laravel.com/docs/5.5/authorization), you can check if a user has a permission with Laravel's default `can` function:
-
-```php
-$user->can('edit articles');
-```
-
-Spatie is a web design agency in Antwerp, Belgium. You'll find an overview of all
-our open source projects [on our website](https://spatie.be/opensource).
-
-## Installation
-
-- [Laravel](#laravel)
-- [Lumen](#lumen)
-
-### Laravel
-
-This package can be used in Laravel 5.4 or higher. If you are using an older version of Laravel, take a look at [the v1 branch of this package](https://github.com/spatie/laravel-permission/tree/v1).
-
-You can install the package via composer:
-
-``` bash
-composer require spatie/laravel-permission
-```
-
-In Laravel 5.5 the service provider will automatically get registered. In older versions of the framework just add the service provider in `config/app.php` file:
-
-```php
-'providers' => [
- // ...
- Spatie\Permission\PermissionServiceProvider::class,
-];
-```
-
-You can publish [the migration](https://github.com/spatie/laravel-permission/blob/master/database/migrations/create_permission_tables.php.stub) with:
-
-```bash
-php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="migrations"
-```
-
-If you're using UUIDs or GUIDs for your `User` models you can update the `create_permission_tables.php` migration and replace `$table->unsignedBigInteger($columnNames['model_morph_key'])` with `$table->uuid($columnNames['model_morph_key'])`.
-For consistency, you can also update the package configuration file to use the `model_uuid` column name instead of the default `model_id` column.
-
-After the migration has been published you can create the role- and permission-tables by running the migrations:
-
-```bash
-php artisan migrate
-```
-
-You can publish the config file with:
-
-```bash
-php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config"
-```
-
-When published, [the `config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/master/config/permission.php) contains:
-
-```php
-return [
-
- 'models' => [
-
- /*
- * When using the "HasPermissions" trait from this package, we need to know which
- * Eloquent model should be used to retrieve your permissions. Of course, it
- * is often just the "Permission" model but you may use whatever you like.
- *
- * The model you want to use as a Permission model needs to implement the
- * `Spatie\Permission\Contracts\Permission` contract.
- */
-
- 'permission' => Spatie\Permission\Models\Permission::class,
-
- /*
- * When using the "HasRoles" trait from this package, we need to know which
- * Eloquent model should be used to retrieve your roles. Of course, it
- * is often just the "Role" model but you may use whatever you like.
- *
- * The model you want to use as a Role model needs to implement the
- * `Spatie\Permission\Contracts\Role` contract.
- */
-
- 'role' => Spatie\Permission\Models\Role::class,
-
- ],
-
- 'table_names' => [
-
- /*
- * When using the "HasRoles" trait from this package, we need to know which
- * table should be used to retrieve your roles. We have chosen a basic
- * default value but you may easily change it to any table you like.
- */
-
- 'roles' => 'roles',
-
- /*
- * When using the "HasPermissions" trait from this package, we need to know which
- * table should be used to retrieve your permissions. We have chosen a basic
- * default value but you may easily change it to any table you like.
- */
-
- 'permissions' => 'permissions',
-
- /*
- * When using the "HasPermissions" trait from this package, we need to know which
- * table should be used to retrieve your models permissions. We have chosen a
- * basic default value but you may easily change it to any table you like.
- */
-
- 'model_has_permissions' => 'model_has_permissions',
-
- /*
- * When using the "HasRoles" trait from this package, we need to know which
- * table should be used to retrieve your models roles. We have chosen a
- * basic default value but you may easily change it to any table you like.
- */
-
- 'model_has_roles' => 'model_has_roles',
-
- /*
- * When using the "HasRoles" trait from this package, we need to know which
- * table should be used to retrieve your roles permissions. We have chosen a
- * basic default value but you may easily change it to any table you like.
- */
-
- 'role_has_permissions' => 'role_has_permissions',
- ],
-
- 'column_names' => [
-
- /*
- * Change this if you want to name the related model primary key other than
- * `model_id`.
- *
- * For example, this would be nice if your primary keys are all UUIDs. In
- * that case, name this `model_uuid`.
- */
- 'model_morph_key' => 'model_id',
- ],
-
- /*
- * When set to true, the required permission/role names are added to the exception
- * message. This could be considered an information leak in some contexts, so
- * the default setting is false here for optimum safety.
- */
-
- 'display_permission_in_exception' => false,
-
- 'cache' => [
-
- /*
- * By default all permissions will be cached for 24 hours unless a permission or
- * role is updated. Then the cache will be flushed immediately.
- */
-
- 'expiration_time' => 60 * 24,
-
- /*
- * The key to use when tagging and prefixing entries in the cache.
- */
-
- '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
- * file. Using 'default' here means to use the `default` set in cache.php.
- */
- 'store' => 'default',
- ],
-];
-```
-
-### Lumen
-
-You can install the package via Composer:
-
-``` bash
-composer require spatie/laravel-permission
-```
-
-Copy the required files:
-
-```bash
-mkdir -p config
-cp vendor/spatie/laravel-permission/config/permission.php config/permission.php
-cp vendor/spatie/laravel-permission/database/migrations/create_permission_tables.php.stub database/migrations/2018_01_01_000000_create_permission_tables.php
-```
-
-You will also need to create another configuration file at `config/auth.php`. Get it on the Laravel repository or just run the following command:
-
-```bash
-curl -Ls https://raw.githubusercontent.com/laravel/lumen-framework/5.7/config/auth.php -o config/auth.php
-```
-
-Then, in `bootstrap/app.php`, register the middlewares:
-
-```php
-$app->routeMiddleware([
- 'auth' => App\Http\Middleware\Authenticate::class,
- 'permission' => Spatie\Permission\Middlewares\PermissionMiddleware::class,
- 'role' => Spatie\Permission\Middlewares\RoleMiddleware::class,
-]);
-```
-
-As well as the config file, service provider, and cache alias:
-
-```php
-$app->configure('permission');
-$app->alias('cache', \Illuminate\Cache\CacheManager::class); // if you don't have this already
-$app->register(Spatie\Permission\PermissionServiceProvider::class);
-```
-
-Now, run your migrations:
-
-```bash
-php artisan migrate
-```
-
-## Usage
-
-First, add the `Spatie\Permission\Traits\HasRoles` trait to your `User` model(s):
-
-```php
-use Illuminate\Foundation\Auth\User as Authenticatable;
-use Spatie\Permission\Traits\HasRoles;
-
-class User extends Authenticatable
-{
- use HasRoles;
-
- // ...
-}
-```
-
-> - note that if you need to use `HasRoles` trait with another model ex.`Page` you will also need to add `protected $guard_name = 'web';` as well to that model or you would get an error
->
->```php
->use Illuminate\Database\Eloquent\Model;
->use Spatie\Permission\Traits\HasRoles;
->
->class Page extends Model
->{
-> use HasRoles;
->
-> protected $guard_name = 'web'; // or whatever guard you want to use
->
-> // ...
->}
->```
-
-This package allows for users to be associated with permissions and roles. Every role is associated with multiple permissions.
-A `Role` and a `Permission` are regular Eloquent models. They require a `name` and can be created like this:
-
-```php
-use Spatie\Permission\Models\Role;
-use Spatie\Permission\Models\Permission;
-
-$role = Role::create(['name' => 'writer']);
-$permission = Permission::create(['name' => 'edit articles']);
-```
-
-
-A permission can be assigned to a role using 1 of these methods:
-
-```php
-$role->givePermissionTo($permission);
-$permission->assignRole($role);
-```
-
-Multiple permissions can be synced to a role using 1 of these methods:
-
-```php
-$role->syncPermissions($permissions);
-$permission->syncRoles($roles);
-```
-
-A permission can be removed from a role using 1 of these methods:
-
-```php
-$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](#using-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:
-
-```php
-// get a list of all permissions directly assigned to the user
-$permissions = $user->permissions;
-
-// get all permissions for the user, either directly, or from roles, or from both
-$permissions = $user->getDirectPermissions();
-$permissions = $user->getPermissionsViaRoles();
-$permissions = $user->getAllPermissions();
-
-// get the names of the user's roles
-$roles = $user->getRoleNames(); // Returns a collection
-```
-
-The `HasRoles` trait also adds a `role` scope to your models to scope the query to certain roles or permissions:
-
-```php
-$users = User::role('writer')->get(); // Returns only users with the role 'writer'
-```
-
-The `role` scope can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object.
-
-The same trait also adds a scope to only get users that have a certain permission.
-
-```php
-$users = User::permission('edit articles')->get(); // Returns only users with the permission 'edit articles' (inherited or directly)
-```
-
-The scope can accept a string, a `\Spatie\Permission\Models\Permission` object or an `\Illuminate\Support\Collection` object.
-
-### Using "direct" permissions (see below to use both roles and permissions)
-
-A permission can be given to any user:
-
-```php
-$user->givePermissionTo('edit articles');
-
-// You can also give multiple permission at once
-$user->givePermissionTo('edit articles', 'delete articles');
-
-// You may also pass an array
-$user->givePermissionTo(['edit articles', 'delete articles']);
-```
-
-A permission can be revoked from a user:
-
-```php
-$user->revokePermissionTo('edit articles');
-```
-
-Or revoke & add new permissions in one go:
-
-```php
-$user->syncPermissions(['edit articles', 'delete articles']);
-```
-
-You can check if a user has a permission:
-
-```php
-$user->hasPermissionTo('edit articles');
-```
-
-Or you may pass an integer representing the permission id
-
-```php
-$user->hasPermissionTo('1');
-$user->hasPermissionTo(Permission::find(1)->id);
-$user->hasPermissionTo($somePermission->id);
-```
-
-You can check if a user has Any of an array of permissions:
-
-```php
-$user->hasAnyPermission(['edit articles', 'publish articles', 'unpublish articles']);
-```
-
-...or if a user has All of an array of permissions:
-
-```php
-$user->hasAllPermissions(['edit articles', 'publish articles', 'unpublish articles']);
-```
-
-You may also pass integers to lookup by permission id
-
-```php
-$user->hasAnyPermission(['edit articles', 1, 5]);
-```
-
-Saved permissions will be registered with the `Illuminate\Auth\Access\Gate` class for the default guard. So you can
-check if a user has a permission with Laravel's default `can` function:
+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:
```php
$user->can('edit articles');
```
-### Using permissions via roles
-
-A role can be assigned to any user:
-
-```php
-$user->assignRole('writer');
-
-// You can also assign multiple roles at once
-$user->assignRole('writer', 'admin');
-// or as an array
-$user->assignRole(['writer', 'admin']);
-```
-
-A role can be removed from a user:
-
-```php
-$user->removeRole('writer');
-```
-
-Roles can also be synced:
-
-```php
-// All current roles will be removed from the user and replaced by the array given
-$user->syncRoles(['writer', 'admin']);
-```
-
-You can determine if a user has a certain role:
-
-```php
-$user->hasRole('writer');
-```
-
-You can also determine if a user has any of a given list of roles:
-
-```php
-$user->hasAnyRole(Role::all());
-```
-
-You can also determine if a user has all of a given list of roles:
-
-```php
-$user->hasAllRoles(Role::all());
-```
-
-The `assignRole`, `hasRole`, `hasAnyRole`, `hasAllRoles` and `removeRole` functions can accept a
- string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object.
-
-A permission can be given to a role:
-
-```php
-$role->givePermissionTo('edit articles');
-```
-
-You can determine if a role has a certain permission:
-
-```php
-$role->hasPermissionTo('edit articles');
-```
-
-A permission can be revoked from a role:
-
-```php
-$role->revokePermissionTo('edit articles');
-```
-
-The `givePermissionTo` and `revokePermissionTo` functions can accept a
-string or a `Spatie\Permission\Models\Permission` object.
-
-
-Permissions are inherited from roles automatically.
-Additionally, individual permissions can be assigned to the user too.
-For instance:
-
-```php
-$role = Role::findByName('writer');
-$role->givePermissionTo('edit articles');
-
-$user->assignRole('writer');
-
-$user->givePermissionTo('delete articles');
-```
-
-In the above example, a role is given permission to edit articles and this role is assigned to a user.
-Now the user can edit articles and additionally delete articles. The permission of 'delete articles' is the user's direct permission because it is assigned directly to them.
-When we call `$user->hasDirectPermission('delete articles')` it returns `true`,
-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 list all of these permissions:
-
-```php
-// Direct permissions
-$user->getDirectPermissions() // Or $user->permissions;
-
-// Permissions inherited from the user's roles
-$user->getPermissionsViaRoles();
-
-// All permissions which apply on the user (inherited and direct)
-$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.
-
-### Using Blade directives
-This package also adds Blade directives to verify whether the currently logged in user has all or any of a given list of roles.
-
-Optionally you can pass in the `guard` that the check will be performed on as a second argument.
-
-#### Blade and Roles
-Check for a specific role:
-```php
-@role('writer')
- I am a writer!
-@else
- I am not a writer...
-@endrole
-```
-is the same as
-```php
-@hasrole('writer')
- I am a writer!
-@else
- I am not a writer...
-@endhasrole
-```
-
-Check for any role in a list:
-```php
-@hasanyrole($collectionOfRoles)
- I have one or more of these roles!
-@else
- I have none of these roles...
-@endhasanyrole
-// or
-@hasanyrole('writer|admin')
- I am either a writer or an admin or both!
-@else
- I have none of these roles...
-@endhasanyrole
-```
-Check for all roles:
-
-```php
-@hasallroles($collectionOfRoles)
- I have all of these roles!
-@else
- I do not have all of these roles...
-@endhasallroles
-// or
-@hasallroles('writer|admin')
- I am both a writer and an admin!
-@else
- I do not have all of these roles...
-@endhasallroles
-```
-
-Alternatively, `@unlessrole` gives the reverse for checking a singular role, like this:
-
-```php
-@unlessrole('does not have this role')
- I do not have the role
-@else
- I do have the role
-@endunlessrole
-```
-
-#### Blade and Permissions
-This package doesn't add any permission-specific Blade directives. Instead, use Laravel's native `@can` directive to check if a user has a certain permission.
-
-```php
-@can('edit articles')
- //
-@endcan
-```
-or
-```php
-@if(auth()->user()->can('edit articles') && $some_other_condition)
- //
-@endif
-```
-
-## Defining a Super-Admin
-
-We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` rule which checks for the desired role.
-
-Then you can implement the best-practice of primarily using permission-based controls throughout your app, without always having to check for "is this a super-admin" everywhere.
-
-See this wiki article on [Defining a Super-Admin Gate rule](https://github.com/spatie/laravel-permission/wiki/Global-%22Admin%22-role) in your app.
-
-## Best Practices -- roles vs permissions
-
-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.
-
-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.
-
-
-## Using multiple guards
-
-When using the default Laravel auth configuration all of the above methods will work out of the box, no extra configuration required.
-
-However, when using multiple guards they will act like namespaces for your permissions and roles. Meaning every guard has its own set of permissions and roles that can be assigned to their user model.
-
-### Using permissions and roles with multiple guards
-
-When creating new permissions and roles, if no guard is specified, then the **first** defined guard in `auth.guards` config array will be used. When creating permissions and roles for specific guards you'll have to specify their `guard_name` on the model:
-
-```php
-// Create a superadmin role for the admin users
-$role = Role::create(['guard_name' => 'admin', 'name' => 'superadmin']);
-
-// Define a `publish articles` permission for the admin users belonging to the admin guard
-$permission = Permission::create(['guard_name' => 'admin', 'name' => 'publish articles']);
-
-// Define a *different* `publish articles` permission for the regular users belonging to the web guard
-$permission = Permission::create(['guard_name' => 'web', 'name' => 'publish articles']);
-```
-
-To check if a user has permission for a specific guard:
-
-```php
-$user->hasPermissionTo('publish articles', 'admin');
-```
-
-> **Note**: When determining whether a role/permission is valid on a given model, it chooses the guard in this order: first the `$guard_name` property of the model; then the guard in the config (through a provider); then the first-defined guard in the `auth.guards` config array; then the `auth.defaults.guard` config.
-
-> **Note**: When using other than the default `web` guard, you will need to declare which `guard_name` you wish each model to use by setting the `$guard_name` property in your model. One per model is simplest.
-
-> **Note**: If your app uses only a single guard, but is not `web` then change the order of your listed guards in your `config/app.php` to list your primary guard as the default and as the first in the list of defined guards.
-
-### Assigning permissions and roles to guard users
-
-You can use the same methods to assign permissions and roles to users as described above in [using permissions via roles](#using-permissions-via-roles). Just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` exception will be thrown.
-
-### Using blade directives with multiple guards
-
-You can use all of the blade directives listed in [using blade directives](#using-blade-directives) by passing in the guard you wish to use as the second argument to the directive:
-
-```php
-@role('super-admin', 'admin')
- I am a super-admin!
-@else
- I am not a super-admin...
-@endrole
-```
-
-## Using a middleware
-
-This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware. You can add them inside your `app/Http/Kernel.php` file.
-
-```php
-protected $routeMiddleware = [
- // ...
- 'role' => \Spatie\Permission\Middlewares\RoleMiddleware::class,
- 'permission' => \Spatie\Permission\Middlewares\PermissionMiddleware::class,
- 'role_or_permission' => \Spatie\Permission\Middlewares\RoleOrPermissionMiddleware::class,
-];
-```
-
-Then you can protect your routes using middleware rules:
-
-```php
-Route::group(['middleware' => ['role:super-admin']], function () {
- //
-});
-
-Route::group(['middleware' => ['permission:publish articles']], function () {
- //
-});
-
-Route::group(['middleware' => ['role:super-admin','permission:publish articles']], function () {
- //
-});
-
-Route::group(['middleware' => ['role_or_permission:super-admin']], function () {
- //
-});
-
-Route::group(['middleware' => ['role_or_permission:publish articles']], function () {
- //
-});
-```
-
-Alternatively, you can separate multiple roles or permission with a `|` (pipe) character:
-
-```php
-Route::group(['middleware' => ['role:super-admin|writer']], function () {
- //
-});
-
-Route::group(['middleware' => ['permission:publish articles|edit articles']], function () {
- //
-});
-
-Route::group(['middleware' => ['role_or_permission:super-admin|edit articles']], function () {
- //
-});
-```
-
-You can protect your controllers similarly, by setting desired middleware in the constructor:
-
-```php
-public function __construct()
-{
- $this->middleware(['role:super-admin','permission:publish articles|edit articles']);
-}
-```
-
-```php
-public function __construct()
-{
- $this->middleware(['role_or_permission:super-admin|edit articles']);
-}
-```
-
-### Catching role and permission failures
-If you want to override the default `403` response, you can catch the `UnauthorizedException` using your app's exception handler:
-
-```php
-public function render($request, Exception $exception)
-{
- if ($exception instanceof \Spatie\Permission\Exceptions\UnauthorizedException) {
- // Code here ...
- }
-
- return parent::render($request, $exception);
-}
-```
-
-## Using artisan commands
-
-You can create a role or permission from a console with artisan commands.
-
-```bash
-php artisan permission:create-role writer
-```
-
-```bash
-php artisan permission:create-permission "edit articles"
-```
-
-When creating permissions/roles for specific guards you can specify the guard names as a second argument:
-
-```bash
-php artisan permission:create-role writer web
-```
-
-```bash
-php artisan permission:create-permission "edit articles" web
-```
-
-When creating roles you can also create and link permissions at the same time:
-
-```bash
-php artisan permission:create-role writer web "create articles|edit articles"
-```
-
-
-## Unit Testing
-
-In your application's tests, if you are not seeding roles and permissions as part of your test `setUp()` then you may run into a chicken/egg situation where roles and permissions aren't registered with the gate (because your tests create them after that gate registration is done). Working around this is simple: In your tests simply add a `setUp()` instruction to re-register the permissions, like this:
-
-```php
- public function setUp()
- {
- // first include all the normal setUp operations
- parent::setUp();
-
- // now re-register all the roles and permissions
- $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();
- }
-```
-
-## Database Seeding
-
-You may discover that it is best to flush this package's cache before seeding, to avoid cache conflict errors. This can be done directly in a seeder class. Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional):
-
-```php
-use Illuminate\Database\Seeder;
-use Spatie\Permission\Models\Role;
-use Spatie\Permission\Models\Permission;
-
-class RolesAndPermissionsSeeder extends Seeder
-{
- public function run()
- {
- // Reset cached roles and permissions
- app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
-
- // create permissions
- Permission::create(['name' => 'edit articles']);
- Permission::create(['name' => 'delete articles']);
- Permission::create(['name' => 'publish articles']);
- Permission::create(['name' => 'unpublish articles']);
-
- // create roles and assign created permissions
-
- // this can be done as separate statements
- $role = Role::create(['name' => 'writer']);
- $role->givePermissionTo('edit articles');
-
- // or may be done by chaining
- $role = Role::create(['name' => 'moderator'])
- ->givePermissionTo(['publish articles', 'unpublish articles']);
-
- $role = Role::create(['name' => 'super-admin']);
- $role->givePermissionTo(Permission::all());
- }
-}
-```
-
-## 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
-
-If you need to REPLACE the existing `Role` or `Permission` models you need to keep the
-following things in mind:
-
-- Your `Role` model needs to implement the `Spatie\Permission\Contracts\Role` contract
-- Your `Permission` model needs to implement the `Spatie\Permission\Contracts\Permission` contract
-
-In BOTH cases, whether extending or replacing, you will need to specify your new models in the configuration. To do this you must update the `models.role` and `models.permission` values in the configuration file after publishing the configuration with this command:
-
-```bash
-php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider" --tag="config"
-```
-
-
-## Cache
-
-Role and Permission data are cached to speed up performance.
-
-While we recommend not changing the cache "key" name, if you wish to alter the expiration time you may do so in the `config/permission.php` file, in the `cache` array. Note that as of v2.26.0 the `cache` entry here is now an array, and `expiration_time` is a sub-array entry.
-
-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);
-$permission->assignRole('writer');
-$permission->removeRole('writer');
-$permission->syncRoles(params);
-```
-
-HOWEVER, if you manipulate permission/role data directly in the database instead of calling the supplied methods, then you will not see the changes reflected in the application unless you manually reset the cache.
-
-### Manual cache reset
-To manually reset the cache for this package, you can run the following in your app code:
-```php
-$this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
-```
-
-Or you can use an Artisan command:
-```bash
-php artisan permission:cache-reset
-```
-
-
-### Cache Identifier
-
-TIP: If you are leveraging a caching service such as `redis` or `memcached` and there are other sites
-running on your server, you could run into cache clashes between apps. It is prudent to set your own
-cache `prefix` in Laravel's `/config/cache.php` to something unique for each application.
-This will prevent other applications from accidentally using/changing your cached data.
-
+## Support us
-## Need a UI?
+[](https://spatie.be/github-ad-click/laravel-permission)
-The package doesn't come with any screens out of the box, you should build that yourself. Here are some options to get you started:
+We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
-- [Laravel Nova package by @vyuldashev for managing Roles and Permissions](https://github.com/vyuldashev/nova-permission)
+We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
-- [Laravel Nova package by @paras-malhotra for managing Roles and Permissions and permissions based authorization for Nova Resources](https://github.com/insenseanalytics/laravel-nova-permission)
+## Changelog
-- [Extensive tutorial for building permissions UI](https://scotch.io/tutorials/user-authorization-in-laravel-54-with-spatie-laravel-permission) by [Caleb Oki](http://www.caleboki.com/).
+Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
-- [How to create a UI for managing the permissions and roles](http://www.qcode.in/easy-roles-and-permissions-in-laravel-5-4/)
+## Contributing
+Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.
### Testing
@@ -935,32 +61,21 @@ The package doesn't come with any screens out of the box, you should build that
composer test
```
-### Upgrading
-If you're upgrading from v1 to v2, @fabricecw prepared [a gist which may make your data migration easier](https://gist.github.com/fabricecw/58ee93dd4f99e78724d8acbb851658a4).
-You will also need to remove your old `laravel-permission.php` config file and publish the new one `permission.php`, and edit accordingly.
-
-### Changelog
-
-Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
-
-## Contributing
-
-Please see [CONTRIBUTING](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
You're free to use this package, but if it makes it to your production environment we highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using.
-Our address is: Spatie, Samberstraat 69D, 2060 Antwerp, Belgium.
+Our address is: Spatie, Kruikstraat 22, 2018 Antwerp, Belgium.
We publish all received postcards [on our company website](https://spatie.be/en/opensource/postcards).
## Credits
+- [Chris Brown](https://github.com/drbyte)
- [Freek Van der Herten](https://github.com/freekmurze)
- [All Contributors](../../contributors)
@@ -970,17 +85,14 @@ can be found [in this repo on GitHub](https://github.com/laracasts/laravel-5-rol
Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who greatly helped with `v2`, and to [Chris Brown](https://github.com/drbyte) for his longtime support helping us maintain the package.
+Special thanks to [Caneco](https://twitter.com/caneco) for the original logo.
+
## Alternatives
- [Povilas Korop](https://twitter.com/@povilaskorop) did an excellent job listing the alternatives [in an article on Laravel News](https://laravel-news.com/two-best-roles-permissions-packages). In that same article, he compares laravel-permission to [Joseph Silber](https://github.com/JosephSilber)'s [Bouncer]((https://github.com/JosephSilber/bouncer)), which in our book is also an excellent package.
-- [ultraware/roles](https://github.com/ultraware/roles) takes a slightly different approach to its features.
-
-## Support us
-
-Spatie is a web design agency based in Antwerp, Belgium. You'll find an overview of all our open source projects [on our website](https://spatie.be/opensource).
-
-Does your business depend on our contributions? Reach out and support us on [Patreon](https://www.patreon.com/spatie).
-All pledges will be dedicated to allocating workforce on maintenance and new awesome stuff.
+- [santigarcor/laratrust](https://github.com/santigarcor/laratrust) implements team support
+- [ultraware/roles](https://github.com/ultraware/roles) (archived) takes a slightly different approach to its features.
+- [zizaco/entrust](https://github.com/zizaco/entrust) offers some wildcard pattern matching
## License
diff --git a/art/README.md b/art/README.md
new file mode 100644
index 000000000..bda495e53
--- /dev/null
+++ b/art/README.md
@@ -0,0 +1,58 @@
+
+
+
+
+# Laravel Permission Art
+
+The logo was inspired by the [Spatie](https://spatie.be) brand, and the well known minimal design of Laravel packages.
+
+## Fonts
+
+The logo is using the following fonts:
+
+- [Inter 500](https://fonts.google.com/specimen/Inter#500)
+- [Inter 600](https://fonts.google.com/specimen/Inter#600)
+
+## Colors
+
+| |#hex |rgb() |
+|--- |--- |--- |
+||`#E8F1F4`|`rgb(232,241,244)`|
+||`#C6DDE4`|`rgb(198,221,228)`|
+||`#A3C8D4`|`rgb(163,200,212)`|
+||`#5E9EB3`|`rgb(94,158,179)` |
+||`#197593`|`rgb(25,117,147)` |
+||`#176984`|`rgb(23,105,132)` |
+||`#0F4658`|`rgb(15,70,88)` |
+||`#0B3542`|`rgb(11,53,66)` |
+||`#08232C`|`rgb(8,35,44)` |
+
+## Requirements
+
+- A screen or a printer
+
+## Installation
+
+- Open the file
+- *Right-click* on the image
+- Choose **"Save image as…"** option
+
+## Maintainers
+
+**Laravel Permission** logo is designed and maintained by [Caneco](https://twitter.com/caneco).
+
+## License
+
+All rights reserved, but with the following extra conditions:
+
+- It is **OK** to use the Laravel Permission logo in the following cases:
+ - In marketing materials for technical events, e.g. meetups, hackathons, conferences and workshops that are related to Laravel.
+ - In open source projects related to Laravel.
+ - In technical articles/videos/books/papers for educational purposes.
+ - To illustrate a commercial product.
+
+- It is **NOT OK** to use the Laravel Permission logo in the following cases without prior written consent from the copyright owners:
+ - Using the Laravel Permission logo in a commercial product for purposes other than illustrating its integration.
+ - Sell physical products that uses the Laravel Permission logo or its variants, e.g. t-shirts.
+
+By any means the owner reserves the right of final explanation for any use case not explicitly stated above.
diff --git a/art/logomark.png b/art/logomark.png
new file mode 100644
index 000000000..c02bb8730
Binary files /dev/null and b/art/logomark.png differ
diff --git a/art/logomark.svg b/art/logomark.svg
new file mode 100644
index 000000000..8071f1f70
--- /dev/null
+++ b/art/logomark.svg
@@ -0,0 +1,17 @@
+
diff --git a/art/logomark@2x.png b/art/logomark@2x.png
new file mode 100644
index 000000000..1f0d440f6
Binary files /dev/null and b/art/logomark@2x.png differ
diff --git a/art/logomark@3x.png b/art/logomark@3x.png
new file mode 100644
index 000000000..2e3936dbc
Binary files /dev/null and b/art/logomark@3x.png differ
diff --git a/art/logomark@4x.png b/art/logomark@4x.png
new file mode 100644
index 000000000..ae9a6cf96
Binary files /dev/null and b/art/logomark@4x.png differ
diff --git a/art/palette/100.png b/art/palette/100.png
new file mode 100644
index 000000000..5251db2c3
Binary files /dev/null and b/art/palette/100.png differ
diff --git a/art/palette/200.png b/art/palette/200.png
new file mode 100644
index 000000000..6b2368e58
Binary files /dev/null and b/art/palette/200.png differ
diff --git a/art/palette/300.png b/art/palette/300.png
new file mode 100644
index 000000000..214e112c2
Binary files /dev/null and b/art/palette/300.png differ
diff --git a/art/palette/400.png b/art/palette/400.png
new file mode 100644
index 000000000..cdfe741d4
Binary files /dev/null and b/art/palette/400.png differ
diff --git a/art/palette/500.png b/art/palette/500.png
new file mode 100644
index 000000000..a6b805345
Binary files /dev/null and b/art/palette/500.png differ
diff --git a/art/palette/600.png b/art/palette/600.png
new file mode 100644
index 000000000..a8cc07af8
Binary files /dev/null and b/art/palette/600.png differ
diff --git a/art/palette/700.png b/art/palette/700.png
new file mode 100644
index 000000000..9baf5680b
Binary files /dev/null and b/art/palette/700.png differ
diff --git a/art/palette/800.png b/art/palette/800.png
new file mode 100644
index 000000000..8cf3d3287
Binary files /dev/null and b/art/palette/800.png differ
diff --git a/art/palette/900.png b/art/palette/900.png
new file mode 100644
index 000000000..aaf127dd4
Binary files /dev/null and b/art/palette/900.png differ
diff --git a/art/socialcard.png b/art/socialcard.png
new file mode 100644
index 000000000..e2d9cda01
Binary files /dev/null and b/art/socialcard.png differ
diff --git a/composer.json b/composer.json
index 8c80cc07b..c04c5bff9 100644
--- a/composer.json
+++ b/composer.json
@@ -1,15 +1,17 @@
{
"name": "spatie/laravel-permission",
- "description": "Permission handling for Laravel 5.4 and up",
+ "description": "Permission handling for Laravel 8.0 and up",
+ "license": "MIT",
"keywords": [
"spatie",
"laravel",
"permission",
+ "permissions",
+ "roles",
"acl",
+ "rbac",
"security"
],
- "homepage": "https://github.com/spatie/laravel-permission",
- "license": "MIT",
"authors": [
{
"name": "Freek Van der Herten",
@@ -18,18 +20,22 @@
"role": "Developer"
}
],
+ "homepage": "https://github.com/spatie/laravel-permission",
"require": {
- "php" : ">=7.0",
- "illuminate/auth": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0",
- "illuminate/container": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0",
- "illuminate/contracts": "~5.3.0|~5.4.0|~5.5.0|~5.6.0|~5.7.0",
- "illuminate/database": "~5.4.0|~5.5.0|~5.6.0|~5.7.0"
+ "php": "^8.0",
+ "illuminate/auth": "^8.12|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/container": "^8.12|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/contracts": "^8.12|^9.0|^10.0|^11.0|^12.0",
+ "illuminate/database": "^8.12|^9.0|^10.0|^11.0|^12.0"
},
"require-dev": {
- "orchestra/testbench": "~3.4.2|~3.5.0|~3.6.0|~3.7.0",
- "phpunit/phpunit": "^5.7|6.2|^7.0",
- "predis/predis": "^1.1"
+ "laravel/passport": "^11.0|^12.0",
+ "laravel/pint": "^1.0",
+ "orchestra/testbench": "^6.23|^7.0|^8.0|^9.0|^10.0",
+ "phpunit/phpunit": "^9.4|^10.1|^11.5"
},
+ "minimum-stability": "dev",
+ "prefer-stable": true,
"autoload": {
"psr-4": {
"Spatie\\Permission\\": "src"
@@ -40,20 +46,26 @@
},
"autoload-dev": {
"psr-4": {
- "Spatie\\Permission\\Test\\": "tests"
+ "Spatie\\Permission\\Tests\\": "tests"
}
},
- "scripts": {
- "test": "phpunit"
- },
"config": {
"sort-packages": true
},
"extra": {
+ "branch-alias": {
+ "dev-main": "6.x-dev",
+ "dev-master": "6.x-dev"
+ },
"laravel": {
"providers": [
"Spatie\\Permission\\PermissionServiceProvider"
]
}
+ },
+ "scripts": {
+ "test": "phpunit",
+ "format": "pint",
+ "analyse": "echo 'Checking dependencies...' && composer require --dev larastan/larastan && phpstan analyse"
}
}
diff --git a/config/permission.php b/config/permission.php
index c675dce77..f39f6b5bf 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
@@ -80,48 +85,118 @@
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
+
'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 required permission/role names are added to the exception
- * message. This could be considered an information leak in some contexts, so
- * the default setting is false here for optimum safety.
+ * 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, Laravel\Octane\Events\OperationTerminated event listener will be registered
+ * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
+ * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
+ */
+ 'register_octane_reset_listener' => false,
+
+ /*
+ * Events will fire when a role or permission is assigned/unassigned:
+ * \Spatie\Permission\Events\RoleAttached
+ * \Spatie\Permission\Events\RoleDetached
+ * \Spatie\Permission\Events\PermissionAttached
+ * \Spatie\Permission\Events\PermissionDetached
+ *
+ * To enable, set to true, and then create listeners to watch these events.
+ */
+ 'events_enabled' => false,
+
+ /*
+ * Teams Feature.
+ * 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 this package's migration file)
+ */
+
+ 'teams' => false,
+
+ /*
+ * The class to use to resolve the permissions team id
+ */
+ 'team_resolver' => \Spatie\Permission\DefaultTeamResolver::class,
+
+ /*
+ * Passport Client Credentials Grant
+ * When set to true the package will use Passports Client to check permissions
+ */
+
+ 'use_passport_client_credentials' => false,
+
+ /*
+ * When set to true, the required permission names are added to exception messages.
+ * This could be considered an information leak in some contexts, so the default
+ * setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
- 'cache' => [
+ /*
+ * When set to true, the required role names are added to exception messages.
+ * This could be considered an information leak in some contexts, so the default
+ * setting is false here for optimum safety.
+ */
- /*
- * By default all permissions will be cached for 24 hours unless a permission or
- * role is updated. Then the cache will be flushed immediately.
- */
+ 'display_role_in_exception' => false,
- 'expiration_time' => 60 * 24,
+ /*
+ * By default wildcard permission lookups are disabled.
+ * See documentation to understand supported syntax.
+ */
+
+ 'enable_wildcard_permission' => false,
+
+ /*
+ * The class to use for interpreting wildcard permissions.
+ * If you need to modify delimiters, override the class and specify its name here.
+ */
+ // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
+
+ /* Cache-specific settings */
+
+ 'cache' => [
/*
- * The key to use when tagging and prefixing entries in the cache.
+ * By default all permissions are cached for 24 hours to speed up performance.
+ * When permissions or roles are updated the cache is flushed automatically.
*/
- 'key' => 'spatie.permission.cache',
+ 'expiration_time' => \DateInterval::createFromDateString('24 hours'),
/*
- * 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'.
+ * The cache key used to store all permissions.
*/
- 'model_key' => 'name',
+ 'key' => 'spatie.permission.cache',
/*
* 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
* file. Using 'default' here means to use the `default` set in cache.php.
*/
+
'store' => 'default',
],
];
diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub
new file mode 100644
index 000000000..01fcca697
--- /dev/null
+++ b/database/migrations/add_teams_fields.php.stub
@@ -0,0 +1,91 @@
+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, $pivotPermission) {
+ $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([$pivotPermission]);
+ }
+ $table->dropPrimary();
+
+ $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_permissions_permission_model_type_primary');
+ if (DB::getDriverName() !== 'sqlite') {
+ $table->foreign($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, $pivotRole) {
+ $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([$pivotRole]);
+ }
+ $table->dropPrimary();
+
+ $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_roles_role_model_type_primary');
+ if (DB::getDriverName() !== 'sqlite') {
+ $table->foreign($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.
+ */
+ public function down(): void
+ {
+
+ }
+};
diff --git a/database/migrations/create_permission_tables.php.stub b/database/migrations/create_permission_tables.php.stub
index 33b1e6693..ce4d9d2d4 100644
--- a/database/migrations/create_permission_tables.php.stub
+++ b/database/migrations/create_permission_tables.php.stub
@@ -1,102 +1,136 @@
increments('id');
- $table->string('name');
- $table->string('guard_name');
+ Schema::create($tableNames['permissions'], static function (Blueprint $table) {
+ // $table->engine('InnoDB');
+ $table->bigIncrements('id'); // permission id
+ $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
+ $table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
+
+ $table->unique(['name', 'guard_name']);
});
- Schema::create($tableNames['roles'], function (Blueprint $table) {
- $table->increments('id');
- $table->string('name');
- $table->string('guard_name');
+ Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
+ // $table->engine('InnoDB');
+ $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 MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
+ $table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$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->unsignedInteger('permission_id');
+ Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
+ $table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
- $table->index([$columnNames['model_morph_key'], 'model_type', ]);
+ $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
- $table->foreign('permission_id')
- ->references('id')
+ $table->foreign($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'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
+ } else {
+ $table->primary([$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->unsignedInteger('role_id');
+ Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
+ $table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
- $table->index([$columnNames['model_morph_key'], 'model_type', ]);
+ $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
- $table->foreign('role_id')
- ->references('id')
+ $table->foreign($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'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_roles_role_model_type_primary');
+ } else {
+ $table->primary([$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->unsignedInteger('permission_id');
- $table->unsignedInteger('role_id');
+ Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
+ $table->unsignedBigInteger($pivotPermission);
+ $table->unsignedBigInteger($pivotRole);
- $table->foreign('permission_id')
- ->references('id')
+ $table->foreign($pivotPermission)
+ ->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
- $table->foreign('role_id')
- ->references('id')
+ $table->foreign($pivotRole)
+ ->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
- $table->primary(['permission_id', 'role_id']);
-
- app('cache')
- ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
- ->forget(config('permission.cache.key'));
+ $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
+
+ 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()
+ public function down(): void
{
$tableNames = config('permission.table_names');
+ if (empty($tableNames)) {
+ throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
+ }
+
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
-}
+};
diff --git a/docs/_index.md b/docs/_index.md
new file mode 100644
index 000000000..cf88be1b4
--- /dev/null
+++ b/docs/_index.md
@@ -0,0 +1,6 @@
+---
+title: v6
+slogan: Associate users with roles and permissions
+githubUrl: https://github.com/spatie/laravel-permission
+branch: main
+---
diff --git a/docs/about-us.md b/docs/about-us.md
new file mode 100644
index 000000000..4ebc6742f
--- /dev/null
+++ b/docs/about-us.md
@@ -0,0 +1,16 @@
+---
+title: About us
+---
+
+[Spatie](https://spatie.be) is a webdesign agency based in Antwerp, Belgium.
+
+Open source software is used in all projects we deliver. Laravel, Nginx, Ubuntu are just a few
+of the free pieces of software we use every single day. For this, we are very grateful.
+When we feel we have solved a problem in a way that can help other developers,
+we release our code as open source software [on GitHub](https://spatie.be/opensource).
+
+This package is heavily based on [Jeffrey Way](https://twitter.com/jeffrey_way)'s awesome [Laracasts](https://laracasts.com) lessons
+on [permissions and roles](https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/16). His original code
+can be found [in this repo on GitHub](https://github.com/laracasts/laravel-5-roles-and-permissions-demo).
+
+Special thanks to [Alex Vanderbist](https://github.com/AlexVanderbist) who greatly helped with `v2`, and to [Chris Brown](https://github.com/drbyte) for his longtime support helping us maintain the package.
diff --git a/docs/advanced-usage/_index.md b/docs/advanced-usage/_index.md
new file mode 100644
index 000000000..31fa54b6a
--- /dev/null
+++ b/docs/advanced-usage/_index.md
@@ -0,0 +1,4 @@
+---
+title: Advanced usage
+weight: 3
+---
diff --git a/docs/advanced-usage/cache.md b/docs/advanced-usage/cache.md
new file mode 100644
index 000000000..c2dd260c2
--- /dev/null
+++ b/docs/advanced-usage/cache.md
@@ -0,0 +1,107 @@
+---
+title: Cache
+weight: 5
+---
+
+Role and Permission data are cached to speed up performance.
+
+## Automatic Cache Refresh Using Built-In Functions
+
+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
+// When handling permissions assigned to roles:
+$role->givePermissionTo('edit articles');
+$role->revokePermissionTo('edit articles');
+$role->syncPermissions(params);
+
+// When linking roles to permissions:
+$permission->assignRole('writer');
+$permission->removeRole('writer');
+$permission->syncRoles(params);
+```
+
+HOWEVER, if you manipulate permission/role data directly in the database instead of calling the supplied methods, then you will not see the changes reflected in the application unless you manually reset the cache.
+
+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 operations on a User 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:
+```php
+app()->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
+```
+
+Or you can use an Artisan command:
+```bash
+php artisan permission:cache-reset
+```
+(This command is effectively an alias for `artisan cache:forget spatie.permission.cache` but respects the package config as well.)
+
+## Octane cache reset
+In many cases Octane will not need additional cache resets; however, if you find that cache results are stale or crossing over between requests, you can force a cache flush upon every Octane reset cycle by editing the `/config/permission.php` and setting `register_octane_reset_listener` to true.
+
+## Cache Configuration Settings
+
+This package allows you to customize cache-related operations via its config file. In most cases the defaults are fine; however, in a multitenancy situation you may wish to do some cache-prefix overrides when switching tenants. See below for more details.
+
+### Cache Expiration Time
+
+The default cache `expiration_time` is `24 hours`.
+If you wish to alter the expiration time you may do so in the `config/permission.php` file, in the `cache` array.
+
+
+### Cache Key
+
+The default cache key is `spatie.permission.cache`.
+We recommend not changing the cache "key" name. Usually changing it is a bad idea. More likely setting the cache `prefix` is better, as mentioned below.
+
+
+### Cache Identifier / Prefix
+
+Laravel Tip: If you are leveraging a caching service such as `redis` or `memcached` and there are other sites running on your server, you could run into cache clashes between apps.
+
+To prevent other applications from accidentally using/changing your cached data, it is prudent to set your own cache `prefix` in Laravel's `/config/cache.php` to something unique for each application which shares the same caching service.
+
+Most multi-tenant "packages" take care of this for you when switching tenants. Optionally you might need to change cache boot order by writing a custom [cache boostrapper](https://github.com/spatie/laravel-permission/discussions/2310#discussioncomment-10855389).
+
+Tip: Most parts of your multitenancy app will relate to a single tenant during a given request lifecycle, so the following step will not be needed: However, in the less-common situation where your app might be switching between multiple tenants during a single request lifecycle (specifically: where changing the cache key/prefix (such as when switching between tenants) or switching the cache store), then after switching tenants or changing the cache configuration you will need to reinitialize the cache of the `PermissionRegistrar` so that the updated `CacheStore` and cache configuration are used.
+
+```php
+app()->make(\Spatie\Permission\PermissionRegistrar::class)->initializeCache();
+```
+
+
+### Custom Cache Store
+
+You can configure the package to use any of the Cache Stores you've configured in Laravel's `config/cache.php`. This way you can point this package's caching to its own specified resource.
+
+In `config/permission.php` set `cache.store` to the name of any one of the `config/cache.php` stores you've defined.
+
+## Disabling Cache
+
+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 Store
+
+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 Store 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.
+
+## Database cache Store
+
+TIP: If you have `CACHE_STORE=database` set in your `.env`, remember that [you must install Laravel's cache tables via a migration before performing any cache operations](https://laravel.com/docs/cache#prerequisites-database). If you fail to install those migrations, you'll run into errors like `Call to a member function perform() on null` when the cache store attempts to purge or update the cache. This package does strategic cache resets in various places, so may trigger that error if your app's cache dependencies aren't set up.
+
diff --git a/docs/advanced-usage/custom-permission-check.md b/docs/advanced-usage/custom-permission-check.md
new file mode 100644
index 000000000..956fda047
--- /dev/null
+++ b/docs/advanced-usage/custom-permission-check.md
@@ -0,0 +1,33 @@
+---
+title: Custom Permission Check
+weight: 6
+---
+
+## Default Permission Check Functionality
+By default, this package registers a `Gate::before()` method call on [Laravel's gate](https://laravel.com/docs/authorization). This method is responsible for checking if the user has the required permission or not, for calls to `can()` helpers and most `model policies`. Whether a user has a permission or not is determined by checking the user's permissions stored in the database.
+
+In the permission config file, `register_permission_check_method` is set to `true`, which means this package operates using the default behavior described above. Only set this to `false` if you want to bypass the default operation and implement your own custom logic for checking permissions, as described below.
+
+## Using Custom Permission Check Functionality
+
+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 `Gate::before()` method call to handle this:
+
+**app/Providers/AuthServiceProvider.php** (or maybe `AppServiceProvider.php` since Laravel 11)
+```php
+use Illuminate\Support\Facades\Gate;
+
+public function boot()
+{
+ ...
+
+ Gate::before(function ($user, $ability) {
+ return $user->hasTokenPermission($ability) ?: null;
+ });
+}
+```
+Here `hasTokenPermission` is a **custom method you need to implement yourself**.
+
diff --git a/docs/advanced-usage/events.md b/docs/advanced-usage/events.md
new file mode 100644
index 000000000..04682d1ac
--- /dev/null
+++ b/docs/advanced-usage/events.md
@@ -0,0 +1,21 @@
+---
+title: Events
+weight: 5
+---
+
+By default Events are not enabled, because not all apps need to fire events related to roles and permissions.
+
+However, you may enable events by setting the `events_enabled => true` in `config/permission.php`
+
+## Available Events
+
+The following events are available since `v6.15.0`:
+
+```
+\Spatie\Permission\Events\RoleAttached::class
+\Spatie\Permission\Events\RoleDetached::class
+\Spatie\Permission\Events\PermissionAttached::class
+\Spatie\Permission\Events\PermissionDetached::class
+```
+Note that the events can receive the role or permission details as a model ID or as an Eloquent record, or as an array or collection of ids or records. Be sure to inspect the parameter before acting on it.
+
diff --git a/docs/advanced-usage/exceptions.md b/docs/advanced-usage/exceptions.md
new file mode 100644
index 000000000..231159107
--- /dev/null
+++ b/docs/advanced-usage/exceptions.md
@@ -0,0 +1,38 @@
+---
+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#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/main/src/Exceptions](https://github.com/spatie/laravel-permission/tree/main/src/Exceptions)
+
+
+**Laravel 10: app/Exceptions/Handler.php**
+```php
+
+public function register()
+{
+ $this->renderable(function (\Spatie\Permission\Exceptions\UnauthorizedException $e, $request) {
+ return response()->json([
+ 'responseMessage' => 'You do not have the required authorization.',
+ 'responseStatus' => 403,
+ ]);
+ });
+}
+```
+
+**Laravel 11: bootstrap/app.php**
+```php
+
+->withExceptions(function (Exceptions $exceptions) {
+ $exceptions->render(function (\Spatie\Permission\Exceptions\UnauthorizedException $e, $request) {
+ return response()->json([
+ 'responseMessage' => 'You do not have the required authorization.',
+ 'responseStatus' => 403,
+ ]);
+ });
+}
+```
diff --git a/docs/advanced-usage/extending.md b/docs/advanced-usage/extending.md
new file mode 100644
index 000000000..3a86d0b52
--- /dev/null
+++ b/docs/advanced-usage/extending.md
@@ -0,0 +1,97 @@
+---
+title: Extending
+weight: 4
+---
+
+## Adding fields to your models
+You can add your own migrations to make changes to the role/permission tables, as you would for adding/changing fields in any other tables in your Laravel project.
+
+Following that, you can add any necessary logic for interacting with those fields into your custom/extended Models.
+
+Here is an example of adding a 'description' field to your Permissions and Roles tables:
+
+```sh
+php artisan make:migration add_description_to_permissions_tables
+```
+And in the migration file:
+```php
+public function up()
+{
+ Schema::table('permissions', function (Blueprint $table) {
+ $table->string('description')->nullable();
+ });
+ Schema::table('roles', function (Blueprint $table) {
+ $table->string('description')->nullable();
+ });
+}
+```
+
+Semi-Related article: [Adding Extra Fields To Pivot Table](https://quickadminpanel.com/blog/laravel-belongstomany-add-extra-fields-to-pivot-table/) (video)
+
+## Adding a description to roles and permissions
+A common question is "how do I add a description for my roles or permissions?".
+
+By default, a 'description' field is not included in this package, to keep the model memory usage low, because not every app has a need for displayed descriptions.
+
+But you are free to add it yourself if you wish. You can use the example above.
+
+### Multiple Language Descriptions
+
+If you need your 'description' to support multiple languages, simply use Laravel's built-in language features. You might prefer to rename the 'description' field in these migration examples from 'description' to 'description_key' for clarity.
+
+
+## Extending User Models
+Laravel's authorization features are available in models which implement the `Illuminate\Foundation\Auth\Access\Authorizable` trait.
+
+By default Laravel does this in `\App\Models\User` by extending `Illuminate\Foundation\Auth\User`, in which the trait and `Illuminate\Contracts\Auth\Access\Authorizable` contract are declared.
+
+If you are creating your own User models and wish Authorization features to be available, you need to implement `Illuminate\Contracts\Auth\Access\Authorizable` in one of those ways as well.
+
+## Child User Models
+
+Due to the nature of polymorphism and Eloquent's hard-coded mapping of model names in the database, setting relationships for child models that inherit permissions of the parent can be difficult (even near impossible depending on app requirements, especially when attempting to do inverse mappings). However, one thing you might consider if you need the child model to never have its own permissions/roles but to only use its parent's permissions/roles, is to [override the `getMorphClass` method on the model](https://github.com/laravel/framework/issues/17830#issuecomment-345619085).
+
+eg: This could be useful, but only if you're willing to give up the child's independence for roles/permissions:
+```php
+ public function getMorphClass()
+ {
+ return 'users';
+ }
+```
+
+## Extending Role and Permission Models
+If you are extending or replacing the role/permission models, you will need to specify your new models in this package's `config/permission.php` file.
+
+First be sure that you've published the configuration file (see the Installation instructions), and edit it to update the `models.role` and `models.permission` values to point to your new models.
+
+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/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)
+2. Uncheck "Use default settings", then click on the `Directives` tab.
+3. Add the following new directives for the laravel-permission package:
+
+
+**role**
+
+- has parameter = YES
+- Prefix: ``
+
+--
+
+**elserole**
+
+- has parameter = YES
+- Prefix: ``
+
+**endrole**
+
+- has parameter = NO
+- Prefix: blank
+- Suffix: blank
+
+--
+
+**hasrole**
+
+- has parameter = YES
+- Prefix: ``
+
+--
+
+**endhasrole**
+
+- has parameter = NO
+- Prefix: blank
+- Suffix: blank
+
+--
+
+**hasanyrole**
+
+- has parameter = YES
+- Prefix: ``
+
+--
+
+**endhasanyrole**
+
+- has parameter = NO
+- Prefix: blank
+- Suffix: blank
+
+--
+
+**hasallroles**
+
+- has parameter = YES
+- Prefix: ``
+
+--
+
+**endhasallroles**
+
+- has parameter = NO
+- Prefix: blank
+- Suffix: blank
+
+--
+
+**unlessrole**
+
+- has parameter = YES
+- Prefix: ``
+
+--
+
+**endunlessrole**
+
+- has parameter = NO
+- Prefix: blank
+- 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
new file mode 100644
index 000000000..51c56e8d2
--- /dev/null
+++ b/docs/advanced-usage/seeding.md
@@ -0,0 +1,150 @@
+---
+title: Database Seeding
+weight: 2
+---
+
+## Flush cache before/after seeding
+
+You may discover that it is best to flush this package's cache **BEFORE seeding, to avoid cache conflict errors**.
+
+And if you use the `WithoutModelEvents` trait in your seeders, flush it **AFTER creating any roles/permissions as well, before assigning or granting them**.
+
+```php
+// reset cached roles and permissions
+app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
+```
+
+You can optionally flush the cache before seeding by using the `SetUp()` method of your test suite (see the Testing page in the docs).
+
+Or it can be done directly in a seeder class, as shown below.
+
+## Database Cache Store
+
+TIP: If you have `CACHE_STORE=database` set in your `.env`, remember that [you must install Laravel's cache tables via a migration before performing any cache operations](https://laravel.com/docs/cache#prerequisites-database). If you fail to install those migrations, you'll run into errors like `Call to a member function perform() on null` when the cache store attempts to purge or update the cache. This package does strategic cache resets in various places, so may trigger that error if your app's cache dependencies aren't set up.
+
+## Roles/Permissions Seeder
+
+Here is a sample seeder, which first clears the cache, creates permissions and then assigns permissions to roles (the order of these steps is intentional):
+
+```php
+use Illuminate\Database\Seeder;
+use Spatie\Permission\Models\Role;
+use Spatie\Permission\Models\Permission;
+
+class RolesAndPermissionsSeeder extends Seeder
+{
+ public function run(): void
+ {
+ // Reset cached roles and permissions
+ app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
+
+ // create permissions
+ Permission::create(['name' => 'edit articles']);
+ Permission::create(['name' => 'delete articles']);
+ Permission::create(['name' => 'publish articles']);
+ Permission::create(['name' => 'unpublish articles']);
+
+ // update cache to know about the newly created permissions (required if using WithoutModelEvents in seeders)
+ app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
+
+
+ // create roles and assign created permissions
+
+ // this can be done as separate statements
+ $role = Role::create(['name' => 'writer']);
+ $role->givePermissionTo('edit articles');
+
+ // or may be done by chaining
+ $role = Role::create(['name' => 'moderator'])
+ ->givePermissionTo(['publish articles', 'unpublish articles']);
+
+ $role = Role::create(['name' => 'super-admin']);
+ $role->givePermissionTo(Permission::all());
+ }
+}
+```
+
+## User Seeding with Factories and States
+
+To use Factory States to assign roles after creating users:
+
+```php
+// Factory:
+ public function definition() {...}
+
+ public function active(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'status' => 1,
+ ])
+ ->afterCreating(function (User $user) {
+ $user->assignRole('ActiveMember');
+ });
+ }
+
+// Seeder:
+// To create 4 users using this 'active' state in a Seeder:
+User::factory(4)->active()->create();
+```
+
+To seed multiple users and then assign each of them a role, WITHOUT using Factory States:
+
+```php
+// Seeder:
+User::factory()
+ ->count(50)
+ ->create()
+ ->each(function ($user) {
+ $user->assignRole('Member');
+ });
+```
+
+
+## Speeding up seeding for large data sets
+
+When seeding large quantities of roles or permissions you may consider using Eloquent's `insert` command instead of `create`, as this bypasses all the internal checks that this package does when calling `create` (including extra queries to verify existence, test guards, etc).
+
+```php
+ $arrayOfPermissionNames = ['writer', 'editor'];
+ $permissions = collect($arrayOfPermissionNames)->map(function ($permission) {
+ return ['name' => $permission, 'guard_name' => 'web'];
+ });
+
+ Permission::insert($permissions->toArray());
+```
+
+Alternatively you could use `DB::insert`, as long as you also provide all the required data fields. One example of this is shown below ... but note that this example hard-codes the table names and field names, thus does not respect any customizations you may have in your permissions config file.
+
+```php
+$permissionsByRole = [
+ 'admin' => ['restore posts', 'force delete posts'],
+ 'editor' => ['create a post', 'update a post', 'delete a post'],
+ 'viewer' => ['view all posts', 'view a post']
+];
+
+$insertPermissions = fn ($role) => collect($permissionsByRole[$role])
+ ->map(fn ($name) => DB::table('permissions')->insertGetId(['name' => $name, 'guard_name' => 'web']))
+ ->toArray();
+
+$permissionIdsByRole = [
+ 'admin' => $insertPermissions('admin'),
+ 'editor' => $insertPermissions('editor'),
+ 'viewer' => $insertPermissions('viewer')
+];
+
+foreach ($permissionIdsByRole as $role => $permissionIds) {
+ $role = Role::whereName($role)->first();
+
+ DB::table('role_has_permissions')
+ ->insert(
+ collect($permissionIds)->map(fn ($id) => [
+ 'role_id' => $role->id,
+ 'permission_id' => $id
+ ])->toArray()
+ );
+}
+
+// and also add the command to flush the cache again now after doing all these inserts
+```
+
+**CAUTION**: ANY TIME YOU DIRECTLY RUN DB QUERIES you are bypassing cache-control features. So you will need to manually flush the package cache AFTER running direct DB queries, even in a seeder.
diff --git a/docs/advanced-usage/testing.md b/docs/advanced-usage/testing.md
new file mode 100644
index 000000000..a98955951
--- /dev/null
+++ b/docs/advanced-usage/testing.md
@@ -0,0 +1,56 @@
+---
+title: Testing
+weight: 1
+---
+
+## Clear Cache During Tests
+
+In your application's tests, if you are not seeding roles and permissions as part of your test `setUp()` then you may run into a chicken/egg situation where roles and permissions aren't registered with the gate (because your tests create them after that gate registration is done). Working around this is simple:
+
+In your tests simply add a `setUp()` instruction to re-register the permissions, like this:
+
+```php
+ protected function setUp(): void
+ {
+ // first include all the normal setUp operations
+ parent::setUp();
+
+ // now de-register all the roles and permissions by clearing the permission cache
+ $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
+ }
+```
+
+## Clear Cache When Using Seeders
+
+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 `DatabaseRefreshed` event:
+
+```php
+Event::listen(DatabaseRefreshed::class, function () {
+ $this->artisan('db:seed', ['--class' => RoleAndPermissionSeeder::class]);
+ $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
+});
+```
+
+Note that `PermissionRegistrar::forgetCachedPermissions()` is called 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.
+
+
+## Bypassing Cache When Testing
+
+The caching infrastructure for this package is "always on", but when running your test suite you may wish to reduce its impact.
+
+Two things you might wish to explore include:
+
+- Change the cache driver to `array`. **Very often you will have already done this in your `phpunit.xml` configuration.**
+
+- Shorten cache lifetime to 1 second, by setting the config (not necessary if cache driver is set to `array`) in your test suite TestCase:
+
+ `'permission.cache.expiration_time' = \DateInterval::createFromDateString('1 seconds')`
+
+
+## Testing Using 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.
+
+However, if your application allows users to define their own roles and permissions you may wish to use Model Factories to generate roles and permissions as part of your test suite.
+
+When using Laravel's class-based Model Factory features you will need to `extend` this package's `Role` and/or `Permission` model into your app's namespace, add the `HasFactory` trait to it, and define a model factory for it. Then you can use that factory in your seeders like any other factory related to your application's models.
diff --git a/docs/advanced-usage/timestamps.md b/docs/advanced-usage/timestamps.md
new file mode 100644
index 000000000..ca0b709fc
--- /dev/null
+++ b/docs/advanced-usage/timestamps.md
@@ -0,0 +1,19 @@
+---
+title: Timestamps
+weight: 10
+---
+
+## Excluding Timestamps from JSON
+
+If you want to exclude timestamps from JSON output of role/permission pivots, you can extend the Role and Permission models into your own App namespace and mark the pivot as hidden:
+
+```php
+ protected $hidden = ['pivot'];
+ ```
+
+## Adding Timestamps to Pivots
+
+If you want to add timestamps to your pivot tables, you can do it with a few steps:
+ - update the tables by calling `$table->timestamps();` in a migration
+ - extend the `Permission` and `Role` models and add `->withTimestamps();` to the `BelongsToMany` relationshps for `roles()` and `permissions()`
+ - update your `User` models (wherever you use the `HasRoles` or `HasPermissions` traits) by adding `->withTimestamps();` to the `BelongsToMany` relationships for `roles()` and `permissions()`
diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md
new file mode 100644
index 000000000..4e1d5d41d
--- /dev/null
+++ b/docs/advanced-usage/ui-options.md
@@ -0,0 +1,35 @@
+---
+title: UI Options
+weight: 11
+---
+
+## Need a UI?
+
+The package doesn't come with any UI/screens out of the box, you should build that yourself.
+
+But: [do you really need a UI? Consider what Aaron and Joel have to say in this podcast episode](https://show.nocompromises.io/episodes/should-you-manage-roles-and-permissions-with-a-ui)
+
+If you decide you need a UI, even if it's not for creating/editing role/permission names, but just for controlling which Users have access to which roles/permissions, following are some options to get you started:
+
+- [Code With Tony - video series](https://www.youtube.com/watch?v=lGfV1ddMhHA) to create an admin panel for managing roles and permissions in Laravel 9.
+
+- [FilamentPHP plugin](https://filamentphp.com/plugins/tharinda-rodrigo-spatie-roles-permissions) to manage roles and permissions using this package. (There are a few other Filament plugins which do similarly; use whichever suits your needs best.)
+
+- If you'd like to build your own UI, and understand the underlying logic for Gates and Roles and Users, the [Laravel 6 User Login and Management With Roles](https://www.youtube.com/watch?v=7PpJsho5aak&list=PLxFwlLOncxFLazmEPiB4N0iYc3Dwst6m4) video series by Mark Twigg of Penguin Digital gives thorough coverage to the topic, the theory, and implementation of a basic Roles system, independent of this Permissions Package.
+
+- [Laravel Nova package by @vyuldashev for managing Roles and Permissions](https://github.com/vyuldashev/nova-permission)
+
+- [Laravel Nova package by @paras-malhotra for managing Roles and Permissions and permissions based authorization for Nova Resources](https://github.com/insenseanalytics/laravel-nova-permission)
+
+- [How to create a UI for managing the permissions and roles](http://www.qcode.in/easy-roles-and-permissions-in-laravel-5-4/)
+
+- [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/aliqasemzadeh/bap) User management by [AliQasemzadeh](https://github.com/aliqasemzadeh)
+
+- [JetAdmin](https://github.com/aliqasemzadeh/jetadmin) JetAdmin use laravel livewire starter kit and manage permissions. [AliQasemzadeh](https://github.com/aliqasemzadeh)
+
+- [QuickPanel](https://github.com/aliqasemzadeh/quickpanel) Quick Panel (TALL Flowbite Starter Kit). [AliQasemzadeh](https://github.com/aliqasemzadeh)
diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md
new file mode 100644
index 000000000..806df99d6
--- /dev/null
+++ b/docs/advanced-usage/uuid.md
@@ -0,0 +1,183 @@
+---
+title: UUID/ULID
+weight: 7
+---
+
+If you're using UUIDs (ULID, GUID, etc) for your User models or Role/Permission models there are a few considerations to note.
+
+> NOTE: THIS IS NOT A FULL LESSON ON HOW TO IMPLEMENT UUIDs IN YOUR APP.
+
+Since each UUID implementation approach is different, some of these may or may not benefit you. As always, your implementation may vary.
+
+We use "uuid" in the examples below. Adapt for ULID or GUID as needed.
+
+## Migrations
+You will need to update the `create_permission_tables.php` migration after creating it with `php artisan vendor:publish`. After making your edits, be sure to run the migration!
+
+**User Models using UUIDs**
+If your User models are using `uuid` instead of `unsignedBigInteger` then you'll need to reflect the change in the migration provided by this package. Something like the following would be typical, for **both** `model_has_permissions` and `model_has_roles` tables:
+
+```diff
+// note: this is done in two places in the default migration file, so edit both places:
+- $table->unsignedBigInteger($columnNames['model_morph_key'])
++ $table->uuid($columnNames['model_morph_key'])
+```
+
+**Roles and Permissions using UUIDS**
+If you also want the roles and permissions to use a UUID for their `id` value, then you'll need to change the id fields accordingly, and manually set the primary key.
+
+```diff
+ Schema::create($tableNames['permissions'], function (Blueprint $table) {
+- $table->bigIncrements('id'); // permission id
++ $table->uuid('uuid')->primary()->unique(); // permission id
+//...
+ });
+
+ Schema::create($tableNames['roles'], function (Blueprint $table) {
+- $table->bigIncrements('id'); // role id
++ $table->uuid('uuid')->primary()->unique(); // role id
+//...
+ });
+
+ Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
+- $table->unsignedBigInteger($pivotPermission);
++ $table->uuid($pivotPermission);
+ $table->string('model_type');
+//...
+ $table->foreign($pivotPermission)
+- ->references('id') // permission id
++ ->references('uuid') // permission id
+ ->on($tableNames['permissions'])
+ ->onDelete('cascade');
+//...
+
+ Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
+- $table->unsignedBigInteger($pivotRole);
++ $table->uuid($pivotRole);
+//...
+ $table->foreign($pivotRole)
+- ->references('id') // role id
++ ->references('uuid') // role id
+ ->on($tableNames['roles'])
+ ->onDelete('cascade');//...
+
+ Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames) {
+- $table->unsignedBigInteger($pivotPermission);
+- $table->unsignedBigInteger($pivotRole);
++ $table->uuid($pivotPermission);
++ $table->uuid($pivotRole);
+
+ $table->foreign($pivotPermission)
+- ->references('id') // permission id
++ ->references('uuid') // permission id
+ ->on($tableNames['permissions'])
+ ->onDelete('cascade');
+
+ $table->foreign($pivotRole)
+- ->references('id') // role id
++ ->references('uuid') // role id
+ ->on($tableNames['roles'])
+ ->onDelete('cascade');
+```
+
+
+## Configuration (OPTIONAL)
+You might want to change the pivot table field name from `model_id` to `model_uuid`, just for semantic purposes.
+For this, in the `permission.php` configuration file edit `column_names.model_morph_key`:
+
+- OPTIONAL: Change to `model_uuid` instead of the default `model_id`.
+```diff
+ '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
+ * `model_id`.
+ *
+ * For example, this would be nice if your primary keys are all UUIDs. In
+ * that case, name this `model_uuid`.
+ */
+- 'model_morph_key' => 'model_id',
++ 'model_morph_key' => 'model_uuid',
+ ],
+```
+- If you extend the models into your app, be sure to list those models in your `permissions.php` configuration file. See the Extending section of the documentation and the Models section below.
+
+## Models
+If you want all the role/permission objects to have a UUID instead of an integer, you will need to Extend the default Role and Permission models into your own namespace in order to set some specific properties. (See the Extending section of the docs, where it explains requirements of Extending, as well as the `permissions.php` configuration settings you need to update.)
+
+Examples:
+
+Create new models, which extend the Role and Permission models of this package, and add Laravel's `HasUuids` trait (available since Laravel 9):
+```bash
+php artisan make:model Role
+php artisan make:model Permission
+```
+
+`App\Model\Role.php`
+```php
+ [
+
+ /*
+ * When using the "HasPermissions" trait from this package, we need to know which
+ * Eloquent model should be used to retrieve your permissions. Of course, it
+ * is often just the "Permission" model but you may use whatever you like.
+ *
+ * The model you want to use as a Permission model needs to implement the
+ * `Spatie\Permission\Contracts\Permission` contract.
+ */
+
+- 'permission' => Spatie\Permission\Models\Permission::class
++ 'permission' => \App\Models\Permission::class,
+
+ /*
+ * When using the "HasRoles" trait from this package, we need to know which
+ * Eloquent model should be used to retrieve your roles. Of course, it
+ * is often just the "Role" model but you may use whatever you like.
+ *
+ * The model you want to use as a Role model needs to implement the
+ * `Spatie\Permission\Contracts\Role` contract.
+ */
+
+- 'role' => Spatie\Permission\Models\Role::class,
++ 'role' => \App\Models\Role::class,
+
+ ],
+```
diff --git a/docs/basic-usage/_index.md b/docs/basic-usage/_index.md
new file mode 100644
index 000000000..682ba7ffa
--- /dev/null
+++ b/docs/basic-usage/_index.md
@@ -0,0 +1,4 @@
+---
+title: Basic Usage
+weight: 1
+---
diff --git a/docs/basic-usage/artisan.md b/docs/basic-usage/artisan.md
new file mode 100644
index 000000000..0e259da0c
--- /dev/null
+++ b/docs/basic-usage/artisan.md
@@ -0,0 +1,61 @@
+---
+title: Artisan Commands
+weight: 10
+---
+
+## Creating roles and permissions with Artisan Commands
+
+You can create a role or permission from the console with artisan commands.
+
+```bash
+php artisan permission:create-role writer
+```
+
+```bash
+php artisan permission:create-permission "edit articles"
+```
+
+When creating permissions/roles for specific guards you can specify the guard names as a second argument:
+
+```bash
+php artisan permission:create-role writer web
+```
+
+```bash
+php artisan permission:create-permission "edit articles" web
+```
+
+When creating roles you can also create and link permissions at the same time:
+
+```bash
+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:
+
+```bash
+php artisan permission:show
+```
+
+## Resetting the Cache
+
+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.
+
+See the Advanced-Usage/Cache section of these docs for detailed specifics.
+
+If you need to manually reset the cache for this package, you may use the following artisan command:
+
+```bash
+php artisan permission:cache-reset
+```
+
+Again, it is more efficient to use the API provided by this package, instead of manually clearing the cache.
diff --git a/docs/basic-usage/basic-usage.md b/docs/basic-usage/basic-usage.md
new file mode 100644
index 000000000..76e41e040
--- /dev/null
+++ b/docs/basic-usage/basic-usage.md
@@ -0,0 +1,114 @@
+---
+title: Basic Usage
+weight: 1
+---
+
+## Add The Trait
+First, add the `Spatie\Permission\Traits\HasRoles` trait to your `User` model(s):
+
+```php
+use Illuminate\Foundation\Auth\User as Authenticatable;
+use Spatie\Permission\Traits\HasRoles;
+
+class User extends Authenticatable
+{
+ use HasRoles;
+
+ // ...
+}
+```
+
+## Create A Permission
+This package allows for users to be associated with permissions and roles. Every role is associated with multiple permissions.
+A `Role` and a `Permission` are regular Eloquent models. They require a `name` and can be created like this:
+
+```php
+use Spatie\Permission\Models\Role;
+use Spatie\Permission\Models\Permission;
+
+$role = Role::create(['name' => 'writer']);
+$permission = Permission::create(['name' => 'edit articles']);
+```
+
+## Assign A Permission To A Role
+A permission can be assigned to a role using either of these methods:
+
+```php
+$role->givePermissionTo($permission);
+$permission->assignRole($role);
+```
+
+## Sync Permissions To A Role
+Multiple permissions can be synced to a role using either of these methods:
+
+```php
+$role->syncPermissions($permissions);
+$permission->syncRoles($roles);
+```
+
+## Remove Permission From A Role
+A permission can be removed from a role using either of these methods:
+
+```php
+$role->revokePermissionTo($permission);
+$permission->removeRole($role);
+```
+
+## Guard Name
+If you're using multiple guards then the `guard_name` attribute must be set as well. Read about it in the [using multiple guards](./multiple-guards) documentation.
+
+## Get Permissions For A User
+The `HasRoles` trait adds Eloquent relationships to your models, which can be accessed directly or used as a base query:
+
+```php
+// get a list of all permissions directly assigned to the user
+$permissionNames = $user->getPermissionNames(); // collection of name strings
+$permissions = $user->permissions; // collection of permission objects
+
+// get all permissions for the user, either directly, or from roles, or from both
+$permissions = $user->getDirectPermissions();
+$permissions = $user->getPermissionsViaRoles();
+$permissions = $user->getAllPermissions();
+
+// get the names of the user's roles
+$roles = $user->getRoleNames(); // Returns a collection
+```
+
+## Scopes
+The `HasRoles` trait also adds `role` and `withoutRole` scopes to your models to scope the query to certain roles or permissions:
+
+```php
+$users = User::role('writer')->get(); // Returns only users with the role 'writer'
+$nonEditors = User::withoutRole('editor')->get(); // Returns only users without the role 'editor'
+```
+
+The `role` and `withoutRole` scopes can accept a string, a `\Spatie\Permission\Models\Role` object or an `\Illuminate\Support\Collection` object.
+
+The same trait also adds scopes to only get users that have or don't have a certain permission.
+
+```php
+$users = User::permission('edit articles')->get(); // Returns only users with the permission 'edit articles' (inherited or directly)
+$usersWhoCannotEditArticles = User::withoutPermission('edit articles')->get(); // Returns all users without the permission 'edit articles' (inherited or directly)
+```
+
+The scope can accept a string, a `\Spatie\Permission\Models\Permission` object or an `\Illuminate\Support\Collection` object.
+
+
+## Eloquent Calls
+Since Role and Permission models are extended from Eloquent models, basic Eloquent calls can be used as well:
+
+```php
+$allUsersWithAllTheirRoles = User::with('roles')->get();
+$allUsersWithAllTheirDirectPermissions = User::with('permissions')->get();
+$allRolesInDatabase = Role::all()->pluck('name');
+$usersWithoutAnyRoles = User::doesntHave('roles')->get();
+$allRolesExceptAandB = Role::whereNotIn('name', ['role A', 'role B'])->get();
+```
+
+## Counting Users Having A Role
+One way to count all users who have a certain role is by filtering the collection of all Users with their Roles:
+```php
+$managersCount = User::with('roles')->get()->filter(
+ fn ($user) => $user->roles->where('name', 'Manager')->toArray()
+)->count();
+```
diff --git a/docs/basic-usage/blade-directives.md b/docs/basic-usage/blade-directives.md
new file mode 100644
index 000000000..56629be32
--- /dev/null
+++ b/docs/basic-usage/blade-directives.md
@@ -0,0 +1,118 @@
+---
+title: Blade directives
+weight: 7
+---
+
+## Permissions
+This package lets you use Laravel's native `@can` directive to check if a user has a certain permission (whether you gave them that permission directly or if you granted it indirectly via a role):
+
+```php
+@can('edit articles')
+ //
+@endcan
+```
+or
+```php
+@if(auth()->user()->can('edit articles') && $some_other_condition)
+ //
+@endif
+```
+
+You can use `@can`, `@cannot`, `@canany`, and `@guest` to test for permission-related access.
+
+When using a permission-name associated with permissions created in this package, you can use `@can('permission-name', 'guard_name')` if you need to check against a specific guard.
+
+Example:
+```php
+@can('edit articles', 'guard_name')
+ //
+@endcan
+```
+
+You can also use `@haspermission('permission-name')` or `@haspermission('permission-name', 'guard_name')` in similar fashion. With corresponding `@endhaspermission`.
+
+There is no `@hasanypermission` directive: use `@canany` instead.
+
+
+## Roles
+As discussed in the Best Practices section of the docs, **it is strongly recommended to always use permission directives**, instead of role directives.
+
+Additionally, if your reason for testing against Roles is for a Super-Admin, see the *Defining A Super-Admin* section of the docs.
+
+If you actually need to directly test for Roles, this package offers some Blade directives to verify whether the currently logged in user has all or any of a given list of roles.
+
+Optionally you can pass in the `guard` that the check will be performed on as a second argument.
+
+## Blade and Roles
+Check for a specific role:
+```php
+@role('writer')
+ I am a writer!
+@else
+ I am not a writer...
+@endrole
+```
+is the same as
+```php
+@hasrole('writer')
+ I am a writer!
+@else
+ I am not a writer...
+@endhasrole
+```
+which is also the same as
+```php
+@if(auth()->user()->hasRole('writer'))
+ //
+@endif
+```
+
+Check for any role in a list:
+```php
+@hasanyrole($collectionOfRoles)
+ I have one or more of these roles!
+@else
+ I have none of these roles...
+@endhasanyrole
+// or
+@hasanyrole('writer|admin')
+ I am either a writer or an admin or both!
+@else
+ I have none of these roles...
+@endhasanyrole
+```
+Check for all roles:
+
+```php
+@hasallroles($collectionOfRoles)
+ I have all of these roles!
+@else
+ I do not have all of these roles...
+@endhasallroles
+// or
+@hasallroles('writer|admin')
+ I am both a writer and an admin!
+@else
+ I do not have all of these roles...
+@endhasallroles
+```
+
+Alternatively, `@unlessrole` gives the reverse for checking a singular role, like this:
+
+```php
+@unlessrole('does not have this role')
+ I do not have the role
+@else
+ I do have the role
+@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/direct-permissions.md b/docs/basic-usage/direct-permissions.md
new file mode 100644
index 000000000..6b4987ec5
--- /dev/null
+++ b/docs/basic-usage/direct-permissions.md
@@ -0,0 +1,81 @@
+---
+title: Direct Permissions
+weight: 2
+---
+
+## Best Practice
+
+INSTEAD OF DIRECT PERMISSIONS, it is better to assign permissions to Roles, and then assign Roles to Users.
+
+See the [Roles vs Permissions](../best-practices/roles-vs-permissions) section of the docs for a deeper explanation.
+
+HOWEVER, If you have reason to directly assign individual permissions to specific users (instead of to roles which are assigned to those users), you can do that as well:
+
+## Direct Permissions to Users
+
+### Giving/Revoking direct permissions
+
+A permission can be given to any user:
+
+```php
+$user->givePermissionTo('edit articles');
+
+// You can also give multiple permission at once
+$user->givePermissionTo('edit articles', 'delete articles');
+
+// You may also pass an array
+$user->givePermissionTo(['edit articles', 'delete articles']);
+```
+
+A permission can be revoked from a user:
+
+```php
+$user->revokePermissionTo('edit articles');
+```
+
+Or revoke & add new permissions in one go:
+
+```php
+$user->syncPermissions(['edit articles', 'delete articles']);
+```
+
+## Checking Direct Permissions
+Like all permissions assigned via roles, you can check if a user has a permission by using Laravel's default `can` function. This will also allow you to use Super-Admin features provided by Laravel's Gate:
+
+```php
+$user->can('edit articles');
+```
+
+> NOTE: The following `hasPermissionTo`, `hasAnyPermission`, `hasAllPermissions` functions do not support Super-Admin functionality. Use `can`, `canAny` instead.
+
+You can check if a user has a permission:
+
+```php
+$user->hasPermissionTo('edit articles');
+```
+
+Or you may pass an integer representing the permission id
+
+```php
+$user->hasPermissionTo('1');
+$user->hasPermissionTo(Permission::find(1)->id);
+$user->hasPermissionTo($somePermission->id);
+```
+
+You can check if a user has Any of an array of permissions:
+
+```php
+$user->hasAnyPermission(['edit articles', 'publish articles', 'unpublish articles']);
+```
+
+...or if a user has All of an array of permissions:
+
+```php
+$user->hasAllPermissions(['edit articles', 'publish articles', 'unpublish articles']);
+```
+
+You may also pass integers to lookup by permission id
+
+```php
+$user->hasAnyPermission(['edit articles', 1, 5]);
+```
diff --git a/docs/basic-usage/enums.md b/docs/basic-usage/enums.md
new file mode 100644
index 000000000..23a4167f4
--- /dev/null
+++ b/docs/basic-usage/enums.md
@@ -0,0 +1,105 @@
+---
+title: Enums
+weight: 4
+---
+
+## Enum Prerequisites
+
+Requires `version 6` of this package.
+
+Requires PHP 8.1 or higher.
+
+If you are using PHP 8.1+ you can implement Enums as native types.
+
+Internally, Enums implicitly implement `\BackedEnum`, which is how this package recognizes that you're passing an Enum.
+
+NOTE: Presently (version 6) this package does not support using `$casts` to specify enums on the `Permission` model. You can still use enums to reference things as shown below, just without declaring it in a `$casts` property.
+
+
+## Code Requirements
+
+You can create your Enum object for use with Roles and/or Permissions. You will probably create separate Enums for Roles and for Permissions, although if your application needs are simple you might choose a single Enum for both.
+
+Usually the list of application Roles is much shorter than the list of Permissions, so having separate objects for them can make them easier to manage.
+
+Here is an example Enum for Roles. You would do similarly for Permissions.
+
+```php
+namespace App\Enums;
+
+enum RolesEnum: string
+{
+ // case NAMEINAPP = 'name-in-database';
+
+ case WRITER = 'writer';
+ case EDITOR = 'editor';
+ case USERMANAGER = 'user-manager';
+
+ // extra helper to allow for greater customization of displayed values, without disclosing the name/value data directly
+ public function label(): string
+ {
+ return match ($this) {
+ static::WRITER => 'Writers',
+ static::EDITOR => 'Editors',
+ static::USERMANAGER => 'User Managers',
+ };
+ }
+}
+```
+
+## Creating Roles/Permissions using Enums
+
+When **creating** roles/permissions, you cannot pass an Enum name directly, because Eloquent expects a string for the name.
+
+You must manually convert the name to its value in order to pass the correct string to Eloquent for the role/permission name.
+
+eg: use `RolesEnum::WRITER->value` when specifying the role/permission name
+
+```php
+ $role = app(Role::class)->findOrCreate(RolesEnum::WRITER->value, 'web');
+```
+Same with creating Permissions.
+
+### Authorizing using Enums
+
+In your application code, when checking for authorization using features of this package, you can use `MyEnum::NAME` directly in most cases, without passing `->value` to convert to a string.
+
+There may occasionally be times where you will need to manually fallback to adding `->value` (eg: `MyEnum::NAME->value`) when using features that aren't aware of Enum support, such as when you need to pass `string` values instead of an `Enum` to a function that doesn't recognize Enums (Prior to Laravel v11.23.0 the framework didn't support Enums when interacting with Gate via the `can()` methods/helpers (eg: `can`, `canAny`, etc)).
+
+Examples:
+```php
+// the following are identical because `hasPermissionTo` is aware of `BackedEnum` support:
+$user->hasPermissionTo(PermissionsEnum::VIEWPOSTS);
+$user->hasPermissionTo(PermissionsEnum::VIEWPOSTS->value);
+
+// when calling Gate features, such as Model Policies, etc, prior to Laravel v11.23.0
+$user->can(PermissionsEnum::VIEWPOSTS->value);
+$model->can(PermissionsEnum::VIEWPOSTS->value);
+
+// Blade directives:
+@can(PermissionsEnum::VIEWPOSTS->value)
+```
+
+
+## Package methods supporting BackedEnums:
+The following methods of this package support passing `BackedEnum` parameters directly:
+
+```php
+ $user->assignRole(RolesEnum::WRITER);
+ $user->removeRole(RolesEnum::EDITOR);
+
+ $role->givePermissionTo(PermissionsEnum::EDITPOSTS);
+ $role->revokePermissionTo(PermissionsEnum::EDITPOSTS);
+
+ $user->givePermissionTo(PermissionsEnum::EDITPOSTS);
+ $user->revokePermissionTo(PermissionsEnum::EDITPOSTS);
+
+ $user->hasPermissionTo(PermissionsEnum::EDITPOSTS);
+ $user->hasAnyPermission([PermissionsEnum::EDITPOSTS, PermissionsEnum::VIEWPOSTS]);
+ $user->hasDirectPermission(PermissionsEnum::EDITPOSTS);
+
+ $user->hasRole(RolesEnum::WRITER);
+ $user->hasAllRoles([RolesEnum::WRITER, RolesEnum::EDITOR]);
+ $user->hasExactRoles([RolesEnum::WRITER, RolesEnum::EDITOR, RolesEnum::MANAGER]);
+
+```
diff --git a/docs/basic-usage/middleware.md b/docs/basic-usage/middleware.md
new file mode 100644
index 000000000..e9173ec59
--- /dev/null
+++ b/docs/basic-usage/middleware.md
@@ -0,0 +1,126 @@
+---
+title: Middleware
+weight: 11
+---
+
+## Default Middleware
+
+For checking against a single permission (see Best Practices) using `can`, you can use the built-in Laravel middleware provided by `\Illuminate\Auth\Middleware\Authorize::class` like this:
+
+```php
+Route::group(['middleware' => ['can:publish articles']], function () { ... });
+
+// or with static method (requires Laravel 10.9+)
+Route::group(['middleware' => [\Illuminate\Auth\Middleware\Authorize::using('publish articles')]], function () { ... });
+```
+
+## Package Middleware
+
+**See a typo? Note that since v6 the _'Middleware'_ namespace is singular. Prior to v6 it was _'Middlewares'_. Time to upgrade your implementation!**
+
+This package comes with `RoleMiddleware`, `PermissionMiddleware` and `RoleOrPermissionMiddleware` middleware.
+
+You can register their aliases for easy reference elsewhere in your app:
+
+In Laravel 11+ open `/bootstrap/app.php` and register them there:
+
+```php
+ ->withMiddleware(function (Middleware $middleware) {
+ $middleware->alias([
+ 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
+ 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
+ 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
+ ]);
+ })
+```
+
+In Laravel 9 and 10 you can add them in `app/Http/Kernel.php`:
+
+```php
+// Laravel 9 uses $routeMiddleware = [
+//protected $routeMiddleware = [
+// Laravel 10+ uses $middlewareAliases = [
+protected $middlewareAliases = [
+ // ...
+ 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
+ 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
+ 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
+];
+```
+
+### Middleware Priority
+If your app is triggering *404 Not Found* responses when a *403 Not Authorized* response might be expected, it might be a middleware priority clash. Explore reordering priorities so that this package's middleware runs before Laravel's `SubstituteBindings` middleware. (See [Middleware docs](https://laravel.com/docs/master/middleware#sorting-middleware) ).
+
+In Laravel 11 you could explore `$middleware->prependToGroup()` instead. See the Laravel Documentation for details.
+
+
+## Using Middleware in Routes and Controllers
+
+After you have registered the aliases as shown above, you can use them in your Routes and Controllers much the same way you use any other middleware:
+
+### Routes
+
+```php
+Route::group(['middleware' => ['role:manager']], function () { ... });
+Route::group(['middleware' => ['permission:publish articles']], function () { ... });
+Route::group(['middleware' => ['role_or_permission:publish articles']], function () { ... });
+
+// for a specific guard:
+Route::group(['middleware' => ['role:manager,api']], function () { ... });
+
+// multiple middleware
+Route::group(['middleware' => ['role:manager','permission:publish articles']], function () { ... });
+```
+
+You can specify multiple roles or permissions with a `|` (pipe) character, which is treated as `OR`:
+
+```php
+Route::group(['middleware' => ['role:manager|writer']], function () { ... });
+Route::group(['middleware' => ['permission:publish articles|edit articles']], function () { ... });
+Route::group(['middleware' => ['role_or_permission:manager|edit articles']], function () { ... });
+
+// for a specific guard
+Route::group(['middleware' => ['permission:publish articles|edit articles,api']], function () { ... });
+```
+
+### Controllers
+
+In Laravel 11, if your controller implements the `HasMiddleware` interface, you can register [controller middleware](https://laravel.com/docs/11.x/controllers#controller-middleware) using the `middleware()` method:
+
+```php
+public static function middleware(): array
+{
+ return [
+ // examples with aliases, pipe-separated names, guards, etc:
+ 'role_or_permission:manager|edit articles',
+ new Middleware('role:author', only: ['index']),
+ new Middleware(\Spatie\Permission\Middleware\RoleMiddleware::using('manager'), except:['show']),
+ new Middleware(\Spatie\Permission\Middleware\PermissionMiddleware::using('delete records,api'), only:['destroy']),
+ ];
+}
+```
+
+In Laravel 10 and older, you can register it in the constructor:
+```php
+public function __construct()
+{
+ // examples:
+ $this->middleware(['role:manager','permission:publish articles|edit articles']);
+ $this->middleware(['role_or_permission:manager|edit articles']);
+ // or with specific guard
+ $this->middleware(['role_or_permission:manager|edit articles,api']);
+}
+```
+
+You can also use Laravel's Model Policy feature in your controller methods. See the Model Policies section of these docs.
+
+## Middleware via Static Methods
+
+All of the middleware can also be applied by calling the static `using` method, which accepts either an array or a `|`-separated string as input.
+
+```php
+Route::group(['middleware' => [\Spatie\Permission\Middleware\RoleMiddleware::using('manager')]], function () { ... });
+Route::group(['middleware' => [\Spatie\Permission\Middleware\PermissionMiddleware::using('publish articles|edit articles')]], function () { ... });
+Route::group(['middleware' => [\Spatie\Permission\Middleware\RoleOrPermissionMiddleware::using(['manager', 'edit articles'])]], function () { ... });
+```
+
diff --git a/docs/basic-usage/multiple-guards.md b/docs/basic-usage/multiple-guards.md
new file mode 100644
index 000000000..062e49fad
--- /dev/null
+++ b/docs/basic-usage/multiple-guards.md
@@ -0,0 +1,80 @@
+---
+title: Using multiple guards
+weight: 9
+---
+
+When using the default Laravel auth configuration all of the core methods of this package will work out of the box, with no extra configuration required.
+
+However, when using multiple guards they will act like namespaces for your permissions and roles: Every guard has its own set of permissions and roles that can be assigned to its user model.
+
+## The Downside To Multiple Guards
+
+Note that this package requires you to register a permission name (same for roles) for each guard you want to authenticate with. So, "edit-article" would have to be created multiple times for each guard your app uses. An exception will be thrown if you try to authenticate against a non-existing permission+guard combination. Same for roles.
+
+> **Tip**: If your app uses only a single guard, but it is not `web` (Laravel's default, which shows "first" in the auth config file) then change the order of your listed guards in your `config/auth.php` to list your primary guard as the default and as the first in the list of defined guards. While you're editing that file, it is best to remove any guards you don't use, too.
+>
+> OR you could use the suggestion below to force the use of a single guard:
+
+## Forcing Use Of A Single Guard
+
+If your app structure does NOT differentiate between guards when it comes to roles/permissions, (ie: if ALL your roles/permissions are the SAME for ALL guards), you can override the `getDefaultGuardName` function by adding it to your User model, and specifying your desired `$guard_name`. Then you only need to create roles/permissions for that single `$guard_name`, not duplicating them. The example here sets it to `web`, but use whatever your application's default is:
+
+```php
+ protected string $guard_name = 'web';
+ protected function getDefaultGuardName(): string { return $this->guard_name; }
+````
+
+
+## Using permissions and roles with multiple guards
+
+When creating new permissions and roles, if no guard is specified, then the **first** defined guard in `auth.guards` config array will be used.
+
+```php
+// Create a manager role for users authenticating with the admin guard:
+$role = Role::create(['guard_name' => 'admin', 'name' => 'manager']);
+
+// Define a `publish articles` permission for the admin users belonging to the admin guard
+$permission = Permission::create(['guard_name' => 'admin', 'name' => 'publish articles']);
+
+// Define a *different* `publish articles` permission for the regular users belonging to the web guard
+$permission = Permission::create(['guard_name' => 'web', 'name' => 'publish articles']);
+```
+
+To check if a user has permission for a specific guard:
+
+```php
+$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 (may return a string or array);
+- then the `$guard_name` property if it exists on the model (may return a string or array);
+- 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).
+
+
+## Assigning permissions and roles to guard users
+
+You can use the same core methods to assign permissions and roles to users; just make sure the `guard_name` on the permission or role matches the guard of the user, otherwise a `GuardDoesNotMatch` or `Role/PermissionDoesNotExist` exception will be thrown.
+
+If your User is able to consume multiple roles or permissions from different guards; make sure the User class's `$guard_name` property or `guardName()` method returns all allowed guards as an array:
+
+```php
+ protected $guard_name = ['web', 'admin'];
+````
+or
+```php
+ public function guardName() { return ['web', 'admin']; }
+````
+
+## Using blade directives with multiple guards
+
+You can use all of the blade directives offered by this package by passing in the guard you wish to use as the second argument to the directive:
+
+```php
+@role('super-admin', 'admin')
+ I am a super-admin!
+@else
+ I am not a super-admin...
+@endrole
+```
diff --git a/docs/basic-usage/new-app.md b/docs/basic-usage/new-app.md
new file mode 100644
index 000000000..04cf9d212
--- /dev/null
+++ b/docs/basic-usage/new-app.md
@@ -0,0 +1,218 @@
+---
+title: Example App
+weight: 90
+---
+
+## Creating A Demo App
+
+If you want to just try out the features of this package you can get started with the following.
+
+The examples on this page are primarily added for assistance in creating a quick demo app for troubleshooting purposes, to post the repo on github for convenient sharing to collaborate or get support.
+
+If you're new to Laravel or to any of the concepts mentioned here, you can learn more in the [Laravel documentation](https://laravel.com/docs/) and in the free videos at Laracasts such as with the [Laravel 11 in 30 days](https://laracasts.com/series/30-days-to-learn-laravel-11) or [Laravel 8 From Scratch](https://laracasts.com/series/laravel-8-from-scratch/) series.
+
+### Initial setup:
+
+```sh
+cd ~/Sites
+laravel new mypermissionsdemo
+# (No Starter Kit is needed, but you could go with Livewire or Breeze/Jetstream, with Laravel's Built-In-Auth; or use Bootstrap using laravel/ui described later, below)
+# (You might be asked to select a dark-mode-support choice)
+# (Choose your desired testing framework: Pest or PHPUnit)
+# (If offered, say Yes to initialize a Git repo, so that you can track your code changes)
+# (If offered a database selection, choose SQLite, because it is simplest for test scenarios)
+# (If prompted, say Yes to run default database migrations)
+# (If prompted, say Yes to run npm install and related commands)
+
+cd mypermissionsdemo
+
+# The following git commands are not needed if you Initialized a git repo while "laravel new" was running above:
+git init
+git add .
+git commit -m "Fresh Laravel Install"
+
+# These Environment steps are not needed if you already selected SQLite while "laravel new" was running above:
+cp -n .env.example .env
+sed -i '' 's/DB_CONNECTION=mysql/DB_CONNECTION=sqlite/' .env
+sed -i '' 's/DB_DATABASE=/#DB_DATABASE=/' .env
+touch database/database.sqlite
+
+# Package
+composer require spatie/laravel-permission
+php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
+git add .
+git commit -m "Add Spatie Laravel Permissions package"
+php artisan migrate:fresh
+
+# Add `HasRoles` trait to User model
+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"
+```
+
+If you didn't install a Starter Kit like Livewire or Breeze or Jetstream, add Laravel's basic auth scaffolding:
+This Auth scaffolding will make it simpler to provide login capability for a test/demo user, and test roles/permissions with them.
+```php
+composer require laravel/ui --dev
+php artisan ui bootstrap --auth
+# npm install && npm run build
+git add . && git commit -m "Setup auth scaffold"
+```
+
+### Add some basic permissions
+- Add a new file, `/database/seeders/PermissionsDemoSeeder.php` such as the following (You could create it with `php artisan make:seed` and then edit the file accordingly):
+
+```php
+forgetCachedPermissions();
+
+ // create permissions
+ Permission::create(['name' => 'edit articles']);
+ Permission::create(['name' => 'delete articles']);
+ Permission::create(['name' => 'publish articles']);
+ Permission::create(['name' => 'unpublish articles']);
+
+ // create roles and assign existing permissions
+ $role1 = Role::create(['name' => 'writer']);
+ $role1->givePermissionTo('edit articles');
+ $role1->givePermissionTo('delete articles');
+
+ $role2 = Role::create(['name' => 'admin']);
+ $role2->givePermissionTo('publish articles');
+ $role2->givePermissionTo('unpublish articles');
+
+ $role3 = Role::create(['name' => 'Super-Admin']);
+ // gets all permissions via Gate::before rule; see AuthServiceProvider
+
+ // create demo users
+ $user = \App\Models\User::factory()->create([
+ 'name' => 'Example User',
+ 'email' => 'tester@example.com',
+ ]);
+ $user->assignRole($role1);
+
+ $user = \App\Models\User::factory()->create([
+ 'name' => 'Example Admin User',
+ 'email' => 'admin@example.com',
+ ]);
+ $user->assignRole($role2);
+
+ $user = \App\Models\User::factory()->create([
+ 'name' => 'Example Super-Admin User',
+ 'email' => 'superadmin@example.com',
+ ]);
+ $user->assignRole($role3);
+ }
+}
+
+```
+
+- re-migrate and seed the database:
+
+```sh
+php artisan migrate:fresh --seed --seeder=PermissionsDemoSeeder
+```
+
+### Grant Super-Admin access
+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` (or `AppServiceProvider` since Laravel 11):
+
+```diff
++ use Illuminate\Support\Facades\Gate;
+
+ public function boot()
+ {
++ // Implicitly grant "Super-Admin" role all permission checks using can()
++ Gate::before(function ($user, $ability) {
++ if ($user->hasRole('Super-Admin')) {
++ return true;
++ }
++ });
+ }
+```
+
+
+### Application Code
+The permissions created in the seeder above imply that there will be some sort of Posts or Article features, and that various users will have various access control levels to manage/view those objects.
+
+Your app will have Models, Controllers, routes, Views, Factories, Policies, Tests, middleware, and maybe additional Seeders.
+
+You can see examples of these in the demo app at https://github.com/drbyte/spatie-permissions-demo/
+
+
+### Quick Examples
+If you are creating a demo app for reporting a bug or getting help with troubleshooting something, skip this section and proceed to "Sharing" below.
+
+If this is your first app with this package, you may want some quick permission examples to see it in action. If you've set up your app using the instructions above, the following examples will work in conjunction with the users and permissions created in the seeder.
+
+Three users were created: tester@example.com, admin@example.com, superadmin@example.com and the password for each is "password".
+
+`/resources/views/dashboard.php`
+```diff
+
+ {{ __("You're logged in!") }}
+
++ @can('edit articles')
++ You can EDIT ARTICLES.
++ @endcan
++ @can('publish articles')
++ You can PUBLISH ARTICLES.
++ @endcan
++ @can('only super-admins can see this section')
++ Congratulations, you are a super-admin!
++ @endcan
+```
+With the above code, when you login with each respective user, you will see different messages based on that access.
+
+Here's a routes example with Breeze and Laravel 11.
+Edit `/routes/web.php`:
+```diff
+-Route::middleware('auth')->group(function () {
++Route::middleware('role_or_permission:publish articles')->group(function () {
+ Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit');
+ Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update');
+ Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
+});
+```
+With the above change, you will be unable to access the user "Profile" page unless you are logged in with "admin" or "super-admin". You could change `role_or_permission:publish_articles` to `role:writer` to make it only available to the "test" user.
+
+## Sharing
+To share your app on Github for easy collaboration:
+
+- create a new public repository on Github, without any extras like readme/etc.
+- follow github's sample code for linking your local repo and uploading the code. It will look like this:
+
+```sh
+git remote add origin git@github.com:YOURUSERNAME/REPONAME.git
+git push -u origin main
+```
+The above only needs to be done once.
+
+- then add the rest of your code by making new commits:
+
+```sh
+git add .
+git commit -m "Explain what your commit is about here"
+git push origin main
+```
+Repeat the above process whenever you change code that you want to share.
+
+Those are the basics!
diff --git a/docs/basic-usage/passport.md b/docs/basic-usage/passport.md
new file mode 100644
index 000000000..9bfe86d50
--- /dev/null
+++ b/docs/basic-usage/passport.md
@@ -0,0 +1,58 @@
+---
+title: Passport Client Credentials Grant usage
+weight: 12
+---
+
+**NOTE** currently this only works for Laravel 9 and Passport 11 and newer.
+
+## Install Passport
+First of all make sure to have Passport installed as described in the [Laravel documentation](https://laravel.com/docs/master/passport).
+
+## Extend the Client model
+After installing the Passport package we need to extend Passports Client model.
+The extended Client model should look like something as shown below.
+
+```php
+use Illuminate\Contracts\Auth\Access\Authorizable as AuthorizableContract;
+use Illuminate\Foundation\Auth\Access\Authorizable;
+use Laravel\Passport\Client as BaseClient;
+use Spatie\Permission\Traits\HasRoles;
+
+class Client extends BaseClient implements AuthorizableContract
+{
+ use HasRoles;
+ use Authorizable;
+
+ public $guard_name = 'api';
+
+ // or
+
+ public function guardName()
+ {
+ return 'api';
+ }
+}
+```
+
+You need to extend the Client model to make it possible to add the required traits and properties/ methods.
+The extended Client should either provide a `$guard_name` property or a `guardName()` method.
+They should return a string that matches the [configured](https://laravel.com/docs/master/passport#installation) guard name for the passport driver.
+
+To tell Passport to use this extended Client, add the rule below to the `boot` method of your `App\Providers\AuthServiceProvider` class.
+```php
+Passport::useClientModel(\App\Models\Client::class); // Use the namespace of your extended Client.
+```
+
+## Middleware
+All middleware provided by this package work with the Client.
+
+Do make sure that you only wrap your routes in the [`client`](https://laravel.com/docs/master/passport#via-middleware) middleware and not the `auth:api` middleware as well.
+Wrapping routes in the `auth:api` middleware currently does not work for the Client Credentials Grant.
+
+## Config
+Finally, update the config file as well. Setting `use_passport_client_credentials` to `true` will make sure that the right checks are performed.
+
+```php
+// config/permission.php
+'use_passport_client_credentials' => true,
+```
diff --git a/docs/basic-usage/role-permissions.md b/docs/basic-usage/role-permissions.md
new file mode 100644
index 000000000..20eeedd5e
--- /dev/null
+++ b/docs/basic-usage/role-permissions.md
@@ -0,0 +1,174 @@
+---
+title: Using Permissions via Roles
+weight: 3
+---
+
+## Assigning Roles
+
+A role can be assigned to any user:
+
+```php
+$user->assignRole('writer');
+
+// You can also assign multiple roles at once
+$user->assignRole('writer', 'admin');
+// or as an array
+$user->assignRole(['writer', 'admin']);
+```
+
+A role can be removed from a user:
+
+```php
+$user->removeRole('writer');
+```
+
+Roles can also be synced:
+
+```php
+// All current roles will be removed from the user and replaced by the array given
+$user->syncRoles(['writer', 'admin']);
+```
+
+## Checking Roles
+
+You can determine if a user has a certain role:
+
+```php
+$user->hasRole('writer');
+
+// or at least one role from an array of roles:
+$user->hasRole(['editor', 'moderator']);
+```
+
+You can also determine if a user has any of a given list of roles:
+
+```php
+$user->hasAnyRole(['writer', 'reader']);
+// or
+$user->hasAnyRole('writer', 'reader');
+```
+
+You can also determine if a user has all of a given list of roles:
+
+```php
+$user->hasAllRoles(Role::all());
+```
+
+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
+$role->givePermissionTo('edit articles');
+```
+
+You can determine if a role has a certain permission:
+
+```php
+$role->hasPermissionTo('edit articles');
+```
+
+A permission can be revoked from a role:
+
+```php
+$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.
+
+
+**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:
+
+```php
+$role = Role::findByName('writer');
+$role->givePermissionTo('edit articles');
+
+$user->assignRole('writer');
+
+$user->givePermissionTo('delete articles');
+```
+
+In the above example, a role is given permission to edit articles and this role is assigned to a user.
+Now the user can edit articles and additionally delete articles. The permission of 'delete articles' is the user's direct permission because it is assigned directly to them.
+When we call `$user->hasDirectPermission('delete articles')` it returns `true`,
+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 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']);
+
+// Check if the user has Any permission directly
+$user->hasAnyDirectPermission(['create articles', 'delete articles']);
+```
+By following the previous example, when we call `$user->hasAllDirectPermissions(['edit articles', 'delete articles'])`
+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 examine all of these permissions:
+
+```php
+// Direct permissions
+$user->getDirectPermissions() // Or $user->permissions;
+
+// Permissions inherited from the user's roles
+$user->getPermissionsViaRoles();
+
+// All permissions which apply on the user (inherited and direct)
+$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.
+
diff --git a/docs/basic-usage/super-admin.md b/docs/basic-usage/super-admin.md
new file mode 100644
index 000000000..00a2af27f
--- /dev/null
+++ b/docs/basic-usage/super-admin.md
@@ -0,0 +1,76 @@
+---
+title: Defining a Super-Admin
+weight: 8
+---
+
+We strongly recommend that a Super-Admin be handled by setting a global `Gate::before` or `Gate::after` rule which checks for the desired role.
+
+Then you can implement the best-practice of primarily using permission-based controls (@can and $user->can, etc) throughout your app, without always having to check for "is this a super-admin" everywhere. **Best not to use role-checking (ie: `hasRole`) (except here in Gate/Policy rules) when you have Super Admin features like this.**
+
+## Gate::before/Policy::before vs HasPermissionTo / HasAnyPermission / HasDirectPermission / HasAllPermissions
+IMPORTANT:
+The Gate::before is the best approach for Super-Admin functionality, and aligns well with the described "Best Practices" of using roles as a way of grouping permissions, and assigning that access to Users. Using this approach, you can/must call Laravel's standard `can()`, `canAny()`, `cannot()`, etc checks for permission authorization to get a correct Super response.
+
+### HasPermissionTo, HasAllPermissions, HasAnyPermission, HasDirectPermission
+Calls to this package's internal API which bypass Laravel's Gate (such as a direct call to `->hasPermissionTo()`) will not go through the Gate, and thus will not get the Super response, unless you have actually added that specific permission to the Super-Admin "role".
+
+The only reason for giving specific permissions to a Super-Admin role is if you intend to call the `has` methods directly instead of the Gate's `can()` methods.
+
+
+## `Gate::before`
+If you want a "Super Admin" role to respond `true` to all permissions, without needing to assign all those permissions to a role, you can use [Laravel's `Gate::before()` method](https://laravel.com/docs/master/authorization#intercepting-gate-checks). For example:
+
+In Laravel 11 this would go in the `boot()` method of `AppServiceProvider`:
+In Laravel 10 and below it would go in the `boot()` method of `AuthServiceProvider.php`:
+```php
+use Illuminate\Support\Facades\Gate;
+// ...
+public function boot()
+{
+ // Implicitly grant "Super Admin" role all permissions
+ // This works in the app by using gate-related functions like auth()->user->can() and @can()
+ Gate::before(function ($user, $ability) {
+ return $user->hasRole('Super Admin') ? true : null;
+ });
+}
+```
+
+NOTE: `Gate::before` rules need to return `null` rather than `false`, else it will interfere with normal policy operation. [See more.](https://laracasts.com/discuss/channels/laravel/policy-gets-never-called#reply=492526)
+
+Jeffrey Way explains the concept of a super-admin (and a model owner, and model policies) in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) video and some related lessons in that chapter.
+
+## Policy `before()`
+
+If you aren't using `Gate::before()` as described above, you could alternatively grant super-admin control by checking the role in individual Policy classes, using the `before()` method.
+
+Here is an example from the [Laravel Documentation on Policy Filters](https://laravel.com/docs/master/authorization#policy-filters), where you can define `before()` in your Policy where needed:
+
+```php
+use App\Models\User; // could be any Authorizable model
+
+/**
+ * Perform pre-authorization checks on the model.
+ */
+public function before(User $user, string $ability): ?bool
+{
+ if ($user->hasRole('Super Admin')) {
+ return true;
+ }
+
+ return null; // see the note above in Gate::before about why null must be returned here.
+}
+```
+
+## `Gate::after`
+
+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://freek.dev/1325-when-to-use-gateafter-in-laravel) where this topic is discussed further. You can also consult the [Laravel Docs on gate interceptions](https://laravel.com/docs/master/authorization#intercepting-gate-checks)
+
+```php
+// somewhere in a service provider
+
+Gate::after(function ($user, $ability) {
+ return $user->hasRole('Super Admin'); // note this returns boolean
+});
+```
diff --git a/docs/basic-usage/teams-permissions.md b/docs/basic-usage/teams-permissions.md
new file mode 100644
index 000000000..15095a7ab
--- /dev/null
+++ b/docs/basic-usage/teams-permissions.md
@@ -0,0 +1,170 @@
+---
+title: Teams permissions
+weight: 5
+---
+
+When enabled, teams permissions offers you 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/).
+
+## Enabling Teams Permissions Feature
+
+NOTE: These configuration changes must be made **before** performing the migration when first installing the package.
+
+If you have already run the migration and want to upgrade your implementation, 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.
+
+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 set it in the permission config file:
+```php
+// config/permission.php
+'team_foreign_key' => 'custom_team_id',
+```
+
+## Working with Teams Permissions
+
+After implementing a solution for selecting a team on the authentication process
+(for example, setting the `team_id` of the currently selected team on the **session**: `session(['team_id' => $team->team_id]);` ),
+we can set global `team_id` from anywhere, but works better if you create a `Middleware`.
+
+Example Team Middleware:
+
+```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);
+ }
+}
+```
+
+**YOU MUST ALSO** set [the `$middlewarePriority` array](https://laravel.com/docs/master/middleware#sorting-middleware) in `app/Http/Kernel.php` to include your custom middleware before the `SubstituteBindings` middleware, else you may get *404 Not Found* responses when a *403 Not Authorized* response might be expected.
+
+For example, in Laravel 11.27+ you can add something similiar to the `boot` method of your `AppServiceProvider`.
+
+```php
+use App\Http\Middleware\YourCustomMiddlewareClass;
+use Illuminate\Foundation\Http\Kernel;
+use Illuminate\Routing\Middleware\SubstituteBindings;
+use Illuminate\Support\ServiceProvider;
+
+class AppServiceProvider extends ServiceProvider
+{
+ public function register(): void
+ {
+ //
+ }
+
+ public function boot(): void
+ {
+ /** @var Kernel $kernel */
+ $kernel = app()->make(Kernel::class);
+
+ $kernel->addToMiddlewarePriorityBefore(
+ YourCustomMiddlewareClass::class,
+ SubstituteBindings::class,
+ );
+ }
+}
+```
+### Using LiveWire?
+
+You may need to register your team middleware as Persisted in Livewire. See [Livewire docs: Configuring Persistent Middleware](https://livewire.laravel.com/docs/security#configuring-persistent-middleware)
+
+## 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 and Removal
+
+The role/permission assignment and removal for teams are the same as without teams, but they take the global `team_id` which is set on login.
+
+## Changing The Active Team ID
+
+While your middleware will set a user's `team_id` upon login, you may later need to set it to another team for various reasons. The two most common reasons are these:
+
+### Switching Teams After Login
+If your application allows the user to switch between various teams which they belong to, you can activate the roles/permissions for that team by calling `setPermissionsTeamId($new_team_id)` and unsetting relations as described below.
+
+### Administrating Team Details
+You may have created a User-Manager page where you can view the roles/permissions of users on certain teams. For managing that user in each team they belong to, you must also use `setPermissionsTeamId($new_team_id)` to cause lookups to relate to that new team, and unset prior relations as described below.
+
+### Querying Roles/Permissions for Other Teams
+Whenever you switch the active `team_id` using `setPermissionsTeamId()`, you need to `unset` the user's/model's `roles` and `permissions` relations before querying what roles/permissions that user has (`$user->roles`, etc) and before calling any authorization functions (`can()`, `hasPermissionTo()`, `hasRole()`, etc).
+
+Example:
+```php
+// set active global team_id
+setPermissionsTeamId($new_team_id);
+
+// $user = Auth::user();
+
+// unset cached model relations so new team relations will get reloaded
+$user->unsetRelation('roles')->unsetRelation('permissions');
+
+// Now you can check:
+$roles = $user->roles;
+$hasRole = $user->hasRole('my_role');
+$user->hasPermissionTo('foo');
+$user->can('bar');
+// etc
+```
+
+## Defining a Super-Admin on Teams
+
+Global roles can be assigned to different teams, and `team_id` (which is the primary key of the relationships) is always required.
+
+If you want a "Super Admin" global role for a user, when you create 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) {
+ // temporary: get session team_id for restore at end
+ $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 using temporary value stored above
+ setPermissionsTeamId($session_team_id);
+ });
+ }
+ // ...
+}
+```
diff --git a/docs/basic-usage/wildcard-permissions.md b/docs/basic-usage/wildcard-permissions.md
new file mode 100644
index 000000000..b1412f5cc
--- /dev/null
+++ b/docs/basic-usage/wildcard-permissions.md
@@ -0,0 +1,88 @@
+---
+title: Wildcard permissions
+weight: 6
+---
+
+When enabled, wildcard permissions offers you a flexible representation for a variety of permission schemes.
+
+The wildcard permissions implementation is inspired by the default permission implementation of
+ [Apache Shiro](https://shiro.apache.org/permissions.html). See the Shiro documentation for more examples and deeper explanation of the concepts.
+
+## Enabling Wildcard Features
+
+Wildcard permissions can be enabled in the permission config file:
+
+```php
+// config/permission.php
+'enable_wildcard_permission' => true,
+```
+
+## Wildcard Syntax
+
+A wildcard permission string is made of one or more parts separated by dots (.).
+
+```php
+$permission = 'posts.create.1';
+```
+
+The meaning of each part of the string depends on the application layer.
+
+> You can use as many parts as you like. So you are not limited to the three-tiered structure, even though
+this is the common use-case, representing `{resource}.{action}.{target}`.
+
+> **NOTE: You must actually create the wildcarded permissions** (eg: `posts.create.1`) before you can assign them or check for them.
+
+> **NOTE: You must create any wildcard permission patterns** (eg: `posts.create.*`) before you can assign them or check for them.
+
+## Using Wildcards
+
+> ALERT: The `*` means "ALL". It does **not** mean "ANY".
+
+Each part can also contain wildcards (`*`). So let's say we assign the following permission to a user:
+
+```php
+Permission::create(['name'=>'posts.*']);
+$user->givePermissionTo('posts.*');
+// is the same as
+Permission::create(['name'=>'posts']);
+$user->givePermissionTo('posts');
+```
+
+Given the example above, everyone who is assigned to this permission will be allowed every action on posts. It is not necessary to use a
+wildcard on the last part of the string. This is automatically assumed.
+
+```php
+// will be true
+$user->can('posts.create');
+$user->can('posts.edit');
+$user->can('posts.delete');
+```
+(Note that the `posts.create` and `posts.edit` and `posts.delete` permissions must also be created.)
+
+## Meaning of the * Asterisk
+
+The `*` means "ALL". It does **not** mean "ANY".
+
+Thus `can('post.*')` will only pass if the user has been assigned `post.*` explicitly, and the `post.*` Permission has been created.
+
+
+## Subparts
+
+Besides the use of parts and wildcards, subparts can also be used. Subparts are divided with commas (,). This is a
+powerful feature that lets you create complex permission schemes.
+
+```php
+// user can only do the actions create, update and view on both resources posts and users
+Permission::create(['name'=>'posts,users.create,update,view']);
+$user->givePermissionTo('posts,users.create,update,view');
+
+// user can do the actions create, update, view on any available resource
+Permission::create(['name'=>'*.create,update,view']);
+$user->givePermissionTo('*.create,update,view');
+
+// user can do any action on posts with ids 1, 4 and 6
+Permission::create(['name'=>'posts.*.1,4,6']);
+$user->givePermissionTo('posts.*.1,4,6');
+```
+
+> Remember: the meaning of each 'part' is determined by your application! So, you are free to use each part as you like. And you can use as many parts and subparts as you want.
diff --git a/docs/best-practices/_index.md b/docs/best-practices/_index.md
new file mode 100644
index 000000000..1e46c30d5
--- /dev/null
+++ b/docs/best-practices/_index.md
@@ -0,0 +1,4 @@
+---
+title: Best Practices
+weight: 2
+---
diff --git a/docs/best-practices/performance.md b/docs/best-practices/performance.md
new file mode 100644
index 000000000..81c40be78
--- /dev/null
+++ b/docs/best-practices/performance.md
@@ -0,0 +1,30 @@
+---
+title: Performance Tips
+weight: 10
+---
+
+On small apps, most of the following will be moot, and unnecessary.
+
+Often we think in terms of "roles have permissions" so we lookup a Role, and call `$role->givePermissionTo()`
+to indicate what users with that role are allowed to do. This is perfectly fine!
+
+And yet, in some situations, particularly if your app is deleting and adding new permissions frequently,
+you may find that things are more performant if you lookup the permission and assign it to the role, like:
+`$permission->assignRole($role)`.
+The end result is the same, but sometimes it runs quite a lot faster.
+
+Also, because of the way this package enforces some protections for you, on large databases you may find
+that instead of creating permissions with:
+```php
+Permission::create([attributes]);
+```
+it might be faster (more performant) to use:
+```php
+$permission = Permission::make([attributes]);
+$permission->saveOrFail();
+```
+
+As always, if you choose to bypass the provided object methods for adding/removing/syncing roles and permissions
+by manipulating Role and Permission objects directly in the database,
+**you will need to manually reset the package cache** with the PermissionRegistrar's method for that,
+as described in the Cache section of the docs.
diff --git a/docs/best-practices/roles-vs-permissions.md b/docs/best-practices/roles-vs-permissions.md
new file mode 100644
index 000000000..033d3eb52
--- /dev/null
+++ b/docs/best-practices/roles-vs-permissions.md
@@ -0,0 +1,37 @@
+---
+title: Roles vs Permissions
+weight: 1
+---
+
+Best-Practice for thinking about Roles vs Permissions is this:
+
+**Roles** are best to only assign to **Users** in order to "**group**" people by "**sets of permissions**".
+
+**Permissions** are best assigned **to roles**.
+The more granular/detailed your permission-names (such as separate permissions like "view document" and "edit document"), the easier it is to control access in your application.
+
+**Users** should *rarely* be given "direct" permissions. Best if Users inherit permissions via the Roles that they're assigned to.
+
+When designed this way, all the sections of your application can check for specific permissions needed to access certain features or perform certain actions AND this way you can always **use the native Laravel `@can` and `can()` directives everywhere** in your app, which allows Laravel's Gate layer to do all the heavy lifting.
+
+Example: it's safer to have your Views test `@can('view member addresses')` or `@can('edit document')`, INSTEAD of testing for `$user->hasRole('Editor')`. It's easier to control displaying a "section" of content vs edit/delete buttons if you have "view document" and "edit document" permissions defined. And then Writer role would get both "view" and "edit" assigned to it. And then the user would get the Writer role.
+
+This also allows you to treat permission names as static (only editable by developers), and then your application (almost) never needs to know anything about role names, so you could (almost) change role names at will.
+
+Summary:
+- **users** have `roles`
+- **roles** have `permissions`
+- app always checks for `permissions` (as much as possible), not `roles`
+- **views** check permission-names
+- **policies** check permission-names
+- **model policies** check permission-names
+- **controller methods** check permission-names
+- **middleware** check permission names, or sometimes role-names
+- **routes** check permission-names, or maybe role-names if you need to code that way.
+
+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.
+
+
+### FURTHER READING:
+
+[@joelclermont](https://github.com/joelclermont) at [masteringlaravel.io](https://masteringlaravel.io/daily) offers similar guidance in his post about [Treating Feature Access As Data, Not Code](https://masteringlaravel.io/daily/2025-01-09-treat-feature-access-as-data-not-code)
diff --git a/docs/best-practices/using-policies.md b/docs/best-practices/using-policies.md
new file mode 100644
index 000000000..648ad1971
--- /dev/null
+++ b/docs/best-practices/using-policies.md
@@ -0,0 +1,72 @@
+---
+title: Model Policies
+weight: 2
+---
+
+The best way to incorporate access control for application features is with [Laravel's Model Policies](https://laravel.com/docs/authorization#creating-policies).
+
+Using Policies allows you to simplify things by abstracting your "control" rules into one place, where your application logic can be combined with your permission rules.
+
+Jeffrey Way explains the concept simply in the [Laravel 6 Authorization Filters](https://laracasts.com/series/laravel-6-from-scratch/episodes/51) and [policies](https://laracasts.com/series/laravel-6-from-scratch/episodes/63) videos and in other related lessons in that chapter. He also mentions how to set up a super-admin, both in a model policy and globally in your application.
+
+Here's an example of a PostPolicy which could control access to Post model records:
+```php
+published) {
+ return true;
+ }
+
+ // visitors cannot view unpublished items
+ if ($user === null) {
+ return false;
+ }
+
+ // admin overrides published status
+ if ($user->can('view unpublished posts')) {
+ return true;
+ }
+
+ // authors can view their own unpublished posts
+ return $user->id == $post->user_id;
+ }
+
+ public function create(User $user): bool
+ {
+ return $user->can('create posts');
+ }
+
+ public function update(User $user, Post $post): bool
+ {
+ if ($user->can('edit all posts')) {
+ return true;
+ }
+
+ if ($user->can('edit own posts')) {
+ return $user->id == $post->user_id;
+ }
+ }
+
+ public function delete(User $user, Post $post): bool
+ {
+ if ($user->can('delete any post')) {
+ return true;
+ }
+
+ if ($user->can('delete own posts')) {
+ return $user->id == $post->user_id;
+ }
+ }
+}
+```
diff --git a/docs/changelog.md b/docs/changelog.md
new file mode 100644
index 000000000..3280d4f29
--- /dev/null
+++ b/docs/changelog.md
@@ -0,0 +1,6 @@
+---
+title: Changelog
+weight: 10
+---
+
+All notable changes to laravel-permission are documented [on GitHub](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md)
diff --git a/docs/images/header.jpg b/docs/images/header.jpg
new file mode 100644
index 000000000..489010bee
Binary files /dev/null and b/docs/images/header.jpg differ
diff --git a/docs/installation-laravel.md b/docs/installation-laravel.md
new file mode 100644
index 000000000..06b430727
--- /dev/null
+++ b/docs/installation-laravel.md
@@ -0,0 +1,65 @@
+---
+title: Installation in Laravel
+weight: 4
+---
+
+## Laravel Version Compatibility
+
+See the "Prerequisites" documentation page for compatibility details.
+
+## 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.
+
+3. You can **install the package via composer**:
+
+ composer require spatie/laravel-permission
+
+4. The Service Provider will automatically be registered; however, if you wish to manually register it, you can manually add the `Spatie\Permission\PermissionServiceProvider::class` service provider to the array in `bootstrap/providers.php` (`config/app.php` in Laravel 10 or older).
+
+
+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. BEFORE RUNNING MIGRATIONS
+
+ - **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 the TEAMS features** you must update your [`config/permission.php` config file](https://github.com/spatie/laravel-permission/blob/main/config/permission.php):
+ - must set `'teams' => true,`
+ - and (optional) you may set `team_foreign_key` name in the config file if you want to use a custom foreign key in your database for teams
+
+ - **If you are using MySQL 8+**, look at the migration files for notes about MySQL 8+ to set/limit the index key length, and edit accordingly. If you get `ERROR: 1071 Specified key was too long` then you need to do this.
+
+ - **If you are using CACHE_STORE=database**, be sure to [install Laravel's cache migration](https://laravel.com/docs/cache#prerequisites-database), else you will encounter cache errors.
+
+7. **Clear your config cache**. This package requires access to the `permission` config settings in order to run migrations. If you've been caching configurations locally, clear your config cache with either of these commands:
+
+ php artisan optimize:clear
+ # or
+ php artisan config:clear
+
+8. **Run the migrations**: After the config and migration have been published and configured, you can create the tables for this package by running:
+
+ php artisan migrate
+
+9. **Add the necessary trait to your User model**:
+
+ // The User model requires this trait
+ use HasRoles;
+
+10. Consult the **Basic Usage** section of the docs to get started using the features of this package.
+
+.
+
+
+## Default config file contents
+
+You can view the default config file contents at:
+
+[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
new file mode 100644
index 000000000..b5f079e75
--- /dev/null
+++ b/docs/installation-lumen.md
@@ -0,0 +1,81 @@
+---
+title: Installation in Lumen
+weight: 5
+---
+
+NOTE: Lumen is **not** officially supported by this package. And Lumen is no longer under active development.
+
+However, the following are some steps which may help get you started.
+
+Lumen installation instructions can be found in the [Lumen documentation](https://lumen.laravel.com/docs).
+
+## Installing
+
+Install the permissions package via Composer:
+
+``` bash
+composer require spatie/laravel-permission
+```
+
+Copy the required files:
+
+```bash
+mkdir -p config
+cp vendor/spatie/laravel-permission/config/permission.php config/permission.php
+cp vendor/spatie/laravel-permission/database/migrations/create_permission_tables.php.stub database/migrations/2018_01_01_000000_create_permission_tables.php
+```
+
+You will also need the `config/auth.php` file. If you don't already have it, copy it from the vendor folder:
+
+```bash
+cp vendor/laravel/lumen-framework/config/auth.php config/auth.php
+```
+
+Next, if you wish to use this package's middleware, clone whichever ones you want from `Spatie\Permission\Middleware` namespace into your own `App\Http\Middleware` namespace AND replace the `canAny()` call with `hasAnyPermission()` (because Lumen doesn't support `canAny()`).
+
+Then, in `bootstrap/app.php`, uncomment the `auth` middleware, and register the middleware you've created. For example:
+
+```php
+$app->routeMiddleware([
+ 'auth' => App\Http\Middleware\Authenticate::class,
+ 'permission' => App\Http\Middleware\PermissionMiddleware::class, // cloned from Spatie\Permission\Middleware
+ 'role' => App\Http\Middleware\RoleMiddleware::class, // cloned from Spatie\Permission\Middleware
+]);
+```
+
+... and also in `bootstrap/app.php`, in the ServiceProviders section, register the package configuration, service provider, and cache alias:
+
+```php
+$app->configure('permission');
+$app->alias('cache', \Illuminate\Cache\CacheManager::class); // if you don't have this already
+$app->register(Spatie\Permission\PermissionServiceProvider::class);
+```
+
+... and in the same file, since the Authorization layer uses guards you will need to uncomment the AuthServiceProvider line:
+```php
+$app->register(App\Providers\AuthServiceProvider::class);
+```
+
+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:
+
+```bash
+php artisan migrate
+```
+
+---
+## User Model
+NOTE: Remember that Laravel's authorization layer requires that your `User` model implement the `Illuminate\Contracts\Auth\Access\Authorizable` contract. In Lumen you will then also need to use the `Laravel\Lumen\Auth\Authorizable` trait. Note that Lumen does not support the `User::canAny()` authorization method.
+
+---
+## 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/0001_01_01_000000_create_users_table.php](https://github.com/laravel/laravel/blob/master/database/migrations/0001_01_01_000000_create_users_table.php)
+
+(You will need to run `php artisan migrate` after adding this file.)
+
+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
new file mode 100644
index 000000000..604fa8cac
--- /dev/null
+++ b/docs/introduction.md
@@ -0,0 +1,34 @@
+---
+title: Introduction
+weight: 1
+---
+
+This package allows you to manage user permissions and roles in a database.
+
+Once installed you can do stuff like this:
+
+```php
+// Adding permissions to a user
+$user->givePermissionTo('edit articles');
+
+// Adding permissions via a role
+$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](./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:
+
+```php
+$user->can('edit articles');
+```
+
+and Blade directives:
+
+```blade
+@can('edit articles')
+...
+@endcan
+```
diff --git a/docs/prerequisites.md b/docs/prerequisites.md
new file mode 100644
index 000000000..c4be5cb88
--- /dev/null
+++ b/docs/prerequisites.md
@@ -0,0 +1,84 @@
+---
+title: Prerequisites
+weight: 3
+---
+
+## Laravel Version Compatibility
+
+Laravel Version | Package Version
+----------------|-----------
+ 8,9,10,11,12 | `^6.0` (PHP 8.0+)
+ 7,8,9,10 | `^5.8`
+ 7,8,9 | `^5.7`
+ 7,8 | `^5.4`-`^5.6`
+ 6,7,8 | `^5.0`-`^5.3`
+ 6,7,8 | `^4`
+ 5.8 | `^3`
+
+## 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.
+Otherwise the `can()` and `authorize()` methods will not work in your controllers, policies, templates, etc.
+
+In the `Installation` instructions you'll see that the `HasRoles` trait must be added to the User model to enable this package's features.
+
+Thus, a typical basic User model would have these basic minimum requirements:
+
+```php
+use Illuminate\Foundation\Auth\User as Authenticatable;
+use Spatie\Permission\Traits\HasRoles;
+
+class User extends Authenticatable
+{
+ use HasRoles;
+
+ // ...
+}
+```
+
+## Must not have a [role] or [roles] property/relation, nor a [roles()] method
+
+Your `User` model/object MUST NOT have a `role` or `roles` property (or field in the database by that name), nor a `roles()` method on it (nor a `roles` relation). Those will interfere with the properties and methods and relations 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/relation, nor a [permissions()] method
+
+Your `User` model/object MUST NOT have a `permission` or `permissions` property (or field in the database by that name), nor a `permissions()` method on it (nor a `permissions` relation). Those will interfere with the properties and methods and relations 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.
+
+## Database Schema Limitations
+
+Potential error message: "1071 Specified key was too long; max key length is 1000 bytes"
+
+MySQL 8.0+ limits index key lengths, which might be too short for some compound indexes used by this package.
+This package publishes a migration which combines multiple columns in a single index. With `utf8mb4` the 4-bytes-per-character requirement of `mb4` means the total length of the columns in the hybrid index can only be `25%` of that maximum index length.
+
+- MyISAM tables limit the index to 1000 characters (which is only 250 total chars in `utf8mb4`)
+- InnoDB tables using ROW_FORMAT of 'Redundant' or 'Compact' limit the index to 767 characters (which is only 191 total chars in `utf8mb4`)
+- InnoDB tables using ROW_FORMAT of 'Dynamic' or 'Compressed' have a 3072 character limit (which is 768 total chars in `utf8mb4`).
+
+Depending on your MySQL or MariaDB configuration, you may implement one of the following approaches:
+
+1. Ideally, configure the database to use InnoDB by default, and use ROW FORMAT of 'Dynamic' by default for all new tables. (See [MySQL](https://dev.mysql.com/doc/refman/8.0/en/innodb-limits.html) and [MariaDB](https://mariadb.com/kb/en/innodb-dynamic-row-format/) docs.)
+
+2. OR if your app doesn't require a longer default, in your AppServiceProvider you can set `Schema::defaultStringLength(125)`. [See the Laravel Docs for instructions](https://laravel.com/docs/10.x/migrations#index-lengths-mysql-mariadb). This will have Laravel set all strings to 125 characters by default.
+
+3. OR you could edit the migration and specify a shorter length for 4 fields. Then in your app be sure to manually impose validation limits on any form fields related to these fields.
+There are 2 instances of this code snippet where you can explicitly set the length.:
+```php
+ $table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
+ $table->string('guard_name'); // For MyISAM use string('guard_name', 25);
+```
+
+## 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/advanced-usage/uuid](https://spatie.be/docs/laravel-permission/advanced-usage/uuid) for more information.
+
+## Database foreign-key relationship support
+
+To enforce database integrity, this package uses foreign-key relationships with cascading deletes. This prevents data mismatch situations if database records are manipulated outside of this package. If your database engine does not support foreign-key relationships, then you will have to alter the migration files accordingly.
+
+This package does its own detaching of pivot records when deletes are called using provided package methods, so if your database does not support foreign keys then as long as you only use method calls provided by this package for managing related records, there should not be data integrity issues.
diff --git a/docs/questions-issues.md b/docs/questions-issues.md
new file mode 100644
index 000000000..4c4313290
--- /dev/null
+++ b/docs/questions-issues.md
@@ -0,0 +1,8 @@
+---
+title: Questions and issues
+weight: 9
+---
+
+Find yourself stuck using the package? Found a bug? Do you have general questions or suggestions for improving the package? Feel free to [create an issue on GitHub](https://github.com/spatie/laravel-permission/issues), we'll try to address it as soon as possible.
+
+If you've found a bug regarding security please mail [freek@spatie.be](mailto:freek@spatie.be) instead of using the issue tracker.
diff --git a/docs/support-us.md b/docs/support-us.md
new file mode 100644
index 000000000..0453692fe
--- /dev/null
+++ b/docs/support-us.md
@@ -0,0 +1,8 @@
+---
+title: Support us
+weight: 2
+---
+
+We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
+
+We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
diff --git a/docs/upgrading.md b/docs/upgrading.md
new file mode 100644
index 000000000..f0b58f64c
--- /dev/null
+++ b/docs/upgrading.md
@@ -0,0 +1,81 @@
+---
+title: Upgrading
+weight: 6
+---
+
+## Upgrade Essentials
+
+ALL upgrades of this package should follow these steps:
+
+1. Composer. Upgrading between major versions of this package always requires the usual Composer steps:
+ - Update your `composer.json` to specify the new major version, for example: `^6.0`
+ - Then run `composer update spatie/laravel-permission`.
+
+2. Migrations. Compare the `migration` file stubs in the NEW version of this package against the migrations you've already run inside your app. If necessary, create a new migration (by hand) to apply any new database changes.
+
+3. Config file. Incorporate any changes to the permission.php config file, updating your existing file. (It may be easiest to make a backup copy of your existing file, re-publish it from this package, and then re-make your customizations to it.)
+
+4. Models. If you have made any custom Models by extending them into your own app, compare the package's old and new models and apply any relevant updates to your custom models.
+
+5. Custom Methods/Traits. If you have overridden any methods from this package's Traits, compare the old and new traits, and apply any relevant updates to your overridden methods.
+
+6. Contract/Interface updates. If you have implemented this package's contracts in any models, check to see if there were any changes to method signatures. Mismatches will trigger PHP errors.
+
+7. Apply any version-specific special updates as outlined below...
+
+8. Review the changelog, which details all the changes: [CHANGELOG](https://github.com/spatie/laravel-permission/blob/main/CHANGELOG.md)
+and/or consult the [Release Notes](https://github.com/spatie/laravel-permission/releases)
+
+
+## Upgrading from v5 to v6
+There are a few breaking-changes when upgrading to v6, but most of them won't affect you unless you have been customizing things.
+
+For guidance with upgrading your extended models, your migrations, your routes, etc, see the **Upgrade Essentials** section at the top of this file.
+
+1. Due to the improved ULID/UUID/GUID support, any package methods which accept a Permission or Role `id` must pass that `id` as an `integer`. If you pass it as a numeric string, the functions will attempt to look up the role/permission as a string. In such cases, you may see errors such as `There is no permission named '123' for guard 'web'.` (where `'123'` is being treated as a string because it was passed as a string instead of as an integer). This also applies to arrays of id's: if it's an array of strings we will do a lookup on the name instead of on the id. **This will mostly only affect UI pages** because an HTML Request is received as string data. **The solution is simple:** if you're passing integers to a form field, then convert them back to integers when using that field's data for calling functions to grant/assign/sync/remove/revoke permissions and roles. One way to convert an array of permissions `id`'s from strings to integers is: `collect($validated['permission'])->map(fn($val)=>(int)$val)`
+
+2. If you have overridden the `getPermissionClass()` or `getRoleClass()` methods or have custom Models, you will need to revisit those customizations. See PR #2368 for details.
+eg: if you have a custom model you will need to make changes, including accessing the model using `$this->permissionClass::` syntax (eg: using `::` instead of `->`) in all the overridden methods that make use of the models.
+
+ Be sure to compare your custom models with the originals to see what else may have changed.
+
+3. Model and Contract/Interface updates. The Role and Permission Models and Contracts/Interfaces have been updated with syntax changes to method signatures. Update any models you have extended, or contracts implemented, accordingly. See PR [#2380](https://github.com/spatie/laravel-permission/pull/2380) and [#2480](https://github.com/spatie/laravel-permission/pull/2480) for some of the specifics.
+
+4. Migrations WILL need to be upgraded. (They have been updated to anonymous-class syntax that was introduced in Laravel 8, AND some structural coding changes in the registrar class changed the way we extracted configuration settings in the migration files.) There are no changes to the package's structure since v5, so if you had not customized it from the original then replacing the contents of the file should be enough. (Usually, the only customization is if you've switched to UUIDs or customized MySQL index name lengths.)
+**If you get the following error, it means your migration file needs upgrading: `Error: Access to undeclared static property Spatie\Permission\PermissionRegistrar::$pivotPermission`**
+
+5. MIDDLEWARE:
+
+ 1. The `\Spatie\Permission\Middlewares\` namespace has been renamed to `\Spatie\Permission\Middleware\` (singular). Update any references to them in your `/app/Http/Kernel.php` and any routes (or imported classes in your routes files) that have the fully qualified namespace.
+
+ 2. NOTE: For consistency with `PermissionMiddleware`, the `RoleOrPermissionMiddleware` has switched from only checking permissions provided by this package to using `canAny()` to check against any abilities registered by your application. This may have the effect of granting those other abilities (such as Super Admin) when using the `RoleOrPermissionMiddleware`, which previously would have failed silently.
+
+ 3. In the unlikely event that you have customized the Wildcard Permissions feature by extending the `WildcardPermission` model, please note that the public interface has changed significantly and you will need to update your extended model with the new method signatures.
+
+6. Test suites. If you have tests that manually clear the permission cache and re-register permissions, you no longer need to call `\Spatie\Permission\PermissionRegistrar::class)->registerPermissions();`. In fact, **calls to `->registerPermissions()` MUST be deleted from your tests**.
+
+ (Calling `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();` after creating roles and permissions in migrations and factories and seeders is still okay and encouraged.)
+
+
+## Upgrading from v4 to v5
+
+Follow the instructions described in "Essentials" above.
+
+## Upgrading from v3 to v4
+
+Update `composer.json` as described in "Essentials" above.
+
+## Upgrading from v2 to v3
+
+Update `composer.json` as described in "Essentials" above.
+
+
+## Upgrading from v1 to v2
+There were significant database and code changes between v1 to v2.
+
+If you're upgrading from v1 to v2, there's no built-in automatic migration/conversion of your data to the new structure.
+You will need to carefully adapt your code and your data manually.
+
+Tip: @fabricecw prepared [a gist which may make your data migration easier](https://gist.github.com/fabricecw/58ee93dd4f99e78724d8acbb851658a4).
+
+You will also need to remove your old `laravel-permission.php` config file and publish the new one `permission.php`, and edit accordingly (setting up your custom settings again in the new file, where relevant).
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/phpstan-baseline.neon b/phpstan-baseline.neon
new file mode 100644
index 000000000..364905f71
--- /dev/null
+++ b/phpstan-baseline.neon
@@ -0,0 +1,2 @@
+parameters:
+ ignoreErrors:
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
new file mode 100644
index 000000000..e6a4b0f89
--- /dev/null
+++ b/phpstan.neon.dist
@@ -0,0 +1,22 @@
+includes:
+ - ./vendor/larastan/larastan/extension.neon
+ - phpstan-baseline.neon
+
+parameters:
+ level: 5
+ paths:
+ - src
+ - config
+ - database/migrations/create_permission_tables.php.stub
+ - database/migrations/add_teams_fields.php.stub
+ tmpDir: build/phpstan
+ checkOctaneCompatibility: true
+
+ ignoreErrors:
+ - '#Unsafe usage of new static#'
+ # wildcard permissions:
+ - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getWildcardClass#'
+ - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::getAllPermissions#'
+ # contract checks:
+ - '#Call to function is_a\(\) with (.*) and ''Spatie\\\\Permission\\\\Contracts\\\\Permission'' will always evaluate to true\.$#'
+ - '#Call to function is_a\(\) with (.*) and ''Spatie\\\\Permission\\\\Contracts\\\\Role'' will always evaluate to true\.$#'
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index a83848104..96497e656 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -1,25 +1,23 @@
-
-
-
- tests
-
-
-
-
- src/
-
-
-
-
-
+ xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.4/phpunit.xsd"
+>
+
+
+ src/
+
+
+
+
+ tests
+
+
+
+
+
+
+
+
diff --git a/pint.json b/pint.json
new file mode 100644
index 000000000..ea56b7b88
--- /dev/null
+++ b/pint.json
@@ -0,0 +1,6 @@
+{
+ "preset": "laravel",
+ "rules": {
+ "php_unit_method_casing": false
+ }
+}
diff --git a/src/Commands/CacheReset.php b/src/Commands/CacheReset.php
index cd5eb41e3..85f7c24eb 100644
--- a/src/Commands/CacheReset.php
+++ b/src/Commands/CacheReset.php
@@ -13,8 +13,13 @@ class CacheReset extends Command
public function handle()
{
- app(PermissionRegistrar::class)->forgetCachedPermissions();
+ $permissionRegistrar = app(PermissionRegistrar::class);
+ $cacheExists = $permissionRegistrar->getCacheRepository()->has($permissionRegistrar->cacheKey);
- $this->info('Permission cache flushed.');
+ if ($permissionRegistrar->forgetCachedPermissions()) {
+ $this->info('Permission cache flushed.');
+ } elseif ($cacheExists) {
+ $this->error('Unable to flush cache.');
+ }
}
}
diff --git a/src/Commands/CreatePermission.php b/src/Commands/CreatePermission.php
index c93546485..c3bc20693 100644
--- a/src/Commands/CreatePermission.php
+++ b/src/Commands/CreatePermission.php
@@ -17,11 +17,8 @@ public function handle()
{
$permissionClass = app(PermissionContract::class);
- $permission = $permissionClass::create([
- 'name' => $this->argument('name'),
- 'guard_name' => $this->argument('guard'),
- ]);
+ $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 975a21baa..7cb92aa35 100644
--- a/src/Commands/CreateRole.php
+++ b/src/Commands/CreateRole.php
@@ -3,29 +3,49 @@
namespace Spatie\Permission\Commands;
use Illuminate\Console\Command;
-use Spatie\Permission\Contracts\Role as RoleContract;
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';
- public function handle()
+ public function handle(PermissionRegistrar $permissionRegistrar)
{
$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'));
}
+ /**
+ * @param array|null|string $string
+ */
protected function makePermissions($string = null)
{
if (empty($string)) {
diff --git a/src/Commands/Show.php b/src/Commands/Show.php
new file mode 100644
index 000000000..2cbfb04f5
--- /dev/null
+++ b/src/Commands/Show.php
@@ -0,0 +1,77 @@
+argument('style') ?? 'default';
+ $guard = $this->argument('guard');
+
+ if ($guard) {
+ $guards = Collection::make([$guard]);
+ } else {
+ $guards = $permissionClass::pluck('guard_name')->merge($roleClass::pluck('guard_name'))->unique();
+ }
+
+ foreach ($guards as $guard) {
+ $this->info("Guard: $guard");
+
+ $roles = $roleClass::whereGuardName($guard)
+ ->with('permissions')
+ ->when($teamsEnabled, fn ($q) => $q->orderBy($team_key))
+ ->orderBy('name')->get()->mapWithKeys(fn ($role) => [
+ $role->name.'_'.($teamsEnabled ? ($role->$team_key ?: '') : '') => [
+ 'permissions' => $role->permissions->pluck($permissionClass->getKeyName()),
+ $team_key => $teamsEnabled ? $role->$team_key : null,
+ ],
+ ]);
+
+ $permissions = $permissionClass::whereGuardName($guard)->orderBy('name')->pluck('name', $permissionClass->getKeyName());
+
+ $body = $permissions->map(fn ($permission, $id) => $roles->map(
+ fn (array $role_data) => $role_data['permissions']->contains($id) ? ' ✔' : ' ·'
+ )->prepend($permission)
+ );
+
+ if ($teamsEnabled) {
+ $teams = $roles->groupBy($team_key)->values()->map(
+ fn ($group, $id) => new TableCell('Team ID: '.($id ?: 'NULL'), ['colspan' => $group->count()])
+ );
+ }
+
+ $this->table(
+ array_merge(
+ isset($teams) ? $teams->prepend(new TableCell(''))->toArray() : [],
+ $roles->keys()->map(function ($val) {
+ $name = explode('_', $val);
+ array_pop($name);
+
+ return implode('_', $name);
+ })
+ ->prepend(new TableCell(''))->toArray(),
+ ),
+ $body->toArray(),
+ $style
+ );
+ }
+ }
+}
diff --git a/src/Commands/UpgradeForTeams.php b/src/Commands/UpgradeForTeams.php
new file mode 100644
index 000000000..8d7f9004d
--- /dev/null
+++ b/src/Commands/UpgradeForTeams.php
@@ -0,0 +1,122 @@
+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?', true)) {
+ 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.
+ *
+ * @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, fn ($carry, $fileName) => $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(fn ($path) => 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/Contracts/Permission.php b/src/Contracts/Permission.php
index 344173901..5446e501a 100644
--- a/src/Contracts/Permission.php
+++ b/src/Contracts/Permission.php
@@ -4,46 +4,40 @@
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+/**
+ * @property int|string $id
+ * @property string $name
+ * @property string|null $guard_name
+ *
+ * @mixin \Spatie\Permission\Models\Permission
+ *
+ * @phpstan-require-extends \Spatie\Permission\Models\Permission
+ */
interface Permission
{
/**
* A permission can be applied to roles.
- *
- * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function roles(): BelongsToMany;
/**
* Find a permission by its name.
*
- * @param string $name
- * @param string|null $guardName
*
* @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist
- *
- * @return Permission
*/
- public static function findByName(string $name, $guardName): self;
+ public static function findByName(string $name, ?string $guardName): self;
/**
* Find a permission by its id.
*
- * @param int $id
- * @param string|null $guardName
*
* @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist
- *
- * @return Permission
*/
- public static function findById(int $id, $guardName): self;
+ public static function findById(int|string $id, ?string $guardName): self;
/**
* Find or Create a permission by its name and guard name.
- *
- * @param string $name
- * @param string|null $guardName
- *
- * @return Permission
*/
- public static function findOrCreate(string $name, $guardName): self;
+ public static function findOrCreate(string $name, ?string $guardName): self;
}
diff --git a/src/Contracts/PermissionsTeamResolver.php b/src/Contracts/PermissionsTeamResolver.php
new file mode 100644
index 000000000..780cd2a1d
--- /dev/null
+++ b/src/Contracts/PermissionsTeamResolver.php
@@ -0,0 +1,15 @@
+getKey();
+ }
+ $this->teamId = $id;
+ }
+
+ public function getPermissionsTeamId(): int|string|null
+ {
+ return $this->teamId;
+ }
+}
diff --git a/src/Events/PermissionAttached.php b/src/Events/PermissionAttached.php
new file mode 100644
index 000000000..c7734f382
--- /dev/null
+++ b/src/Events/PermissionAttached.php
@@ -0,0 +1,28 @@
+implode(', ')}` instead of `{$givenGuard}`.");
+ return new static(__('The given role or permission should use guard `:expected` instead of `:given`.', [
+ 'expected' => $expectedGuards->implode(', '),
+ 'given' => $givenGuard,
+ ]));
}
}
diff --git a/src/Exceptions/PermissionAlreadyExists.php b/src/Exceptions/PermissionAlreadyExists.php
index 2748c9d1d..2270db148 100644
--- a/src/Exceptions/PermissionAlreadyExists.php
+++ b/src/Exceptions/PermissionAlreadyExists.php
@@ -8,6 +8,9 @@ class PermissionAlreadyExists extends InvalidArgumentException
{
public static function create(string $permissionName, string $guardName)
{
- return new static("A `{$permissionName}` permission already exists for guard `{$guardName}`.");
+ return new static(__('A `:permission` permission already exists for guard `:guard`.', [
+ 'permission' => $permissionName,
+ 'guard' => $guardName,
+ ]));
}
}
diff --git a/src/Exceptions/PermissionDoesNotExist.php b/src/Exceptions/PermissionDoesNotExist.php
index 305b61906..853bab10e 100644
--- a/src/Exceptions/PermissionDoesNotExist.php
+++ b/src/Exceptions/PermissionDoesNotExist.php
@@ -6,13 +6,23 @@
class PermissionDoesNotExist extends InvalidArgumentException
{
- public static function create(string $permissionName, string $guardName = '')
+ public static function create(string $permissionName, ?string $guardName)
{
- return new static("There is no permission named `{$permissionName}` for guard `{$guardName}`.");
+ return new static(__('There is no permission named `:permission` for guard `:guard`.', [
+ 'permission' => $permissionName,
+ 'guard' => $guardName,
+ ]));
}
- public static function withId(int $permissionId)
+ /**
+ * @param int|string $permissionId
+ * @return static
+ */
+ public static function withId($permissionId, ?string $guardName)
{
- return new static("There is no [permission] with id `{$permissionId}`.");
+ return new static(__('There is no [permission] with ID `:id` for guard `:guard`.', [
+ 'id' => $permissionId,
+ 'guard' => $guardName,
+ ]));
}
}
diff --git a/src/Exceptions/RoleAlreadyExists.php b/src/Exceptions/RoleAlreadyExists.php
index 939c55bac..141be8d26 100644
--- a/src/Exceptions/RoleAlreadyExists.php
+++ b/src/Exceptions/RoleAlreadyExists.php
@@ -8,6 +8,9 @@ class RoleAlreadyExists extends InvalidArgumentException
{
public static function create(string $roleName, string $guardName)
{
- return new static("A role `{$roleName}` already exists for guard `{$guardName}`.");
+ return new static(__('A role `:role` already exists for guard `:guard`.', [
+ 'role' => $roleName,
+ 'guard' => $guardName,
+ ]));
}
}
diff --git a/src/Exceptions/RoleDoesNotExist.php b/src/Exceptions/RoleDoesNotExist.php
index cee34e146..641b4d37f 100644
--- a/src/Exceptions/RoleDoesNotExist.php
+++ b/src/Exceptions/RoleDoesNotExist.php
@@ -6,13 +6,23 @@
class RoleDoesNotExist extends InvalidArgumentException
{
- public static function named(string $roleName)
+ public static function named(string $roleName, ?string $guardName)
{
- return new static("There is no role named `{$roleName}`.");
+ return new static(__('There is no role named `:role` for guard `:guard`.', [
+ 'role' => $roleName,
+ 'guard' => $guardName,
+ ]));
}
- public static function withId(int $roleId)
+ /**
+ * @param int|string $roleId
+ * @return static
+ */
+ public static function withId($roleId, ?string $guardName)
{
- return new static("There is no role with id `{$roleId}`.");
+ return new static(__('There is no role with ID `:id` for guard `:guard`.', [
+ 'id' => $roleId,
+ 'guard' => $guardName,
+ ]));
}
}
diff --git a/src/Exceptions/UnauthorizedException.php b/src/Exceptions/UnauthorizedException.php
index 373f426cc..b9e9fe902 100644
--- a/src/Exceptions/UnauthorizedException.php
+++ b/src/Exceptions/UnauthorizedException.php
@@ -2,6 +2,7 @@
namespace Spatie\Permission\Exceptions;
+use Illuminate\Contracts\Auth\Access\Authorizable;
use Symfony\Component\HttpKernel\Exception\HttpException;
class UnauthorizedException extends HttpException
@@ -12,11 +13,10 @@ class UnauthorizedException extends HttpException
public static function forRoles(array $roles): self
{
- $message = 'User does not have the right roles.';
+ $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 :roles', ['roles' => implode(', ', $roles)]);
}
$exception = new static(403, $message, null, []);
@@ -27,11 +27,10 @@ public static function forRoles(array $roles): self
public static function forPermissions(array $permissions): self
{
- $message = 'User does not have the right permissions.';
+ $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 :permissions', ['permissions' => implode(', ', $permissions)]);
}
$exception = new static(403, $message, null, []);
@@ -42,11 +41,10 @@ public static function forPermissions(array $permissions): self
public static function forRolesOrPermissions(array $rolesOrPermissions): self
{
- $message = 'User does not have any of the necessary access rights.';
+ $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 :values', ['values' => implode(', ', $rolesOrPermissions)]);
}
$exception = new static(403, $message, null, []);
@@ -55,9 +53,18 @@ public static function forRolesOrPermissions(array $rolesOrPermissions): self
return $exception;
}
+ public static function missingTraitHasRoles(Authorizable $user): self
+ {
+ $class = get_class($user);
+
+ return new static(403, __('Authorizable class `:class` must use Spatie\\Permission\\Traits\\HasRoles trait.', [
+ 'class' => $class,
+ ]), null, []);
+ }
+
public static function notLoggedIn(): self
{
- return new static(403, 'User is not logged in.', null, []);
+ return new static(403, __('User is not logged in.'), null, []);
}
public function getRequiredRoles(): array
diff --git a/src/Exceptions/WildcardPermissionInvalidArgument.php b/src/Exceptions/WildcardPermissionInvalidArgument.php
new file mode 100644
index 000000000..2c0a802ee
--- /dev/null
+++ b/src/Exceptions/WildcardPermissionInvalidArgument.php
@@ -0,0 +1,13 @@
+ $permission,
+ ]));
+ }
+}
diff --git a/src/Guard.php b/src/Guard.php
index a99265337..3e157be63 100644
--- a/src/Guard.php
+++ b/src/Guard.php
@@ -2,25 +2,32 @@
namespace Spatie\Permission;
+use Illuminate\Contracts\Auth\Access\Authorizable;
+use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection;
+use Illuminate\Support\Facades\Auth;
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 Collection
+ * 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
*/
- public static function getNames($model) : Collection
+ public static function getNames($model): Collection
{
+ $class = is_object($model) ? get_class($model) : $model;
+
if (is_object($model)) {
- $guardName = $model->guard_name ?? null;
+ if (\method_exists($model, 'guardName')) {
+ $guardName = $model->guardName();
+ } else {
+ $guardName = $model->getAttributeValue('guard_name');
+ }
}
if (! isset($guardName)) {
- $class = is_object($model) ? get_class($model) : $model;
-
$guardName = (new \ReflectionClass($class))->getDefaultProperties()['guard_name'] ?? null;
}
@@ -28,24 +35,110 @@ public static function getNames($model) : Collection
return collect($guardName);
}
+ return self::getConfigAuthGuards($class);
+ }
+
+ /**
+ * Get the model class associated with a given provider.
+ */
+ protected static function getProviderModel(string $provider): ?string
+ {
+ // Get the provider configuration
+ $providerConfig = config("auth.providers.{$provider}");
+
+ // Handle LDAP provider or standard Eloquent provider
+ if (isset($providerConfig['driver']) && $providerConfig['driver'] === 'ldap') {
+ return $providerConfig['database']['model'] ?? null;
+ }
+
+ return $providerConfig['model'] ?? null;
+ }
+
+ /**
+ * 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
+ */
+ 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");
- })
- ->filter(function ($model) use ($class) {
- return $class === $model;
+ return static::getProviderModel($guard['provider']);
})
+ ->filter(fn ($model) => $class === $model)
->keys();
}
+ /**
+ * Get the model associated with a given guard name.
+ */
+ public static function getModelForGuard(string $guard): ?string
+ {
+ // Get the provider configuration for the given guard
+ $provider = config("auth.guards.{$guard}.provider");
+
+ if (! $provider) {
+ return null;
+ }
+
+ return static::getProviderModel($provider);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Lookup a passport guard
+ */
+ public static function getPassportClient($guard): ?Authorizable
+ {
+ $guards = collect(config('auth.guards'))->where('driver', 'passport');
+
+ if (! $guards->count()) {
+ return null;
+ }
+
+ $authGuard = Auth::guard($guards->keys()[0]);
+
+ if (! \method_exists($authGuard, 'client')) {
+ return null;
+ }
+
+ $client = $authGuard->client();
+
+ if (! $guard || ! $client) {
+ return $client;
+ }
+
+ if (self::getNames($client)->contains($guard)) {
+ return $client;
+ }
+
+ return null;
}
}
diff --git a/src/Middleware/PermissionMiddleware.php b/src/Middleware/PermissionMiddleware.php
new file mode 100644
index 000000000..9a157611f
--- /dev/null
+++ b/src/Middleware/PermissionMiddleware.php
@@ -0,0 +1,81 @@
+user();
+
+ // For machine-to-machine Passport clients
+ if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) {
+ $user = Guard::getPassportClient($guard);
+ }
+
+ if (! $user) {
+ throw UnauthorizedException::notLoggedIn();
+ }
+
+ if (! method_exists($user, 'hasAnyPermission')) {
+ throw UnauthorizedException::missingTraitHasRoles($user);
+ }
+
+ $permissions = explode('|', self::parsePermissionsToString($permission));
+
+ if (! $user->canAny($permissions)) {
+ throw UnauthorizedException::forPermissions($permissions);
+ }
+
+ return $next($request);
+ }
+
+ /**
+ * Specify the permission and guard for the middleware.
+ *
+ * @param array|string|\BackedEnum $permission
+ * @param string|null $guard
+ * @return string
+ */
+ public static function using($permission, $guard = null)
+ {
+ // Convert Enum to its value if an Enum is passed
+ if ($permission instanceof \BackedEnum) {
+ $permission = $permission->value;
+ }
+
+ $permissionString = self::parsePermissionsToString($permission);
+
+ $args = is_null($guard) ? $permissionString : "$permissionString,$guard";
+
+ return static::class.':'.$args;
+ }
+
+ /**
+ * Convert array or string of permissions to string representation.
+ *
+ * @return string
+ */
+ protected static function parsePermissionsToString(array|string|\BackedEnum $permission)
+ {
+ // Convert Enum to its value if an Enum is passed
+ if ($permission instanceof \BackedEnum) {
+ $permission = $permission->value;
+ }
+
+ if (is_array($permission)) {
+ $permission = array_map(fn ($r) => $r instanceof \BackedEnum ? $r->value : $r, $permission);
+
+ return implode('|', $permission);
+ }
+
+ return (string) $permission;
+ }
+}
diff --git a/src/Middleware/RoleMiddleware.php b/src/Middleware/RoleMiddleware.php
new file mode 100644
index 000000000..edc02ff90
--- /dev/null
+++ b/src/Middleware/RoleMiddleware.php
@@ -0,0 +1,76 @@
+user();
+
+ // For machine-to-machine Passport clients
+ if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) {
+ $user = Guard::getPassportClient($guard);
+ }
+
+ if (! $user) {
+ throw UnauthorizedException::notLoggedIn();
+ }
+
+ if (! method_exists($user, 'hasAnyRole')) {
+ throw UnauthorizedException::missingTraitHasRoles($user);
+ }
+
+ $roles = explode('|', self::parseRolesToString($role));
+
+ if (! $user->hasAnyRole($roles)) {
+ throw UnauthorizedException::forRoles($roles);
+ }
+
+ return $next($request);
+ }
+
+ /**
+ * Specify the role and guard for the middleware.
+ *
+ * @param array|string|\BackedEnum $role
+ * @param string|null $guard
+ * @return string
+ */
+ public static function using($role, $guard = null)
+ {
+ $roleString = self::parseRolesToString($role);
+
+ $args = is_null($guard) ? $roleString : "$roleString,$guard";
+
+ return static::class.':'.$args;
+ }
+
+ /**
+ * Convert array or string of roles to string representation.
+ *
+ * @return string
+ */
+ protected static function parseRolesToString(array|string|\BackedEnum $role)
+ {
+ // Convert Enum to its value if an Enum is passed
+ if ($role instanceof \BackedEnum) {
+ $role = $role->value;
+ }
+
+ if (is_array($role)) {
+ $role = array_map(fn ($r) => $r instanceof \BackedEnum ? $r->value : $r, $role);
+
+ return implode('|', $role);
+ }
+
+ return (string) $role;
+ }
+}
diff --git a/src/Middleware/RoleOrPermissionMiddleware.php b/src/Middleware/RoleOrPermissionMiddleware.php
new file mode 100644
index 000000000..6e9fa691c
--- /dev/null
+++ b/src/Middleware/RoleOrPermissionMiddleware.php
@@ -0,0 +1,56 @@
+user();
+
+ // For machine-to-machine Passport clients
+ if (! $user && $request->bearerToken() && config('permission.use_passport_client_credentials')) {
+ $user = Guard::getPassportClient($guard);
+ }
+
+ if (! $user) {
+ throw UnauthorizedException::notLoggedIn();
+ }
+
+ if (! method_exists($user, 'hasAnyRole') || ! method_exists($user, 'hasAnyPermission')) {
+ throw UnauthorizedException::missingTraitHasRoles($user);
+ }
+
+ $rolesOrPermissions = is_array($roleOrPermission)
+ ? $roleOrPermission
+ : explode('|', $roleOrPermission);
+
+ if (! $user->canAny($rolesOrPermissions) && ! $user->hasAnyRole($rolesOrPermissions)) {
+ throw UnauthorizedException::forRolesOrPermissions($rolesOrPermissions);
+ }
+
+ return $next($request);
+ }
+
+ /**
+ * Specify the role or permission and guard for the middleware.
+ *
+ * @param array|string $roleOrPermission
+ * @param string|null $guard
+ * @return string
+ */
+ public static function using($roleOrPermission, $guard = null)
+ {
+ $roleOrPermissionString = is_string($roleOrPermission) ? $roleOrPermission : implode('|', $roleOrPermission);
+ $args = is_null($guard) ? $roleOrPermissionString : "$roleOrPermissionString,$guard";
+
+ return static::class.':'.$args;
+ }
+}
diff --git a/src/Middlewares/PermissionMiddleware.php b/src/Middlewares/PermissionMiddleware.php
deleted file mode 100644
index 5b2112428..000000000
--- a/src/Middlewares/PermissionMiddleware.php
+++ /dev/null
@@ -1,28 +0,0 @@
-guest()) {
- throw UnauthorizedException::notLoggedIn();
- }
-
- $permissions = is_array($permission)
- ? $permission
- : explode('|', $permission);
-
- foreach ($permissions as $permission) {
- if (app('auth')->user()->can($permission)) {
- return $next($request);
- }
- }
-
- throw UnauthorizedException::forPermissions($permissions);
- }
-}
diff --git a/src/Middlewares/RoleMiddleware.php b/src/Middlewares/RoleMiddleware.php
deleted file mode 100644
index 6c238f175..000000000
--- a/src/Middlewares/RoleMiddleware.php
+++ /dev/null
@@ -1,27 +0,0 @@
-hasAnyRole($roles)) {
- throw UnauthorizedException::forRoles($roles);
- }
-
- return $next($request);
- }
-}
diff --git a/src/Middlewares/RoleOrPermissionMiddleware.php b/src/Middlewares/RoleOrPermissionMiddleware.php
deleted file mode 100644
index bb2f94028..000000000
--- a/src/Middlewares/RoleOrPermissionMiddleware.php
+++ /dev/null
@@ -1,27 +0,0 @@
-hasAnyRole($rolesOrPermissions) && ! Auth::user()->hasAnyPermission($rolesOrPermissions)) {
- throw UnauthorizedException::forRolesOrPermissions($rolesOrPermissions);
- }
-
- return $next($request);
- }
-}
diff --git a/src/Models/Permission.php b/src/Models/Permission.php
index a12c2854f..490a6c23c 100644
--- a/src/Models/Permission.php
+++ b/src/Models/Permission.php
@@ -2,48 +2,53 @@
namespace Spatie\Permission\Models;
-use Spatie\Permission\Guard;
-use Illuminate\Support\Collection;
-use Spatie\Permission\Traits\HasRoles;
+use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
-use Spatie\Permission\PermissionRegistrar;
-use Spatie\Permission\Traits\RefreshesPermissionCache;
-use Illuminate\Database\Eloquent\Relations\MorphToMany;
-use Spatie\Permission\Exceptions\PermissionDoesNotExist;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
-use Spatie\Permission\Exceptions\PermissionAlreadyExists;
use Spatie\Permission\Contracts\Permission as PermissionContract;
+use Spatie\Permission\Exceptions\PermissionAlreadyExists;
+use Spatie\Permission\Exceptions\PermissionDoesNotExist;
+use Spatie\Permission\Guard;
+use Spatie\Permission\PermissionRegistrar;
+use Spatie\Permission\Traits\HasRoles;
+use Spatie\Permission\Traits\RefreshesPermissionCache;
+/**
+ * @property ?\Illuminate\Support\Carbon $created_at
+ * @property ?\Illuminate\Support\Carbon $updated_at
+ */
class Permission extends Model implements PermissionContract
{
use HasRoles;
use RefreshesPermissionCache;
- public $guarded = ['id'];
+ protected $guarded = [];
public function __construct(array $attributes = [])
{
- $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard');
+ $attributes['guard_name'] ??= Guard::getDefaultName(static::class);
parent::__construct($attributes);
- $this->setTable(config('permission.table_names.permissions'));
+ $this->guarded[] = $this->primaryKey;
+ $this->table = config('permission.table_names.permissions') ?: parent::getTable();
}
+ /**
+ * @return PermissionContract|Permission
+ *
+ * @throws PermissionAlreadyExists
+ */
public static function create(array $attributes = [])
{
- $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class);
+ $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']);
}
- if (isNotLumen() && app()::VERSION < '5.4') {
- return parent::create($attributes);
- }
-
return static::query()->create($attributes);
}
@@ -54,20 +59,22 @@ public function roles(): BelongsToMany
{
return $this->belongsToMany(
config('permission.models.role'),
- config('permission.table_names.role_has_permissions')
+ config('permission.table_names.role_has_permissions'),
+ app(PermissionRegistrar::class)->pivotPermission,
+ app(PermissionRegistrar::class)->pivotRole
);
}
/**
* A permission belongs to some users of the model associated with its guard.
*/
- public function users(): MorphToMany
+ 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',
+ app(PermissionRegistrar::class)->pivotPermission,
config('permission.column_names.model_morph_key')
);
}
@@ -75,17 +82,14 @@ public function users(): MorphToMany
/**
* Find a permission by its name (and optionally guardName).
*
- * @param string $name
- * @param string|null $guardName
+ * @return PermissionContract|Permission
*
- * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist
- *
- * @return \Spatie\Permission\Contracts\Permission
+ * @throws PermissionDoesNotExist
*/
- public static function findByName(string $name, $guardName = null): PermissionContract
+ public static function findByName(string $name, ?string $guardName = null): PermissionContract
{
- $guardName = $guardName ?? Guard::getDefaultName(static::class);
- $permission = static::getPermissions(['name' => $name, 'guard_name' => $guardName])->first();
+ $guardName ??= Guard::getDefaultName(static::class);
+ $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]);
if (! $permission) {
throw PermissionDoesNotExist::create($name, $guardName);
}
@@ -96,17 +100,14 @@ public static function findByName(string $name, $guardName = null): PermissionCo
/**
* Find a permission by its id (and optionally guardName).
*
- * @param int $id
- * @param string|null $guardName
- *
- * @throws \Spatie\Permission\Exceptions\PermissionDoesNotExist
+ * @return PermissionContract|Permission
*
- * @return \Spatie\Permission\Contracts\Permission
+ * @throws PermissionDoesNotExist
*/
- public static function findById(int $id, $guardName = null): PermissionContract
+ public static function findById(int|string $id, ?string $guardName = null): PermissionContract
{
- $guardName = $guardName ?? Guard::getDefaultName(static::class);
- $permission = static::getPermissions(['id' => $id, 'guard_name' => $guardName])->first();
+ $guardName ??= Guard::getDefaultName(static::class);
+ $permission = static::getPermission([(new static)->getKeyName() => $id, 'guard_name' => $guardName]);
if (! $permission) {
throw PermissionDoesNotExist::withId($id, $guardName);
@@ -118,15 +119,12 @@ public static function findById(int $id, $guardName = null): PermissionContract
/**
* Find or create permission by its name (and optionally guardName).
*
- * @param string $name
- * @param string|null $guardName
- *
- * @return \Spatie\Permission\Contracts\Permission
+ * @return PermissionContract|Permission
*/
- public static function findOrCreate(string $name, $guardName = null): PermissionContract
+ public static function findOrCreate(string $name, ?string $guardName = null): PermissionContract
{
- $guardName = $guardName ?? Guard::getDefaultName(static::class);
- $permission = static::getPermissions(['name' => $name, 'guard_name' => $guardName])->first();
+ $guardName ??= Guard::getDefaultName(static::class);
+ $permission = static::getPermission(['name' => $name, 'guard_name' => $guardName]);
if (! $permission) {
return static::query()->create(['name' => $name, 'guard_name' => $guardName]);
@@ -138,8 +136,21 @@ public static function findOrCreate(string $name, $guardName = null): Permission
/**
* Get the current cached permissions.
*/
- 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, $onlyOne);
+ }
+
+ /**
+ * Get the current cached first permission.
+ *
+ * @return PermissionContract|Permission|null
+ */
+ protected static function getPermission(array $params = []): ?PermissionContract
{
- return app(PermissionRegistrar::class)->getPermissions($params);
+ /** @var PermissionContract|null */
+ return static::getPermissions($params, true)->first();
}
}
diff --git a/src/Models/Role.php b/src/Models/Role.php
index 893f44223..7aa729dc1 100644
--- a/src/Models/Role.php
+++ b/src/Models/Role.php
@@ -2,43 +2,60 @@
namespace Spatie\Permission\Models;
-use Spatie\Permission\Guard;
use Illuminate\Database\Eloquent\Model;
-use Spatie\Permission\Traits\HasPermissions;
-use Spatie\Permission\Exceptions\RoleDoesNotExist;
+use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+use Spatie\Permission\Contracts\Role as RoleContract;
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
+use Spatie\Permission\Exceptions\PermissionDoesNotExist;
use Spatie\Permission\Exceptions\RoleAlreadyExists;
-use Spatie\Permission\Contracts\Role as RoleContract;
+use Spatie\Permission\Exceptions\RoleDoesNotExist;
+use Spatie\Permission\Guard;
+use Spatie\Permission\PermissionRegistrar;
+use Spatie\Permission\Traits\HasPermissions;
use Spatie\Permission\Traits\RefreshesPermissionCache;
-use Illuminate\Database\Eloquent\Relations\MorphToMany;
-use Illuminate\Database\Eloquent\Relations\BelongsToMany;
+/**
+ * @property ?\Illuminate\Support\Carbon $created_at
+ * @property ?\Illuminate\Support\Carbon $updated_at
+ */
class Role extends Model implements RoleContract
{
use HasPermissions;
use RefreshesPermissionCache;
- public $guarded = ['id'];
+ protected $guarded = [];
public function __construct(array $attributes = [])
{
- $attributes['guard_name'] = $attributes['guard_name'] ?? config('auth.defaults.guard');
+ $attributes['guard_name'] ??= Guard::getDefaultName(static::class);
parent::__construct($attributes);
- $this->setTable(config('permission.table_names.roles'));
+ $this->guarded[] = $this->primaryKey;
+ $this->table = config('permission.table_names.roles') ?: parent::getTable();
}
+ /**
+ * @return RoleContract|Role
+ *
+ * @throws RoleAlreadyExists
+ */
public static function create(array $attributes = [])
{
- $attributes['guard_name'] = $attributes['guard_name'] ?? Guard::getDefaultName(static::class);
+ $attributes['guard_name'] ??= Guard::getDefaultName(static::class);
- if (static::where('name', $attributes['name'])->where('guard_name', $attributes['guard_name'])->first()) {
- throw RoleAlreadyExists::create($attributes['name'], $attributes['guard_name']);
- }
+ $params = ['name' => $attributes['name'], 'guard_name' => $attributes['guard_name']];
+ if (app(PermissionRegistrar::class)->teams) {
+ $teamsKey = app(PermissionRegistrar::class)->teamsKey;
- if (isNotLumen() && app()::VERSION < '5.4') {
- return parent::create($attributes);
+ if (array_key_exists($teamsKey, $attributes)) {
+ $params[$teamsKey] = $attributes[$teamsKey];
+ } else {
+ $attributes[$teamsKey] = getPermissionsTeamId();
+ }
+ }
+ if (static::findByParam($params)) {
+ throw RoleAlreadyExists::create($attributes['name'], $attributes['guard_name']);
}
return static::query()->create($attributes);
@@ -51,20 +68,22 @@ public function permissions(): BelongsToMany
{
return $this->belongsToMany(
config('permission.models.permission'),
- config('permission.table_names.role_has_permissions')
+ config('permission.table_names.role_has_permissions'),
+ app(PermissionRegistrar::class)->pivotRole,
+ app(PermissionRegistrar::class)->pivotPermission
);
}
/**
* A role belongs to some users of the model associated with its guard.
*/
- public function users(): MorphToMany
+ 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',
+ app(PermissionRegistrar::class)->pivotRole,
config('permission.column_names.model_morph_key')
);
}
@@ -72,34 +91,36 @@ public function users(): MorphToMany
/**
* Find a role by its name and guard name.
*
- * @param string $name
- * @param string|null $guardName
- *
- * @return \Spatie\Permission\Contracts\Role|\Spatie\Permission\Models\Role
+ * @return RoleContract|Role
*
- * @throws \Spatie\Permission\Exceptions\RoleDoesNotExist
+ * @throws RoleDoesNotExist
*/
- public static function findByName(string $name, $guardName = null): RoleContract
+ public static function findByName(string $name, ?string $guardName = null): RoleContract
{
- $guardName = $guardName ?? Guard::getDefaultName(static::class);
+ $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);
+ throw RoleDoesNotExist::named($name, $guardName);
}
return $role;
}
- public static function findById(int $id, $guardName = null): RoleContract
+ /**
+ * Find a role by its id (and optionally guardName).
+ *
+ * @return RoleContract|Role
+ */
+ public static function findById(int|string $id, ?string $guardName = null): RoleContract
{
- $guardName = $guardName ?? Guard::getDefaultName(static::class);
+ $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);
+ throw RoleDoesNotExist::withId($id, $guardName);
}
return $role;
@@ -108,49 +129,66 @@ public static function findById(int $id, $guardName = null): RoleContract
/**
* Find or create role by its name (and optionally guardName).
*
- * @param string $name
- * @param string|null $guardName
- *
- * @return \Spatie\Permission\Contracts\Role
+ * @return RoleContract|Role
*/
- public static function findOrCreate(string $name, $guardName = null): RoleContract
+ public static function findOrCreate(string $name, ?string $guardName = null): RoleContract
{
- $guardName = $guardName ?? Guard::getDefaultName(static::class);
+ $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] + (app(PermissionRegistrar::class)->teams ? [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : []));
}
return $role;
}
/**
- * Determine if the user may perform the given permission.
- *
- * @param string|Permission $permission
+ * Finds a role based on an array of parameters.
*
- * @return bool
- *
- * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch
+ * @return RoleContract|Role|null
*/
- public function hasPermissionTo($permission): bool
+ protected static function findByParam(array $params = []): ?RoleContract
{
- $permissionClass = $this->getPermissionClass();
+ $query = static::query();
+
+ if (app(PermissionRegistrar::class)->teams) {
+ $teamsKey = app(PermissionRegistrar::class)->teamsKey;
- if (is_string($permission)) {
- $permission = $permissionClass->findByName($permission, $this->getDefaultGuardName());
+ $query->where(fn ($q) => $q->whereNull($teamsKey)
+ ->orWhere($teamsKey, $params[$teamsKey] ?? getPermissionsTeamId())
+ );
+ unset($params[$teamsKey]);
}
- if (is_int($permission)) {
- $permission = $permissionClass->findById($permission, $this->getDefaultGuardName());
+ foreach ($params as $key => $value) {
+ $query->where($key, $value);
}
+ return $query->first();
+ }
+
+ /**
+ * Determine if the role may perform the given permission.
+ *
+ * @param string|int|\Spatie\Permission\Contracts\Permission|\BackedEnum $permission
+ *
+ * @throws PermissionDoesNotExist|GuardDoesNotMatch
+ */
+ public function hasPermissionTo($permission, ?string $guardName = null): bool
+ {
+ if ($this->getWildcardClass()) {
+ return $this->hasWildcardPermission($permission, $guardName);
+ }
+
+ $permission = $this->filterPermission($permission, $guardName);
+
if (! $this->getGuardNames()->contains($permission->guard_name)) {
- throw GuardDoesNotMatch::create($permission->guard_name, $this->getGuardNames());
+ throw GuardDoesNotMatch::create($permission->guard_name, $guardName ? collect([$guardName]) : $this->getGuardNames());
}
- return $this->permissions->contains('id', $permission->id);
+ return $this->loadMissing('permissions')->permissions
+ ->contains($permission->getKeyName(), $permission->getKey());
}
}
diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php
index 9e2c249d0..92e6edc4e 100644
--- a/src/PermissionRegistrar.php
+++ b/src/PermissionRegistrar.php
@@ -3,74 +3,84 @@
namespace Spatie\Permission;
use Illuminate\Cache\CacheManager;
-use Illuminate\Support\Collection;
-use Spatie\Permission\Contracts\Role;
+use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Access\Gate;
+use Illuminate\Contracts\Cache\Repository;
+use Illuminate\Contracts\Cache\Store;
+use Illuminate\Database\Eloquent\Collection;
+use Illuminate\Database\Eloquent\Model;
use Spatie\Permission\Contracts\Permission;
-use Illuminate\Contracts\Auth\Access\Authorizable;
-use Spatie\Permission\Exceptions\PermissionDoesNotExist;
+use Spatie\Permission\Contracts\PermissionsTeamResolver;
+use Spatie\Permission\Contracts\Role;
class PermissionRegistrar
{
- /** @var \Illuminate\Contracts\Auth\Access\Gate */
- protected $gate;
+ protected Repository $cache;
+
+ protected CacheManager $cacheManager;
+
+ protected string $permissionClass;
+
+ protected string $roleClass;
+
+ /** @var Collection|array|null */
+ protected $permissions;
+
+ public string $pivotRole;
- /** @var \Illuminate\Contracts\Cache\Repository */
- protected $cache;
+ public string $pivotPermission;
- /** @var \Illuminate\Cache\CacheManager */
- protected $cacheManager;
+ /** @var \DateInterval|int */
+ public $cacheExpirationTime;
- /** @var string */
- protected $permissionClass;
+ public bool $teams;
- /** @var string */
- protected $roleClass;
+ protected PermissionsTeamResolver $teamResolver;
- /** @var int */
- public static $cacheExpirationTime;
+ public string $teamsKey;
- /** @var string */
- public static $cacheKey;
+ public string $cacheKey;
- /** @var string */
- public static $cacheModelKey;
+ private array $cachedRoles = [];
- /** @var bool */
- public static $cacheIsTaggable = false;
+ private array $alias = [];
+
+ private array $except = [];
+
+ private array $wildcardPermissionsIndex = [];
/**
* PermissionRegistrar constructor.
- *
- * @param \Illuminate\Contracts\Auth\Access\Gate $gate
- * @param \Illuminate\Cache\CacheManager $cacheManager
*/
- public function __construct(Gate $gate, CacheManager $cacheManager)
+ public function __construct(CacheManager $cacheManager)
{
- $this->gate = $gate;
$this->permissionClass = config('permission.models.permission');
$this->roleClass = config('permission.models.role');
+ $this->teamResolver = new (config('permission.team_resolver', DefaultTeamResolver::class));
$this->cacheManager = $cacheManager;
$this->initializeCache();
}
- protected function initializeCache()
+ public function initializeCache(): void
{
- self::$cacheExpirationTime = config('permission.cache.expiration_time', config('permission.cache_expiration_time'));
- self::$cacheKey = config('permission.cache.key');
- self::$cacheModelKey = config('permission.cache.model_key');
+ $this->cacheExpirationTime = config('permission.cache.expiration_time') ?: \DateInterval::createFromDateString('24 hours');
- $cache = $this->getCacheStoreFromConfig();
+ $this->teams = config('permission.teams', false);
+ $this->teamsKey = config('permission.column_names.team_foreign_key', 'team_id');
- self::$cacheIsTaggable = ($cache->getStore() instanceof \Illuminate\Cache\TaggableStore);
+ $this->cacheKey = config('permission.cache.key');
- $this->cache = self::$cacheIsTaggable ? $cache->tags(self::$cacheKey) : $cache;
+ $this->pivotRole = config('permission.column_names.role_pivot_key') ?: 'role_id';
+ $this->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
@@ -87,18 +97,35 @@ protected function getCacheStoreFromConfig(): \Illuminate\Contracts\Cache\Reposi
}
/**
- * Register the permission check method on the gate.
+ * Set the team id for teams/groups support, this id is used when querying permissions/roles
*
- * @return bool
+ * @param int|string|\Illuminate\Database\Eloquent\Model|null $id
*/
- public function registerPermissions(): bool
+ public function setPermissionsTeamId($id): void
{
- $this->gate->before(function (Authorizable $user, string $ability) {
- try {
- if (method_exists($user, 'hasPermissionTo')) {
- return $user->hasPermissionTo($ability) ?: null;
- }
- } catch (PermissionDoesNotExist $e) {
+ $this->teamResolver->setPermissionsTeamId($id);
+ }
+
+ /**
+ * @return int|string|null
+ */
+ public function getPermissionsTeamId()
+ {
+ return $this->teamResolver->getPermissionsTeamId();
+ }
+
+ /**
+ * Register the permission check method on the gate.
+ * We resolve the Gate fresh here, for benefit of long-running instances.
+ */
+ public function registerPermissions(Gate $gate): bool
+ {
+ $gate->before(function (Authorizable $user, string $ability, array &$args = []) {
+ if (is_string($args[0] ?? null) && ! class_exists($args[0])) {
+ $guard = array_shift($args);
+ }
+ if (method_exists($user, 'checkPermissionTo')) {
+ return $user->checkPermissionTo($ability, $guard ?? null) ?: null;
}
});
@@ -110,80 +137,265 @@ public function registerPermissions(): bool
*/
public function forgetCachedPermissions()
{
- self::$cacheIsTaggable ? $this->cache->flush() : $this->cache->forget(self::$cacheKey);
+ $this->permissions = null;
+ $this->forgetWildcardPermissionIndex();
+
+ return $this->cache->forget($this->cacheKey);
+ }
+
+ public function forgetWildcardPermissionIndex(?Model $record = null): void
+ {
+ if ($record) {
+ unset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()]);
+
+ return;
+ }
+
+ $this->wildcardPermissionsIndex = [];
+ }
+
+ public function getWildcardPermissionIndex(Model $record): array
+ {
+ if (isset($this->wildcardPermissionsIndex[get_class($record)][$record->getKey()])) {
+ return $this->wildcardPermissionsIndex[get_class($record)][$record->getKey()];
+ }
+
+ return $this->wildcardPermissionsIndex[get_class($record)][$record->getKey()] = app($record->getWildcardClass(), ['record' => $record])->getIndex();
}
/**
- * Get the permissions based on the passed params.
- *
- * @param array $params
+ * Clear already-loaded permissions collection.
+ * This is only intended to be called by the PermissionServiceProvider on boot,
+ * so that long-running instances like Octane or Swoole don't keep old data in memory.
+ */
+ public function clearPermissionsCollection(): void
+ {
+ $this->permissions = null;
+ $this->wildcardPermissionsIndex = [];
+ }
+
+ /**
+ * @deprecated
*
- * @return \Illuminate\Support\Collection
+ * @alias of clearPermissionsCollection()
*/
- public function getPermissions(array $params = []): Collection
- {
- $permissions = $this->cache->remember($this->getKey($params), self::$cacheExpirationTime,
- function () use ($params) {
- return $this->getPermissionClass()
- ->when($params && self::$cacheIsTaggable, function ($query) use ($params) {
- return $query->where($params);
- })
- ->with('roles')
- ->get();
- });
-
- if (! self::$cacheIsTaggable) {
- foreach ($params as $attr => $value) {
- $permissions = $permissions->where($attr, $value);
- }
+ public function clearClassPermissions()
+ {
+ $this->clearPermissionsCollection();
+ }
+
+ /**
+ * Load permissions from cache
+ * And turns permissions array into a \Illuminate\Database\Eloquent\Collection
+ */
+ private function loadPermissions(): void
+ {
+ if ($this->permissions) {
+ return;
}
- return $permissions;
+ $this->permissions = $this->cache->remember(
+ $this->cacheKey, $this->cacheExpirationTime, fn () => $this->getSerializedPermissionsForCache()
+ );
+
+ $this->alias = $this->permissions['alias'];
+
+ $this->hydrateRolesCache();
+
+ $this->permissions = $this->getHydratedPermissionCollection();
+
+ $this->cachedRoles = $this->alias = $this->except = [];
}
/**
- * Get the key for caching.
- *
- * @param $params
- *
- * @return string
+ * Get the permissions based on the passed params.
*/
- public function getKey(array $params): string
+ public function getPermissions(array $params = [], bool $onlyOne = false): Collection
{
- if ($params && self::$cacheIsTaggable) {
- return self::$cacheKey.'.'.implode('.', array_values($params));
+ $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;
+ }
+ }
+
+ return true;
+ });
+
+ if ($onlyOne) {
+ $permissions = new Collection($permissions ? [$permissions] : []);
}
- return self::$cacheKey;
+ return $permissions;
+ }
+
+ public function getPermissionClass(): string
+ {
+ return $this->permissionClass;
+ }
+
+ public function setPermissionClass($permissionClass)
+ {
+ $this->permissionClass = $permissionClass;
+ config()->set('permission.models.permission', $permissionClass);
+ app()->bind(Permission::class, $permissionClass);
+
+ return $this;
+ }
+
+ public function getRoleClass(): string
+ {
+ return $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();
+ }
+
+ protected function getPermissionsWithRoles(): Collection
+ {
+ return $this->permissionClass::select()->with('roles')->get();
}
/**
- * Get an instance of the permission class.
- *
- * @return \Spatie\Permission\Contracts\Permission
+ * Changes array keys with alias
*/
- public function getPermissionClass(): Permission
+ private function aliasedArray($model): array
{
- return app($this->permissionClass);
+ return collect(is_array($model) ? $model : $model->getAttributes())->except($this->except)
+ ->keyBy(fn ($value, $key) => $this->alias[$key] ?? $key)
+ ->all();
}
/**
- * Get an instance of the role class.
- *
- * @return \Spatie\Permission\Contracts\Role
+ * Array for cache alias
*/
- public function getRoleClass(): Role
+ private function aliasModelFields($newKeys = []): void
{
- return app($this->roleClass);
+ $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));
}
- /**
- * Get the instance of the Cache Store.
- *
- * @return \Illuminate\Contracts\Cache\Store
+ /*
+ * Make the cache smaller using an array with only required fields
*/
- public function getCacheStore(): \Illuminate\Contracts\Cache\Store
+ private function getSerializedPermissionsForCache(): array
{
- return $this->cache->getStore();
+ $this->except = config('permission.cache.column_names_except', ['created_at', 'updated_at', 'deleted_at']);
+
+ $permissions = $this->getPermissionsWithRoles()
+ ->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): array
+ {
+ 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(): Collection
+ {
+ $permissionInstance = (new ($this->getPermissionClass())())->newInstance([], true);
+
+ return Collection::make(array_map(
+ fn ($item) => (clone $permissionInstance)
+ ->setRawAttributes($this->aliasedArray(array_diff_key($item, ['r' => 0])), true)
+ ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? [])),
+ $this->permissions['permissions']
+ ));
+ }
+
+ private function getHydratedRoleCollection(array $roles): Collection
+ {
+ return Collection::make(array_values(
+ array_intersect_key($this->cachedRoles, array_flip($roles))
+ ));
+ }
+
+ private function hydrateRolesCache(): void
+ {
+ $roleInstance = (new ($this->getRoleClass())())->newInstance([], true);
+
+ array_map(function ($item) use ($roleInstance) {
+ $role = (clone $roleInstance)
+ ->setRawAttributes($this->aliasedArray($item), true);
+ $this->cachedRoles[$role->getKey()] = $role;
+ }, $this->permissions['roles']);
+
+ $this->permissions['roles'] = [];
+ }
+
+ public static function isUid($value): bool
+ {
+ if (! is_string($value) || empty(trim($value))) {
+ return false;
+ }
+
+ // check if is UUID/GUID
+ $uid = preg_match('/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iD', $value) > 0;
+ if ($uid) {
+ return true;
+ }
+
+ // check if is ULID
+ $ulid = strlen($value) == 26 && strspn($value, '0123456789ABCDEFGHJKMNPQRSTVWXYZabcdefghjkmnpqrstvwxyz') == 26 && $value[0] <= '7';
+ if ($ulid) {
+ return true;
+ }
+
+ return false;
}
}
diff --git a/src/PermissionServiceProvider.php b/src/PermissionServiceProvider.php
index fe22444a7..3aa63def0 100644
--- a/src/PermissionServiceProvider.php
+++ b/src/PermissionServiceProvider.php
@@ -2,165 +2,207 @@
namespace Spatie\Permission;
+use Composer\InstalledVersions;
+use Illuminate\Contracts\Auth\Access\Gate;
+use Illuminate\Contracts\Events\Dispatcher;
+use Illuminate\Contracts\Foundation\Application;
+use Illuminate\Filesystem\Filesystem;
+use Illuminate\Foundation\Console\AboutCommand;
use Illuminate\Routing\Route;
+use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
-use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\ServiceProvider;
use Illuminate\View\Compilers\BladeCompiler;
-use Spatie\Permission\Contracts\Role as RoleContract;
use Spatie\Permission\Contracts\Permission as PermissionContract;
+use Spatie\Permission\Contracts\Role as RoleContract;
class PermissionServiceProvider extends ServiceProvider
{
- public function boot(PermissionRegistrar $permissionLoader, Filesystem $filesystem)
+ public function boot()
{
- if (isNotLumen()) {
- $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();
- if (app()->version() >= '5.5') {
- $this->registerMacroHelpers();
- }
- }
+ $this->registerMacroHelpers();
- if ($this->app->runningInConsole()) {
- $this->commands([
- Commands\CacheReset::class,
- Commands\CreateRole::class,
- Commands\CreatePermission::class,
- ]);
- }
+ $this->registerCommands();
$this->registerModelBindings();
- $permissionLoader->registerPermissions();
+ $this->registerOctaneListener();
- $this->app->singleton(PermissionRegistrar::class, function ($app) use ($permissionLoader) {
- return $permissionLoader;
+ $this->callAfterResolving(Gate::class, function (Gate $gate, Application $app) {
+ if ($this->app['config']->get('permission.register_permission_check_method')) {
+ /** @var PermissionRegistrar $permissionLoader */
+ $permissionLoader = $app->get(PermissionRegistrar::class);
+ $permissionLoader->clearPermissionsCollection();
+ $permissionLoader->registerPermissions($gate);
+ }
});
+
+ $this->app->singleton(PermissionRegistrar::class);
+
+ $this->registerAbout();
}
public function register()
{
- if (isNotLumen()) {
- $this->mergeConfigFrom(
- __DIR__.'/../config/permission.php',
- 'permission'
- );
+ $this->mergeConfigFrom(
+ __DIR__.'/../config/permission.php',
+ 'permission'
+ );
+
+ $this->callAfterResolving('blade.compiler', fn (BladeCompiler $bladeCompiler) => $this->registerBladeExtensions($bladeCompiler));
+ }
+
+ protected function offerPublishing(): void
+ {
+ if (! $this->app->runningInConsole()) {
+ return;
}
- $this->registerBladeExtensions();
+ 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'),
+ ], 'permission-config');
+
+ $this->publishes([
+ __DIR__.'/../database/migrations/create_permission_tables.php.stub' => $this->getMigrationFileName('create_permission_tables.php'),
+ ], 'permission-migrations');
}
- protected function registerModelBindings()
+ protected function registerCommands(): void
{
- $config = $this->app->config['permission.models'];
+ $this->commands([
+ Commands\CacheReset::class,
+ ]);
- $this->app->bind(PermissionContract::class, $config['permission']);
- $this->app->bind(RoleContract::class, $config['role']);
+ if (! $this->app->runningInConsole()) {
+ return;
+ }
+
+ $this->commands([
+ Commands\CreateRole::class,
+ Commands\CreatePermission::class,
+ Commands\Show::class,
+ Commands\UpgradeForTeams::class,
+ ]);
}
- protected function registerBladeExtensions()
+ protected function registerOctaneListener(): void
{
- $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 '';
- });
+ if ($this->app->runningInConsole() || ! $this->app['config']->get('octane.listeners')) {
+ return;
+ }
+
+ $dispatcher = $this->app[Dispatcher::class];
+ // @phpstan-ignore-next-line
+ $dispatcher->listen(function (\Laravel\Octane\Contracts\OperationTerminated $event) {
+ // @phpstan-ignore-next-line
+ $event->sandbox->make(PermissionRegistrar::class)->setPermissionsTeamId(null);
+ });
+
+ if (! $this->app['config']->get('permission.register_octane_reset_listener')) {
+ return;
+ }
+ // @phpstan-ignore-next-line
+ $dispatcher->listen(function (\Laravel\Octane\Contracts\OperationTerminated $event) {
+ // @phpstan-ignore-next-line
+ $event->sandbox->make(PermissionRegistrar::class)->clearPermissionsCollection();
});
}
- protected function registerMacroHelpers()
+ protected function registerModelBindings(): void
{
- Route::macro('role', function ($roles = []) {
- if (! is_array($roles)) {
- $roles = [$roles];
- }
+ $this->app->bind(PermissionContract::class, fn ($app) => $app->make($app->config['permission.models.permission']));
+ $this->app->bind(RoleContract::class, fn ($app) => $app->make($app->config['permission.models.role']));
+ }
- $roles = implode('|', $roles);
+ public static function bladeMethodWrapper($method, $role, $guard = null): bool
+ {
+ return auth($guard)->check() && auth($guard)->user()->{$method}($role);
+ }
- $this->middleware("role:$roles");
+ protected function registerBladeExtensions(BladeCompiler $bladeCompiler): void
+ {
+ $bladeMethodWrapper = '\\Spatie\\Permission\\PermissionServiceProvider::bladeMethodWrapper';
+
+ // permission checks
+ $bladeCompiler->if('haspermission', fn () => $bladeMethodWrapper('checkPermissionTo', ...func_get_args()));
+
+ // role checks
+ $bladeCompiler->if('role', fn () => $bladeMethodWrapper('hasRole', ...func_get_args()));
+ $bladeCompiler->if('hasrole', fn () => $bladeMethodWrapper('hasRole', ...func_get_args()));
+ $bladeCompiler->if('hasanyrole', fn () => $bladeMethodWrapper('hasAnyRole', ...func_get_args()));
+ $bladeCompiler->if('hasallroles', fn () => $bladeMethodWrapper('hasAllRoles', ...func_get_args()));
+ $bladeCompiler->if('hasexactroles', fn () => $bladeMethodWrapper('hasExactRoles', ...func_get_args()));
+ $bladeCompiler->directive('endunlessrole', fn () => '');
+ }
- return $this;
- });
+ protected function registerMacroHelpers(): void
+ {
+ if (! method_exists(Route::class, 'macro')) { // @phpstan-ignore-line Lumen
+ return;
+ }
- Route::macro('permission', function ($permissions = []) {
- if (! is_array($permissions)) {
- $permissions = [$permissions];
- }
+ Route::macro('role', function ($roles = []) {
+ $roles = Arr::wrap($roles);
+ $roles = array_map(fn ($role) => $role instanceof \BackedEnum ? $role->value : $role, $roles);
- $permissions = implode('|', $permissions);
+ /** @var Route $this */
+ return $this->middleware('role:'.implode('|', $roles));
+ });
- $this->middleware("permission:$permissions");
+ Route::macro('permission', function ($permissions = []) {
+ $permissions = Arr::wrap($permissions);
+ $permissions = array_map(fn ($permission) => $permission instanceof \BackedEnum ? $permission->value : $permission, $permissions);
- return $this;
+ /** @var Route $this */
+ return $this->middleware('permission:'.implode('|', $permissions));
});
}
/**
* 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(string $migrationFileName): string
{
$timestamp = date('Y_m_d_His');
- 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")
+ $filesystem = $this->app->make(Filesystem::class);
+
+ return Collection::make([$this->app->databasePath().DIRECTORY_SEPARATOR.'migrations'.DIRECTORY_SEPARATOR])
+ ->flatMap(fn ($path) => $filesystem->glob($path.'*_'.$migrationFileName))
+ ->push($this->app->databasePath()."/migrations/{$timestamp}_{$migrationFileName}")
->first();
}
+
+ protected function registerAbout(): void
+ {
+ if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) {
+ return;
+ }
+
+ // array format: 'Display Text' => 'boolean-config-key name'
+ $features = [
+ 'Teams' => 'teams',
+ 'Wildcard-Permissions' => 'enable_wildcard_permission',
+ 'Octane-Listener' => 'register_octane_reset_listener',
+ 'Passport' => 'use_passport_client_credentials',
+ ];
+
+ $config = $this->app['config'];
+
+ AboutCommand::add('Spatie Permissions', static fn () => [
+ 'Features Enabled' => collect($features)
+ ->filter(fn (string $feature, string $name): bool => $config->get("permission.{$feature}"))
+ ->keys()
+ ->whenEmpty(fn (Collection $collection) => $collection->push('Default'))
+ ->join(', '),
+ 'Version' => InstalledVersions::getPrettyVersion('spatie/laravel-permission'),
+ ]);
+ }
}
diff --git a/src/Traits/HasPermissions.php b/src/Traits/HasPermissions.php
index 79c6fd632..ff7339b19 100644
--- a/src/Traits/HasPermissions.php
+++ b/src/Traits/HasPermissions.php
@@ -2,18 +2,30 @@
namespace Spatie\Permission\Traits;
-use Spatie\Permission\Guard;
-use Illuminate\Support\Collection;
use Illuminate\Database\Eloquent\Builder;
-use Spatie\Permission\PermissionRegistrar;
+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\Contracts\Wildcard;
+use Spatie\Permission\Events\PermissionAttached;
+use Spatie\Permission\Events\PermissionDetached;
use Spatie\Permission\Exceptions\GuardDoesNotMatch;
-use Illuminate\Database\Eloquent\Relations\MorphToMany;
use Spatie\Permission\Exceptions\PermissionDoesNotExist;
+use Spatie\Permission\Exceptions\WildcardPermissionInvalidArgument;
+use Spatie\Permission\Exceptions\WildcardPermissionNotImplementsContract;
+use Spatie\Permission\Guard;
+use Spatie\Permission\PermissionRegistrar;
+use Spatie\Permission\WildcardPermission;
trait HasPermissions
{
- private $permissionClass;
+ private ?string $permissionClass = null;
+
+ private ?string $wildcardClass = null;
+
+ private array $wildcardPermissionsIndex;
public static function bootHasPermissions()
{
@@ -22,73 +34,113 @@ public static function bootHasPermissions()
return;
}
- $model->permissions()->detach();
+ $teams = app(PermissionRegistrar::class)->teams;
+ app(PermissionRegistrar::class)->teams = false;
+ if (! is_a($model, Permission::class)) {
+ $model->permissions()->detach();
+ }
+ if (is_a($model, Role::class)) {
+ $model->users()->detach();
+ }
+ app(PermissionRegistrar::class)->teams = $teams;
});
}
- public function getPermissionClass()
+ public function getPermissionClass(): string
{
- if (! isset($this->permissionClass)) {
+ if (! $this->permissionClass) {
$this->permissionClass = app(PermissionRegistrar::class)->getPermissionClass();
}
return $this->permissionClass;
}
+ public function getWildcardClass()
+ {
+ if (! is_null($this->wildcardClass)) {
+ return $this->wildcardClass;
+ }
+
+ $this->wildcardClass = '';
+
+ if (config('permission.enable_wildcard_permission')) {
+ $this->wildcardClass = config('permission.wildcard_permission', WildcardPermission::class);
+
+ if (! is_subclass_of($this->wildcardClass, Wildcard::class)) {
+ throw WildcardPermissionNotImplementsContract::create();
+ }
+ }
+
+ return $this->wildcardClass;
+ }
+
/**
* A model may have multiple direct permissions.
*/
- public function permissions(): MorphToMany
+ 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'
+ app(PermissionRegistrar::class)->pivotPermission
);
+
+ if (! app(PermissionRegistrar::class)->teams) {
+ return $relation;
+ }
+
+ $teamsKey = app(PermissionRegistrar::class)->teamsKey;
+ $relation->withPivot($teamsKey);
+
+ return $relation->wherePivot($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
- *
- * @return \Illuminate\Database\Eloquent\Builder
+ * @param string|int|array|Permission|Collection|\BackedEnum $permissions
+ * @param bool $without
*/
- public function scopePermission(Builder $query, $permissions): Builder
+ public function scopePermission(Builder $query, $permissions, $without = false): Builder
{
$permissions = $this->convertToPermissionModels($permissions);
- $rolesWithPermissions = array_unique(array_reduce($permissions, function ($result, $permission) {
- return array_merge($result, $permission->roles->all());
- }, []));
+ $permissionKey = (new ($this->getPermissionClass())())->getKeyName();
+ $roleKey = (new (is_a($this, Role::class) ? static::class : $this->getRoleClass())())->getKeyName();
- return $query->where(function ($query) use ($permissions, $rolesWithPermissions) {
- $query->whereHas('permissions', function ($query) use ($permissions) {
- $query->where(function ($query) use ($permissions) {
- foreach ($permissions as $permission) {
- $query->orWhere(config('permission.table_names.permissions').'.id', $permission->id);
- }
- });
- });
- if (count($rolesWithPermissions) > 0) {
- $query->orWhereHas('roles', function ($query) use ($rolesWithPermissions) {
- $query->where(function ($query) use ($rolesWithPermissions) {
- foreach ($rolesWithPermissions as $role) {
- $query->orWhere(config('permission.table_names.roles').'.id', $role->id);
- }
- });
- });
- }
- });
+ $rolesWithPermissions = is_a($this, Role::class) ? [] : array_unique(
+ array_reduce($permissions, fn ($result, $permission) => array_merge($result, $permission->roles->all()), [])
+ );
+
+ return $query->where(fn (Builder $query) => $query
+ ->{! $without ? 'whereHas' : 'whereDoesntHave'}('permissions', fn (Builder $subQuery) => $subQuery
+ ->whereIn(config('permission.table_names.permissions').".$permissionKey", \array_column($permissions, $permissionKey))
+ )
+ ->when(count($rolesWithPermissions), fn ($whenQuery) => $whenQuery
+ ->{! $without ? 'orWhereHas' : 'whereDoesntHave'}('roles', fn (Builder $subQuery) => $subQuery
+ ->whereIn(config('permission.table_names.roles').".$roleKey", \array_column($rolesWithPermissions, $roleKey))
+ )
+ )
+ );
+ }
+
+ /**
+ * Scope the model query to only those without certain permissions,
+ * whether indirectly by role or by direct permission.
+ *
+ * @param string|int|array|Permission|Collection|\BackedEnum $permissions
+ */
+ public function scopeWithoutPermission(Builder $query, $permissions): Builder
+ {
+ return $this->scopePermission($query, $permissions, true);
}
/**
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
+ * @param string|int|array|Permission|Collection|\BackedEnum $permissions
*
- * @return array
+ * @throws PermissionDoesNotExist
*/
protected function convertToPermissionModels($permissions): array
{
@@ -96,69 +148,44 @@ protected function convertToPermissionModels($permissions): array
$permissions = $permissions->all();
}
- $permissions = array_wrap($permissions);
-
return array_map(function ($permission) {
if ($permission instanceof Permission) {
return $permission;
}
- return $this->getPermissionClass()->findByName($permission, $this->getDefaultGuardName());
- }, $permissions);
- }
-
- /**
- * Determine if the model may perform the given permission.
- *
- * @param string|int|\Spatie\Permission\Contracts\Permission $permission
- * @param string|null $guardName
- *
- * @return bool
- * @throws \Exception
- */
- public function hasPermissionTo($permission, $guardName = null): bool
- {
- if (! is_string($permission) && ! is_int($permission) && ! $permission instanceof Permission) {
- throw new PermissionDoesNotExist;
- }
+ if ($permission instanceof \BackedEnum) {
+ $permission = $permission->value;
+ }
- $registrar = app(PermissionRegistrar::class);
- if (! $registrar::$cacheIsTaggable) {
- return $this->hasUncachedPermissionTo($permission, $guardName);
- }
+ $method = is_int($permission) || PermissionRegistrar::isUid($permission) ? 'findById' : 'findByName';
- return $registrar->getCacheStore()
- ->tags($this->getCacheTags($permission))
- ->remember(
- $this->getPermissionCacheKey($permission),
- $registrar::$cacheExpirationTime,
- function () use ($permission, $guardName) {
- return $this->hasUncachedPermissionTo($permission, $guardName);
- }
- );
+ return $this->getPermissionClass()::{$method}($permission, $this->getDefaultGuardName());
+ }, Arr::wrap($permissions));
}
/**
- * Check the uncached permissions for the model.
+ * Find a permission.
*
- * @param string|int|Permission $permission
- * @param string|null $guardName
+ * @param string|int|Permission|\BackedEnum $permission
+ * @return Permission
*
- * @return bool
+ * @throws PermissionDoesNotExist
*/
- public function hasUncachedPermissionTo($permission, $guardName = null): bool
+ public function filterPermission($permission, $guardName = null)
{
- $permissionClass = $this->getPermissionClass();
+ if ($permission instanceof \BackedEnum) {
+ $permission = $permission->value;
+ }
- if (is_string($permission)) {
- $permission = $permissionClass->findByName(
+ if (is_int($permission) || PermissionRegistrar::isUid($permission)) {
+ $permission = $this->getPermissionClass()::findById(
$permission,
$guardName ?? $this->getDefaultGuardName()
);
}
- if (is_int($permission)) {
- $permission = $permissionClass->findById(
+ if (is_string($permission)) {
+ $permission = $this->getPermissionClass()::findByName(
$permission,
$guardName ?? $this->getDefaultGuardName()
);
@@ -168,106 +195,85 @@ public function hasUncachedPermissionTo($permission, $guardName = null): bool
throw new PermissionDoesNotExist;
}
- return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission);
+ return $permission;
}
/**
- * An alias to hasPermissionTo(), but avoids throwing an exception.
- *
- * @param string|int|\Spatie\Permission\Contracts\Permission $permission
- * @param string|null $guardName
+ * Determine if the model may perform the given permission.
*
- * @return bool
+ * @param string|int|Permission|\BackedEnum $permission
+ * @param string|null $guardName
*
- * @throws \Exception
+ * @throws PermissionDoesNotExist
*/
- public function checkPermissionTo($permission, $guardName = null): bool
+ public function hasPermissionTo($permission, $guardName = null): bool
{
- try {
- return $this->hasPermissionTo($permission, $guardName);
- } catch (PermissionDoesNotExist $e) {
- return false;
+ if ($this->getWildcardClass()) {
+ return $this->hasWildcardPermission($permission, $guardName);
}
+
+ $permission = $this->filterPermission($permission, $guardName);
+
+ return $this->hasDirectPermission($permission) || $this->hasPermissionViaRole($permission);
}
/**
- * Construct the key for the cache entry.
+ * Validates a wildcard permission against all permissions of a user.
*
- * @param null|string|int|\Spatie\Permission\Contracts\Permission $permission
- *
- * @return string
+ * @param string|int|Permission|\BackedEnum $permission
+ * @param string|null $guardName
*/
- protected function getPermissionCacheKey($permission = null)
+ protected function hasWildcardPermission($permission, $guardName = null): bool
{
- $key = PermissionRegistrar::$cacheKey.'.'.$this->getClassCacheString();
+ $guardName = $guardName ?? $this->getDefaultGuardName();
- if ($permission !== null) {
- $key .= $this->getPermissionCacheString($permission);
+ if ($permission instanceof \BackedEnum) {
+ $permission = $permission->value;
}
- return $key;
- }
-
- /**
- * Construct the tags for the cache entry.
- *
- * @param null|string|int|\Spatie\Permission\Contracts\Permission $permission
- *
- * @return array
- */
- protected function getCacheTags($permission = null)
- {
- $tags = [
- PermissionRegistrar::$cacheKey,
- $this->getClassCacheString(),
- ];
+ if (is_int($permission) || PermissionRegistrar::isUid($permission)) {
+ $permission = $this->getPermissionClass()::findById($permission, $guardName);
+ }
- if ($permission !== null) {
- $tags[] = $this->getPermissionCacheString($permission);
+ if ($permission instanceof Permission) {
+ $guardName = $permission->guard_name ?? $guardName;
+ $permission = $permission->name;
}
- return $tags;
- }
+ if (! is_string($permission)) {
+ throw WildcardPermissionInvalidArgument::create();
+ }
- /**
- * Get the key to cache the model by.
- *
- * @return string
- */
- private function getClassCacheString()
- {
- return str_replace('\\', '.', get_class($this)).'.'.$this->getKey();
+ return app($this->getWildcardClass(), ['record' => $this])->implies(
+ $permission,
+ $guardName,
+ app(PermissionRegistrar::class)->getWildcardPermissionIndex($this),
+ );
}
/**
- * Get the key to cache the permission by.
- *
- * @param string|int|\Spatie\Permission\Contracts\Permission $permission
+ * An alias to hasPermissionTo(), but avoids throwing an exception.
*
- * @return mixed
+ * @param string|int|Permission|\BackedEnum $permission
+ * @param string|null $guardName
*/
- protected function getPermissionCacheString($permission)
+ public function checkPermissionTo($permission, $guardName = null): bool
{
- if ($permission instanceof Permission) {
- $permission = $permission[PermissionRegistrar::$cacheModelKey];
+ try {
+ return $this->hasPermissionTo($permission, $guardName);
+ } catch (PermissionDoesNotExist $e) {
+ return false;
}
-
- return str_replace('\\', '.', Permission::class).'.'.$permission;
}
/**
* Determine if the model has any of the given permissions.
*
- * @param array ...$permissions
- *
- * @return bool
- * @throws \Exception
+ * @param string|int|array|Permission|Collection|\BackedEnum ...$permissions
*/
public function hasAnyPermission(...$permissions): bool
{
- if (is_array($permissions[0])) {
- $permissions = $permissions[0];
- }
+ $permissions = collect($permissions)->flatten();
foreach ($permissions as $permission) {
if ($this->checkPermissionTo($permission)) {
@@ -281,19 +287,14 @@ public function hasAnyPermission(...$permissions): bool
/**
* Determine if the model has all of the given permissions.
*
- * @param array ...$permissions
- *
- * @return bool
- * @throws \Exception
+ * @param string|int|array|Permission|Collection|\BackedEnum ...$permissions
*/
public function hasAllPermissions(...$permissions): bool
{
- if (is_array($permissions[0])) {
- $permissions = $permissions[0];
- }
+ $permissions = collect($permissions)->flatten();
foreach ($permissions as $permission) {
- if (! $this->hasPermissionTo($permission)) {
+ if (! $this->checkPermissionTo($permission)) {
return false;
}
}
@@ -303,46 +304,29 @@ public function hasAllPermissions(...$permissions): bool
/**
* Determine if the model has, via roles, the given permission.
- *
- * @param \Spatie\Permission\Contracts\Permission $permission
- *
- * @return bool
*/
protected function hasPermissionViaRole(Permission $permission): bool
{
+ if (is_a($this, Role::class)) {
+ return false;
+ }
+
return $this->hasRole($permission->roles);
}
/**
* Determine if the model has the given permission.
*
- * @param string|int|\Spatie\Permission\Contracts\Permission $permission
+ * @param string|int|Permission|\BackedEnum $permission
*
- * @return bool
+ * @throws PermissionDoesNotExist
*/
public function hasDirectPermission($permission): bool
{
- $permissionClass = $this->getPermissionClass();
-
- if (is_string($permission)) {
- $permission = $permissionClass->findByName($permission, $this->getDefaultGuardName());
- if (! $permission) {
- return false;
- }
- }
-
- if (is_int($permission)) {
- $permission = $permissionClass->findById($permission, $this->getDefaultGuardName());
- if (! $permission) {
- return false;
- }
- }
+ $permission = $this->filterPermission($permission);
- if (! $permission instanceof Permission) {
- return false;
- }
-
- return $this->permissions->contains('id', $permission->id);
+ return $this->loadMissing('permissions')->permissions
+ ->contains($permission->getKeyName(), $permission->getKey());
}
/**
@@ -350,143 +334,190 @@ public function hasDirectPermission($permission): bool
*/
public function getPermissionsViaRoles(): Collection
{
- return $this->load('roles', 'roles.permissions')
- ->roles->flatMap(function ($role) {
- return $role->permissions;
- })->sort()->values();
+ if (is_a($this, Role::class) || is_a($this, Permission::class)) {
+ return collect();
+ }
+
+ return $this->loadMissing('roles', 'roles.permissions')
+ ->roles->flatMap(fn ($role) => $role->permissions)
+ ->sort()->values();
}
/**
* Return all the permissions the model has, both directly and via roles.
- *
- * @throws \Exception
*/
public function getAllPermissions(): Collection
{
- $functionGetAllPermissions = function () {
- $permissions = $this->permissions;
+ /** @var Collection $permissions */
+ $permissions = $this->permissions;
- if ($this->roles) {
- $permissions = $permissions->merge($this->getPermissionsViaRoles());
- }
+ if (! is_a($this, Permission::class)) {
+ $permissions = $permissions->merge($this->getPermissionsViaRoles());
+ }
- return $permissions->sort()->values();
- };
+ return $permissions->sort()->values();
+ }
- $registrar = app(PermissionRegistrar::class);
- if ($registrar::$cacheIsTaggable) {
- return $registrar->getCacheStore()
- ->tags($this->getCacheTags())
- ->remember(
- $this->getPermissionCacheKey(),
- $registrar::$cacheExpirationTime,
- $functionGetAllPermissions
- );
- }
+ /**
+ * Returns array of permissions ids
+ *
+ * @param string|int|array|Permission|Collection|\BackedEnum $permissions
+ */
+ private function collectPermissions(...$permissions): array
+ {
+ return collect($permissions)
+ ->flatten()
+ ->reduce(function ($array, $permission) {
+ if (empty($permission)) {
+ return $array;
+ }
- return $functionGetAllPermissions();
+ $permission = $this->getStoredPermission($permission);
+ if (! $permission instanceof Permission) {
+ return $array;
+ }
+
+ if (! in_array($permission->getKey(), $array)) {
+ $this->ensureModelSharesGuard($permission);
+ $array[] = $permission->getKey();
+ }
+
+ return $array;
+ }, []);
}
/**
* Grant the given permission(s) to a role.
*
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
- *
+ * @param string|int|array|Permission|Collection|\BackedEnum $permissions
* @return $this
*/
public function givePermissionTo(...$permissions)
{
- $permissions = collect($permissions)
- ->flatten()
- ->map(function ($permission) {
- return $this->getStoredPermission($permission);
- })
- ->filter(function ($permission) {
- return $permission instanceof Permission;
- })
- ->each(function ($permission) {
- $this->ensureModelSharesGuard($permission);
- })
- ->map->id
- ->all();
+ $permissions = $this->collectPermissions($permissions);
$model = $this->getModel();
+ $teamPivot = app(PermissionRegistrar::class)->teams && ! is_a($this, Role::class) ?
+ [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : [];
if ($model->exists) {
- $this->permissions()->sync($permissions, false);
- $model->load('permissions');
+ $currentPermissions = $this->permissions->map(fn ($permission) => $permission->getKey())->toArray();
+
+ $this->permissions()->attach(array_diff($permissions, $currentPermissions), $teamPivot);
+ $model->unsetRelation('permissions');
} else {
$class = \get_class($model);
+ $saved = false;
$class::saved(
- function ($object) use ($permissions, $model) {
- static $modelLastFiredOn;
- if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) {
+ function ($object) use ($permissions, $model, $teamPivot, &$saved) {
+ if ($saved || $model->getKey() != $object->getKey()) {
return;
}
- $object->permissions()->sync($permissions, false);
- $object->load('permissions');
- $modelLastFiredOn = $object;
- });
+ $model->permissions()->attach($permissions, $teamPivot);
+ $model->unsetRelation('permissions');
+ $saved = true;
+ }
+ );
+ }
+
+ if (is_a($this, Role::class)) {
+ $this->forgetCachedPermissions();
+ }
+
+ if (config('permission.events_enabled')) {
+ event(new PermissionAttached($this->getModel(), $permissions));
}
- $this->forgetCachedPermissions();
+ $this->forgetWildcardPermissionIndex();
return $this;
}
+ public function forgetWildcardPermissionIndex(): void
+ {
+ app(PermissionRegistrar::class)->forgetWildcardPermissionIndex(
+ is_a($this, Role::class) ? null : $this,
+ );
+ }
+
/**
* Remove all current permissions and set the given ones.
*
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
- *
+ * @param string|int|array|Permission|Collection|\BackedEnum $permissions
* @return $this
*/
public function syncPermissions(...$permissions)
{
- $this->permissions()->detach();
+ if ($this->getModel()->exists) {
+ $this->collectPermissions($permissions);
+ $this->permissions()->detach();
+ $this->setRelation('permissions', collect());
+ }
return $this->givePermissionTo($permissions);
}
/**
- * Revoke the given permission.
- *
- * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|string|string[] $permission
+ * Revoke the given permission(s).
*
+ * @param Permission|Permission[]|string|string[]|\BackedEnum $permission
* @return $this
*/
public function revokePermissionTo($permission)
{
- $this->permissions()->detach($this->getStoredPermission($permission));
+ $storedPermission = $this->getStoredPermission($permission);
+
+ $this->permissions()->detach($storedPermission);
+
+ if (is_a($this, Role::class)) {
+ $this->forgetCachedPermissions();
+ }
+
+ if (config('permission.events_enabled')) {
+ event(new PermissionDetached($this->getModel(), $storedPermission));
+ }
- $this->forgetCachedPermissions();
+ $this->forgetWildcardPermissionIndex();
- $this->load('permissions');
+ $this->unsetRelation('permissions');
return $this;
}
+ public function getPermissionNames(): Collection
+ {
+ return $this->permissions->pluck('name');
+ }
+
/**
- * @param string|array|\Spatie\Permission\Contracts\Permission|\Illuminate\Support\Collection $permissions
- *
- * @return \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Permission[]|\Illuminate\Support\Collection
+ * @param string|int|array|Permission|Collection|\BackedEnum $permissions
+ * @return Permission|Permission[]|Collection
*/
protected function getStoredPermission($permissions)
{
- $permissionClass = $this->getPermissionClass();
+ if ($permissions instanceof \BackedEnum) {
+ $permissions = $permissions->value;
+ }
- if (is_numeric($permissions)) {
- return $permissionClass->findById($permissions, $this->getDefaultGuardName());
+ if (is_int($permissions) || PermissionRegistrar::isUid($permissions)) {
+ return $this->getPermissionClass()::findById($permissions, $this->getDefaultGuardName());
}
if (is_string($permissions)) {
- return $permissionClass->findByName($permissions, $this->getDefaultGuardName());
+ return $this->getPermissionClass()::findByName($permissions, $this->getDefaultGuardName());
}
if (is_array($permissions)) {
- return $permissionClass
- ->whereIn('name', $permissions)
+ $permissions = array_map(function ($permission) {
+ if ($permission instanceof \BackedEnum) {
+ return $permission->value;
+ }
+
+ return is_a($permission, Permission::class) ? $permission->name : $permission;
+ }, $permissions);
+
+ return $this->getPermissionClass()::whereIn('name', $permissions)
->whereIn('guard_name', $this->getGuardNames())
->get();
}
@@ -495,9 +526,9 @@ protected function getStoredPermission($permissions)
}
/**
- * @param \Spatie\Permission\Contracts\Permission|\Spatie\Permission\Contracts\Role $roleOrPermission
+ * @param Permission|Role $roleOrPermission
*
- * @throws \Spatie\Permission\Exceptions\GuardDoesNotMatch
+ * @throws GuardDoesNotMatch
*/
protected function ensureModelSharesGuard($roleOrPermission)
{
@@ -523,4 +554,40 @@ public function forgetCachedPermissions()
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
}
+
+ /**
+ * Check if the model has All of the requested Direct permissions.
+ *
+ * @param string|int|array|Permission|Collection|\BackedEnum ...$permissions
+ */
+ public function hasAllDirectPermissions(...$permissions): bool
+ {
+ $permissions = collect($permissions)->flatten();
+
+ foreach ($permissions as $permission) {
+ if (! $this->hasDirectPermission($permission)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Check if the model has Any of the requested Direct permissions.
+ *
+ * @param string|int|array|Permission|Collection|\BackedEnum ...$permissions
+ */
+ public function hasAnyDirectPermission(...$permissions): bool
+ {
+ $permissions = collect($permissions)->flatten();
+
+ foreach ($permissions as $permission) {
+ if ($this->hasDirectPermission($permission)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
}
diff --git a/src/Traits/HasRoles.php b/src/Traits/HasRoles.php
index bdf13c539..40839e94c 100644
--- a/src/Traits/HasRoles.php
+++ b/src/Traits/HasRoles.php
@@ -2,17 +2,21 @@
namespace Spatie\Permission\Traits;
+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 Illuminate\Database\Eloquent\Builder;
+use Spatie\Permission\Events\RoleAttached;
+use Spatie\Permission\Events\RoleDetached;
use Spatie\Permission\PermissionRegistrar;
-use Illuminate\Database\Eloquent\Relations\MorphToMany;
trait HasRoles
{
use HasPermissions;
- private $roleClass;
+ private ?string $roleClass = null;
public static function bootHasRoles()
{
@@ -21,13 +25,19 @@ public static function bootHasRoles()
return;
}
+ $teams = app(PermissionRegistrar::class)->teams;
+ app(PermissionRegistrar::class)->teams = false;
$model->roles()->detach();
+ if (is_a($model, Permission::class)) {
+ $model->users()->detach();
+ }
+ app(PermissionRegistrar::class)->teams = $teams;
});
}
- public function getRoleClass()
+ public function getRoleClass(): string
{
- if (! isset($this->roleClass)) {
+ if (! $this->roleClass) {
$this->roleClass = app(PermissionRegistrar::class)->getRoleClass();
}
@@ -37,102 +47,145 @@ public function getRoleClass()
/**
* A model may have multiple roles.
*/
- public function roles(): MorphToMany
+ 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'
+ app(PermissionRegistrar::class)->pivotRole
);
+
+ if (! app(PermissionRegistrar::class)->teams) {
+ return $relation;
+ }
+
+ $teamsKey = app(PermissionRegistrar::class)->teamsKey;
+ $relation->withPivot($teamsKey);
+ $teamField = config('permission.table_names.roles').'.'.$teamsKey;
+
+ return $relation->wherePivot($teamsKey, getPermissionsTeamId())
+ ->where(fn ($q) => $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
- *
- * @return \Illuminate\Database\Eloquent\Builder
+ * @param string|int|array|Role|Collection|\BackedEnum $roles
+ * @param string $guard
+ * @param bool $without
*/
- public function scopeRole(Builder $query, $roles): Builder
+ public function scopeRole(Builder $query, $roles, $guard = null, $without = false): Builder
{
if ($roles instanceof Collection) {
$roles = $roles->all();
}
- if (! is_array($roles)) {
- $roles = [$roles];
- }
-
- $roles = array_map(function ($role) {
+ $roles = array_map(function ($role) use ($guard) {
if ($role instanceof Role) {
return $role;
}
- $method = is_numeric($role) ? 'findById' : 'findByName';
+ if ($role instanceof \BackedEnum) {
+ $role = $role->value;
+ }
- return $this->getRoleClass()->{$method}($role, $this->getDefaultGuardName());
- }, $roles);
+ $method = is_int($role) || PermissionRegistrar::isUid($role) ? 'findById' : 'findByName';
- return $query->whereHas('roles', function ($query) use ($roles) {
- $query->where(function ($query) use ($roles) {
- foreach ($roles as $role) {
- $query->orWhere(config('permission.table_names.roles').'.id', $role->id);
- }
- });
- });
+ return $this->getRoleClass()::{$method}($role, $guard ?: $this->getDefaultGuardName());
+ }, Arr::wrap($roles));
+
+ $key = (new ($this->getRoleClass())())->getKeyName();
+
+ return $query->{! $without ? 'whereHas' : 'whereDoesntHave'}('roles', fn (Builder $subQuery) => $subQuery
+ ->whereIn(config('permission.table_names.roles').".$key", \array_column($roles, $key))
+ );
}
/**
- * Assign the given role to the model.
+ * Scope the model query to only those without certain roles.
*
- * @param array|string|\Spatie\Permission\Contracts\Role ...$roles
+ * @param string|int|array|Role|Collection|\BackedEnum $roles
+ * @param string $guard
+ */
+ public function scopeWithoutRole(Builder $query, $roles, $guard = null): Builder
+ {
+ return $this->scopeRole($query, $roles, $guard, true);
+ }
+
+ /**
+ * Returns array of role ids
*
- * @return $this
+ * @param string|int|array|Role|Collection|\BackedEnum $roles
*/
- public function assignRole(...$roles)
+ private function collectRoles(...$roles): array
{
- $roles = collect($roles)
+ return collect($roles)
->flatten()
- ->map(function ($role) {
+ ->reduce(function ($array, $role) {
if (empty($role)) {
- return false;
+ return $array;
+ }
+
+ $role = $this->getStoredRole($role);
+
+ if (! in_array($role->getKey(), $array)) {
+ $this->ensureModelSharesGuard($role);
+ $array[] = $role->getKey();
}
- return $this->getStoredRole($role);
- })
- ->filter(function ($role) {
- return $role instanceof Role;
- })
- ->each(function ($role) {
- $this->ensureModelSharesGuard($role);
- })
- ->map->id
- ->all();
+ return $array;
+ }, []);
+ }
+
+ /**
+ * Assign the given role to the model.
+ *
+ * @param string|int|array|Role|Collection|\BackedEnum ...$roles
+ * @return $this
+ */
+ public function assignRole(...$roles)
+ {
+ $roles = $this->collectRoles($roles);
$model = $this->getModel();
+ $teamPivot = app(PermissionRegistrar::class)->teams && ! is_a($this, Permission::class) ?
+ [app(PermissionRegistrar::class)->teamsKey => getPermissionsTeamId()] : [];
if ($model->exists) {
- $this->roles()->sync($roles, false);
- $model->load('roles');
+ if (app(PermissionRegistrar::class)->teams) {
+ // explicit reload in case team has been changed since last load
+ $this->load('roles');
+ }
+
+ $currentRoles = $this->roles->map(fn ($role) => $role->getKey())->toArray();
+
+ $this->roles()->attach(array_diff($roles, $currentRoles), $teamPivot);
+ $model->unsetRelation('roles');
} else {
$class = \get_class($model);
+ $saved = false;
$class::saved(
- function ($object) use ($roles, $model) {
- static $modelLastFiredOn;
- if ($modelLastFiredOn !== null && $modelLastFiredOn === $model) {
+ function ($object) use ($roles, $model, $teamPivot, &$saved) {
+ if ($saved || $model->getKey() != $object->getKey()) {
return;
}
- $object->roles()->sync($roles, false);
- $object->load('roles');
- $modelLastFiredOn = $object;
- });
+ $model->roles()->attach($roles, $teamPivot);
+ $model->unsetRelation('roles');
+ $saved = true;
+ }
+ );
}
- $this->forgetCachedPermissions();
+ if (is_a($this, Permission::class)) {
+ $this->forgetCachedPermissions();
+ }
+
+ if (config('permission.events_enabled')) {
+ event(new RoleAttached($this->getModel(), $roles));
+ }
return $this;
}
@@ -140,25 +193,48 @@ function ($object) use ($roles, $model) {
/**
* Revoke the given role from the model.
*
- * @param string|\Spatie\Permission\Contracts\Role $role
+ * @param string|int|array|Role|Collection|\BackedEnum ...$role
+ * @return $this
*/
- public function removeRole($role)
+ public function removeRole(...$role)
{
- $this->roles()->detach($this->getStoredRole($role));
+ $roles = $this->collectRoles($role);
+
+ $this->roles()->detach($roles);
+
+ $this->unsetRelation('roles');
+
+ if (is_a($this, Permission::class)) {
+ $this->forgetCachedPermissions();
+ }
+
+ if (config('permission.events_enabled')) {
+ event(new RoleDetached($this->getModel(), $roles));
+ }
- $this->load('roles');
+ return $this;
}
/**
* Remove all current roles and set the given ones.
*
- * @param array|\Spatie\Permission\Contracts\Role|string ...$roles
- *
+ * @param string|int|array|Role|Collection|\BackedEnum ...$roles
* @return $this
*/
public function syncRoles(...$roles)
{
- $this->roles()->detach();
+ if ($this->getModel()->exists) {
+ $this->collectRoles($roles);
+ if (config('permission.events_enabled')) {
+ $currentRoles = $this->roles()->get();
+ if ($currentRoles->isNotEmpty()) {
+ $this->removeRole($currentRoles);
+ }
+ } else {
+ $this->roles()->detach();
+ $this->setRelation('roles', collect());
+ }
+ }
return $this->assignRole($roles);
}
@@ -166,31 +242,53 @@ public function syncRoles(...$roles)
/**
* Determine if the model has (one of) the given role(s).
*
- * @param string|int|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles
- *
- * @return bool
+ * @param string|int|array|Role|Collection|\BackedEnum $roles
*/
- public function hasRole($roles): bool
+ public function hasRole($roles, ?string $guard = null): bool
{
- if (is_string($roles) && false !== strpos($roles, '|')) {
+ $this->loadMissing('roles');
+
+ if (is_string($roles) && strpos($roles, '|') !== false) {
$roles = $this->convertPipeToArray($roles);
}
- if (is_string($roles)) {
- return $this->roles->contains('name', $roles);
+ if ($roles instanceof \BackedEnum) {
+ $roles = $roles->value;
+
+ return $this->roles
+ ->when($guard, fn ($q) => $q->where('guard_name', $guard))
+ ->pluck('name')
+ ->contains(function ($name) use ($roles) {
+ /** @var string|\BackedEnum $name */
+ if ($name instanceof \BackedEnum) {
+ return $name->value == $roles;
+ }
+
+ return $name == $roles;
+ });
+ }
+
+ if (is_int($roles) || PermissionRegistrar::isUid($roles)) {
+ $key = (new ($this->getRoleClass())())->getKeyName();
+
+ return $guard
+ ? $this->roles->where('guard_name', $guard)->contains($key, $roles)
+ : $this->roles->contains($key, $roles);
}
- if (is_int($roles)) {
- return $this->roles->contains('id', $roles);
+ if (is_string($roles)) {
+ return $guard
+ ? $this->roles->where('guard_name', $guard)->contains('name', $roles)
+ : $this->roles->contains('name', $roles);
}
if ($roles instanceof Role) {
- return $this->roles->contains('id', $roles->id);
+ return $this->roles->contains($roles->getKeyName(), $roles->getKey());
}
if (is_array($roles)) {
foreach ($roles as $role) {
- if ($this->hasRole($role)) {
+ if ($this->hasRole($role, $guard)) {
return true;
}
}
@@ -198,17 +296,21 @@ public function hasRole($roles): bool
return false;
}
- return $roles->intersect($this->roles)->isNotEmpty();
+ if ($roles instanceof Collection) {
+ return $roles->intersect($guard ? $this->roles->where('guard_name', $guard) : $this->roles)->isNotEmpty();
+ }
+
+ throw new \TypeError('Unsupported type for $roles parameter to hasRole().');
}
/**
* Determine if the model has any of the given role(s).
*
- * @param string|array|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles
+ * Alias to hasRole() but without Guard controls
*
- * @return bool
+ * @param string|int|array|Role|Collection|\BackedEnum $roles
*/
- public function hasAnyRole($roles): bool
+ public function hasAnyRole(...$roles): bool
{
return $this->hasRole($roles);
}
@@ -216,29 +318,76 @@ public function hasAnyRole($roles): bool
/**
* Determine if the model has all of the given role(s).
*
- * @param string|\Spatie\Permission\Contracts\Role|\Illuminate\Support\Collection $roles
- *
- * @return bool
+ * @param string|array|Role|Collection|\BackedEnum $roles
*/
- public function hasAllRoles($roles): bool
+ public function hasAllRoles($roles, ?string $guard = null): bool
{
- if (is_string($roles) && false !== strpos($roles, '|')) {
+ $this->loadMissing('roles');
+
+ if ($roles instanceof \BackedEnum) {
+ $roles = $roles->value;
+ }
+
+ if (is_string($roles) && strpos($roles, '|') !== false) {
$roles = $this->convertPipeToArray($roles);
}
if (is_string($roles)) {
- return $this->roles->contains('name', $roles);
+ return $this->hasRole($roles, $guard);
}
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) {
+ if ($role instanceof \BackedEnum) {
+ return $role->value;
+ }
+
return $role instanceof Role ? $role->name : $role;
});
- return $roles->intersect($this->roles->pluck('name')) == $roles;
+ $roleNames = $guard
+ ? $this->roles->where('guard_name', $guard)->pluck('name')
+ : $this->getRoleNames();
+
+ $roleNames = $roleNames->transform(function ($roleName) {
+ if ($roleName instanceof \BackedEnum) {
+ return $roleName->value;
+ }
+
+ return $roleName;
+ });
+
+ return $roles->intersect($roleNames) == $roles;
+ }
+
+ /**
+ * Determine if the model has exactly all of the given role(s).
+ *
+ * @param string|array|Role|Collection|\BackedEnum $roles
+ */
+ public function hasExactRoles($roles, ?string $guard = null): bool
+ {
+ $this->loadMissing('roles');
+
+ if (is_string($roles) && strpos($roles, '|') !== false) {
+ $roles = $this->convertPipeToArray($roles);
+ }
+
+ if (is_string($roles)) {
+ $roles = [$roles];
+ }
+
+ if ($roles instanceof Role) {
+ $roles = [$roles->name];
+ }
+
+ $roles = collect()->make($roles)->map(fn ($role) => $role instanceof Role ? $role->name : $role
+ );
+
+ return $this->roles->count() == $roles->count() && $this->hasAllRoles($roles, $guard);
}
/**
@@ -251,19 +400,23 @@ public function getDirectPermissions(): Collection
public function getRoleNames(): Collection
{
+ $this->loadMissing('roles');
+
return $this->roles->pluck('name');
}
protected function getStoredRole($role): Role
{
- $roleClass = $this->getRoleClass();
+ if ($role instanceof \BackedEnum) {
+ $role = $role->value;
+ }
- if (is_numeric($role)) {
- return $roleClass->findById($role, $this->getDefaultGuardName());
+ if (is_int($role) || PermissionRegistrar::isUid($role)) {
+ return $this->getRoleClass()::findById($role, $this->getDefaultGuardName());
}
if (is_string($role)) {
- return $roleClass->findByName($role, $this->getDefaultGuardName());
+ return $this->getRoleClass()::findByName($role, $this->getDefaultGuardName());
}
return $role;
@@ -274,7 +427,7 @@ protected function convertPipeToArray(string $pipeString)
$pipeString = trim($pipeString);
if (strlen($pipeString) <= 2) {
- return $pipeString;
+ return [str_replace('|', '', $pipeString)];
}
$quoteCharacter = substr($pipeString, 0, 1);
diff --git a/src/WildcardPermission.php b/src/WildcardPermission.php
new file mode 100644
index 000000000..a4287187f
--- /dev/null
+++ b/src/WildcardPermission.php
@@ -0,0 +1,118 @@
+record = $record;
+ }
+
+ public function getIndex(): array
+ {
+ $index = [];
+
+ foreach ($this->record->getAllPermissions() as $permission) {
+ $index[$permission->guard_name] = $this->buildIndex(
+ $index[$permission->guard_name] ?? [],
+ explode(static::PART_DELIMITER, $permission->name),
+ $permission->name,
+ );
+ }
+
+ return $index;
+ }
+
+ protected function buildIndex(array $index, array $parts, string $permission): array
+ {
+ if (empty($parts)) {
+ $index[null] = true;
+
+ return $index;
+ }
+
+ $part = array_shift($parts);
+
+ if (blank($part)) {
+ throw WildcardPermissionNotProperlyFormatted::create($permission);
+ }
+
+ if (! Str::contains($part, static::SUBPART_DELIMITER)) {
+ $index[$part] = $this->buildIndex(
+ $index[$part] ?? [],
+ $parts,
+ $permission,
+ );
+ }
+
+ $subParts = explode(static::SUBPART_DELIMITER, $part);
+
+ foreach ($subParts as $subPart) {
+ if (blank($subPart)) {
+ throw WildcardPermissionNotProperlyFormatted::create($permission);
+ }
+
+ $index[$subPart] = $this->buildIndex(
+ $index[$subPart] ?? [],
+ $parts,
+ $permission,
+ );
+ }
+
+ return $index;
+ }
+
+ public function implies(string $permission, string $guardName, array $index): bool
+ {
+ if (! array_key_exists($guardName, $index)) {
+ return false;
+ }
+
+ $permission = explode(static::PART_DELIMITER, $permission);
+
+ return $this->checkIndex($permission, $index[$guardName]);
+ }
+
+ protected function checkIndex(array $permission, array $index): bool
+ {
+ if (array_key_exists(strval(null), $index)) {
+ return true;
+ }
+
+ if (empty($permission)) {
+ return false;
+ }
+
+ $firstPermission = array_shift($permission);
+
+ if (
+ array_key_exists($firstPermission, $index) &&
+ $this->checkIndex($permission, $index[$firstPermission])
+ ) {
+ return true;
+ }
+
+ if (array_key_exists(static::WILDCARD_TOKEN, $index)) {
+ return $this->checkIndex($permission, $index[static::WILDCARD_TOKEN]);
+ }
+
+ return false;
+ }
+}
diff --git a/src/helpers.php b/src/helpers.php
index bb6e8983c..e7d0e18a4 100644
--- a/src/helpers.php
+++ b/src/helpers.php
@@ -1,23 +1,29 @@
map(function ($guard) {
- if (! isset($guard['provider'])) {
- return;
- }
+if (! function_exists('getModelForGuard')) {
+ function getModelForGuard(string $guard): ?string
+ {
+ return Spatie\Permission\Guard::getModelForGuard($guard);
+ }
- return config("auth.providers.{$guard['provider']}.model");
- })->get($guard);
}
-function isNotLumen() : bool
-{
- return ! preg_match('/lumen/i', app()->version());
+if (! function_exists('setPermissionsTeamId')) {
+ /**
+ * @param int|string|null|\Illuminate\Database\Eloquent\Model $id
+ */
+ function setPermissionsTeamId($id)
+ {
+ app(\Spatie\Permission\PermissionRegistrar::class)->setPermissionsTeamId($id);
+ }
+}
+
+if (! function_exists('getPermissionsTeamId')) {
+ /**
+ * @return int|string|null
+ */
+ function getPermissionsTeamId()
+ {
+ return app(\Spatie\Permission\PermissionRegistrar::class)->getPermissionsTeamId();
+ }
}
diff --git a/tests/Admin.php b/tests/Admin.php
deleted file mode 100644
index b97ca2c45..000000000
--- a/tests/Admin.php
+++ /dev/null
@@ -1,26 +0,0 @@
-assertEquals('does not have permission', $this->renderView('can', ['permission' => $permission]));
+ $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission')));
$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)]));
}
/** @test */
- public function all_blade_directives_will_evaluate_falsy_when_somebody_without_roles_or_permissions_is_logged_in()
+ #[Test]
+ public function all_blade_directives_will_evaluate_false_when_somebody_without_roles_or_permissions_is_logged_in()
{
$permission = 'edit-articles';
$role = 'writer';
$roles = 'writer';
$elserole = 'na';
+ $elsepermission = 'na';
auth()->setUser($this->testUser);
- $this->assertEquals('does not have permission', $this->renderView('can', ['permission' => $permission]));
+ $this->assertEquals('does not have permission', $this->renderView('can', compact('permission')));
+ $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission')));
$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', compact('roles')));
@@ -55,16 +62,19 @@ public function all_blade_directives_will_evaluate_falsy_when_somebody_without_r
}
/** @test */
- public function all_blade_directives_will_evaluate_falsy_when_somebody_with_another_guard_is_logged_in()
+ #[Test]
+ public function all_blade_directives_will_evaluate_false_when_somebody_with_another_guard_is_logged_in()
{
$permission = 'edit-articles';
$role = 'writer';
$roles = 'writer';
$elserole = 'na';
+ $elsepermission = 'na';
auth('admin')->setUser($this->testAdmin);
$this->assertEquals('does not have permission', $this->renderView('can', compact('permission')));
+ $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission')));
$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', compact('roles')));
@@ -73,6 +83,38 @@ public function all_blade_directives_will_evaluate_falsy_when_somebody_with_anot
}
/** @test */
+ #[Test]
+ public function the_can_directive_can_accept_a_guard_name()
+ {
+ $user = $this->getWriter();
+ $user->givePermissionTo('edit-articles');
+ auth()->setUser($user);
+
+ $permission = 'edit-articles';
+ $guard = 'web';
+ $this->assertEquals('has permission', $this->renderView('can', compact('permission', 'guard')));
+ $guard = 'admin';
+ $this->assertEquals('does not have permission', $this->renderView('can', compact('permission', 'guard')));
+
+ auth()->logout();
+
+ // log in as the Admin with the permission-via-role
+ $this->testAdmin->givePermissionTo($this->testAdminPermission);
+ $user = $this->testAdmin;
+ auth()->setUser($user);
+
+ $permission = 'edit-articles';
+ $guard = 'web';
+ $this->assertEquals('does not have permission', $this->renderView('can', compact('permission', 'guard')));
+
+ $permission = 'admin-permission';
+ $guard = 'admin';
+ $this->assertTrue($this->testAdmin->checkPermissionTo($permission, $guard));
+ $this->assertEquals('has permission', $this->renderView('can', compact('permission', 'guard')));
+ }
+
+ /** @test */
+ #[Test]
public function the_can_directive_will_evaluate_true_when_the_logged_in_user_has_the_permission()
{
$user = $this->getWriter();
@@ -85,6 +127,32 @@ public function the_can_directive_will_evaluate_true_when_the_logged_in_user_has
}
/** @test */
+ #[Test]
+ public function the_haspermission_directive_will_evaluate_true_when_the_logged_in_user_has_the_permission()
+ {
+ $user = $this->getWriter();
+
+ $permission = 'edit-articles';
+ $user->givePermissionTo('edit-articles');
+
+ auth()->setUser($user);
+
+ $this->assertEquals('has permission', $this->renderView('haspermission', compact('permission')));
+
+ $guard = 'admin';
+ $elsepermission = 'na';
+ $this->assertEquals('does not have permission', $this->renderView('haspermission', compact('permission', 'elsepermission', 'guard')));
+
+ $this->testAdminRole->givePermissionTo($this->testAdminPermission);
+ $this->testAdmin->assignRole($this->testAdminRole);
+ auth('admin')->setUser($this->testAdmin);
+ $guard = 'admin';
+ $permission = 'admin-permission';
+ $this->assertEquals('has permission', $this->renderView('haspermission', compact('permission', 'guard', 'elsepermission')));
+ }
+
+ /** @test */
+ #[Test]
public function the_role_directive_will_evaluate_true_when_the_logged_in_user_has_the_role()
{
auth()->setUser($this->getWriter());
@@ -93,6 +161,7 @@ public function the_role_directive_will_evaluate_true_when_the_logged_in_user_ha
}
/** @test */
+ #[Test]
public function the_elserole_directive_will_evaluate_true_when_the_logged_in_user_has_the_role()
{
auth()->setUser($this->getMember());
@@ -101,6 +170,7 @@ public function the_elserole_directive_will_evaluate_true_when_the_logged_in_use
}
/** @test */
+ #[Test]
public function the_role_directive_will_evaluate_true_when_the_logged_in_user_has_the_role_for_the_given_guard()
{
auth('admin')->setUser($this->getSuperAdmin());
@@ -109,6 +179,7 @@ public function the_role_directive_will_evaluate_true_when_the_logged_in_user_ha
}
/** @test */
+ #[Test]
public function the_hasrole_directive_will_evaluate_true_when_the_logged_in_user_has_the_role()
{
auth()->setUser($this->getWriter());
@@ -117,6 +188,7 @@ public function the_hasrole_directive_will_evaluate_true_when_the_logged_in_user
}
/** @test */
+ #[Test]
public function the_hasrole_directive_will_evaluate_true_when_the_logged_in_user_has_the_role_for_the_given_guard()
{
auth('admin')->setUser($this->getSuperAdmin());
@@ -125,6 +197,7 @@ public function the_hasrole_directive_will_evaluate_true_when_the_logged_in_user
}
/** @test */
+ #[Test]
public function the_unlessrole_directive_will_evaluate_true_when_the_logged_in_user_does_not_have_the_role()
{
auth()->setUser($this->getWriter());
@@ -133,6 +206,7 @@ public function the_unlessrole_directive_will_evaluate_true_when_the_logged_in_u
}
/** @test */
+ #[Test]
public function the_unlessrole_directive_will_evaluate_true_when_the_logged_in_user_does_not_have_the_role_for_the_given_guard()
{
auth('admin')->setUser($this->getSuperAdmin());
@@ -142,6 +216,7 @@ public function the_unlessrole_directive_will_evaluate_true_when_the_logged_in_u
}
/** @test */
+ #[Test]
public function the_hasanyrole_directive_will_evaluate_false_when_the_logged_in_user_does_not_have_any_of_the_required_roles()
{
$roles = ['writer', 'intern'];
@@ -153,6 +228,7 @@ public function the_hasanyrole_directive_will_evaluate_false_when_the_logged_in_
}
/** @test */
+ #[Test]
public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_user_does_have_some_of_the_required_roles()
{
$roles = ['member', 'writer', 'intern'];
@@ -164,6 +240,7 @@ public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_u
}
/** @test */
+ #[Test]
public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_user_does_have_some_of_the_required_roles_for_the_given_guard()
{
$roles = ['super-admin', 'moderator'];
@@ -175,6 +252,7 @@ public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_u
}
/** @test */
+ #[Test]
public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_user_does_have_some_of_the_required_roles_in_pipe()
{
$guard = 'admin';
@@ -185,6 +263,7 @@ public function the_hasanyrole_directive_will_evaluate_true_when_the_logged_in_u
}
/** @test */
+ #[Test]
public function the_hasanyrole_directive_will_evaluate_false_when_the_logged_in_user_doesnt_have_some_of_the_required_roles_in_pipe()
{
$guard = '';
@@ -195,6 +274,7 @@ public function the_hasanyrole_directive_will_evaluate_false_when_the_logged_in_
}
/** @test */
+ #[Test]
public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in_user_does_not_have_all_required_roles()
{
$roles = ['member', 'writer'];
@@ -206,6 +286,7 @@ public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in
}
/** @test */
+ #[Test]
public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles()
{
$roles = ['member', 'writer'];
@@ -221,6 +302,7 @@ public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_
}
/** @test */
+ #[Test]
public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles_for_the_given_guard()
{
$roles = ['super-admin', 'moderator'];
@@ -236,6 +318,7 @@ public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_
}
/** @test */
+ #[Test]
public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_user_does_have_all_required_roles_in_pipe()
{
$guard = 'admin';
@@ -250,6 +333,7 @@ public function the_hasallroles_directive_will_evaluate_true_when_the_logged_in_
}
/** @test */
+ #[Test]
public function the_hasallroles_directive_will_evaluate_false_when_the_logged_in_user_doesnt_have_all_required_roles_in_pipe()
{
$guard = '';
@@ -262,6 +346,35 @@ 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 */
+ #[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 */
+ #[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 368673d0e..be5510fe9 100644
--- a/tests/CacheTest.php
+++ b/tests/CacheTest.php
@@ -1,25 +1,27 @@
cache_init_count = 1;
$this->cache_load_count = 1;
- $this->cache_reload_count = 1;
- $this->cache_untagged_count = -1;
- break;
- case $cacheStore instanceof \Illuminate\Cache\FileStore:
- $this->cache_untagged_count = -2;
- break;
- case $cacheStore instanceof \Illuminate\Cache\RedisStore:
- $this->cache_untagged_count = 0;
- break;
- case $cacheStore instanceof \Illuminate\Cache\MemcachedStore:
- $this->cache_untagged_count = 0;
- break;
- case $cacheStore instanceof \Illuminate\Cache\ArrayStore:
- $this->cache_untagged_count = 0;
+ // no break
default:
}
}
/** @test */
+ #[Test]
public function it_can_cache_the_permissions()
{
$this->resetQueryCount();
@@ -61,13 +51,10 @@ public function it_can_cache_the_permissions()
$this->registrar->getPermissions();
$this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count);
-
- $this->registrar->getPermissions();
-
- $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count + $this->cache_reload_count);
}
/** @test */
+ #[Test]
public function it_flushes_the_cache_when_creating_a_permission()
{
app(Permission::class)->create(['name' => 'new']);
@@ -80,6 +67,7 @@ public function it_flushes_the_cache_when_creating_a_permission()
}
/** @test */
+ #[Test]
public function it_flushes_the_cache_when_updating_a_permission()
{
$permission = app(Permission::class)->create(['name' => 'new']);
@@ -95,6 +83,7 @@ public function it_flushes_the_cache_when_updating_a_permission()
}
/** @test */
+ #[Test]
public function it_flushes_the_cache_when_creating_a_role()
{
app(Role::class)->create(['name' => 'new']);
@@ -107,6 +96,7 @@ public function it_flushes_the_cache_when_creating_a_role()
}
/** @test */
+ #[Test]
public function it_flushes_the_cache_when_updating_a_role()
{
$role = app(Role::class)->create(['name' => 'new']);
@@ -122,6 +112,71 @@ public function it_flushes_the_cache_when_updating_a_role()
}
/** @test */
+ #[Test]
+ 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 */
+ #[Test]
+ public function removing_a_role_from_a_user_should_not_flush_the_cache()
+ {
+ $this->testUser->assignRole('testRole');
+
+ $this->registrar->getPermissions();
+
+ $this->testUser->removeRole('testRole');
+
+ $this->resetQueryCount();
+
+ $this->registrar->getPermissions();
+
+ $this->assertQueryCount(0);
+ }
+
+ /** @test */
+ #[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 */
+ #[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);
+ }
+
+ /** @test */
+ #[Test]
public function user_creation_should_not_flush_the_cache()
{
$this->registrar->getPermissions();
@@ -132,10 +187,12 @@ public function user_creation_should_not_flush_the_cache()
$this->registrar->getPermissions();
- $this->assertQueryCount($this->cache_init_count);
+ // should all be in memory, so no init/load required
+ $this->assertQueryCount(0);
}
/** @test */
+ #[Test]
public function it_flushes_the_cache_when_giving_a_permission_to_a_role()
{
$this->testUserRole->givePermissionTo($this->testUserPermission);
@@ -148,42 +205,82 @@ public function it_flushes_the_cache_when_giving_a_permission_to_a_role()
}
/** @test */
+ #[Test]
public function has_permission_to_should_use_the_cache()
{
- $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']);
+ $this->testUserRole->givePermissionTo(['edit-articles', 'edit-news', 'Edit News']);
$this->testUser->assignRole('testRole');
+ $this->testUser->loadMissing('roles', 'permissions'); // load relations
$this->resetQueryCount();
$this->assertTrue($this->testUser->hasPermissionTo('edit-articles'));
- $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count + $this->cache_relations_count);
+ $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count);
$this->resetQueryCount();
$this->assertTrue($this->testUser->hasPermissionTo('edit-news'));
- $this->assertQueryCount($this->cache_run_count + $this->cache_untagged_count);
+ $this->assertQueryCount(0);
$this->resetQueryCount();
$this->assertTrue($this->testUser->hasPermissionTo('edit-articles'));
- $this->assertQueryCount($this->cache_init_count);
+ $this->assertQueryCount(0);
+
+ $this->resetQueryCount();
+ $this->assertTrue($this->testUser->hasPermissionTo('Edit News'));
+ $this->assertQueryCount(0);
}
/** @test */
+ #[Test]
+ public function the_cache_should_differentiate_by_guard_name()
+ {
+ $this->expectException(PermissionDoesNotExist::class);
+
+ $this->testUserRole->givePermissionTo(['edit-articles', 'web']);
+ $this->testUser->assignRole('testRole');
+ $this->testUser->loadMissing('roles', 'permissions'); // load relations
+
+ $this->resetQueryCount();
+ $this->assertTrue($this->testUser->hasPermissionTo('edit-articles', 'web'));
+ $this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count);
+
+ $this->resetQueryCount();
+ $this->assertFalse($this->testUser->hasPermissionTo('edit-articles', 'admin'));
+ $this->assertQueryCount(1); // 1 for first lookup of this permission with this guard
+ }
+
+ /** @test */
+ #[Test]
public function get_all_permissions_should_use_the_cache()
{
$this->testUserRole->givePermissionTo($expected = ['edit-articles', 'edit-news']);
$this->testUser->assignRole('testRole');
+ $this->testUser->loadMissing('roles.permissions', 'permissions'); // load relations
$this->resetQueryCount();
$this->registrar->getPermissions();
$this->assertQueryCount($this->cache_init_count + $this->cache_load_count + $this->cache_run_count);
$this->resetQueryCount();
- $actual = $this->testUser->getAllPermissions()->pluck('name');
+ $actual = $this->testUser->getAllPermissions()->pluck('name')->sort()->values();
$this->assertEquals($actual, collect($expected));
- $this->assertQueryCount(3);
+ $this->assertQueryCount(0);
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
public function it_can_reset_the_cache_with_artisan_command()
{
Artisan::call('permission:create-permission', ['name' => 'new-permission']);
diff --git a/tests/CommandTest.php b/tests/CommandTest.php
index 703de0760..e1961d812 100644
--- a/tests/CommandTest.php
+++ b/tests/CommandTest.php
@@ -1,14 +1,18 @@
'new-role']);
@@ -18,6 +22,7 @@ public function it_can_create_a_role()
}
/** @test */
+ #[Test]
public function it_can_create_a_role_with_a_specific_guard()
{
Artisan::call('permission:create-role', [
@@ -31,6 +36,7 @@ public function it_can_create_a_role_with_a_specific_guard()
}
/** @test */
+ #[Test]
public function it_can_create_a_permission()
{
Artisan::call('permission:create-permission', ['name' => 'new-permission']);
@@ -39,6 +45,7 @@ public function it_can_create_a_permission()
}
/** @test */
+ #[Test]
public function it_can_create_a_permission_with_a_specific_guard()
{
Artisan::call('permission:create-permission', [
@@ -52,6 +59,7 @@ public function it_can_create_a_permission_with_a_specific_guard()
}
/** @test */
+ #[Test]
public function it_can_create_a_role_and_permissions_at_same_time()
{
Artisan::call('permission:create-role', [
@@ -64,4 +72,180 @@ public function it_can_create_a_role_and_permissions_at_same_time()
$this->assertTrue($role->hasPermissionTo('first permission'));
$this->assertTrue($role->hasPermissionTo('second permission'));
}
+
+ /** @test */
+ #[Test]
+ public function it_can_create_a_role_without_duplication()
+ {
+ Artisan::call('permission:create-role', ['name' => 'new-role']);
+ Artisan::call('permission:create-role', ['name' => 'new-role']);
+
+ $this->assertCount(1, Role::where('name', 'new-role')->get());
+ $this->assertCount(0, Role::where('name', 'new-role')->first()->permissions);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_create_a_permission_without_duplication()
+ {
+ Artisan::call('permission:create-permission', ['name' => 'new-permission']);
+ Artisan::call('permission:create-permission', ['name' => 'new-permission']);
+
+ $this->assertCount(1, Permission::where('name', 'new-permission')->get());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_show_permission_tables()
+ {
+ Role::where('name', 'testRole2')->delete();
+ Role::create(['name' => 'testRole_2']);
+
+ Artisan::call('permission:show');
+
+ $output = Artisan::output();
+
+ $this->assertTrue(strpos($output, 'Guard: web') !== false);
+ $this->assertTrue(strpos($output, 'Guard: admin') !== false);
+
+ // | | testRole | testRole_2 |
+ // | edit-articles | · | · |
+ if (method_exists($this, 'assertMatchesRegularExpression')) {
+ $this->assertMatchesRegularExpression('/\|\s+\|\s+testRole\s+\|\s+testRole_2\s+\|/', $output);
+ $this->assertMatchesRegularExpression('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output);
+ } else { // phpUnit 9/8
+ $this->assertRegExp('/\|\s+\|\s+testRole\s+\|\s+testRole_2\s+\|/', $output);
+ $this->assertRegExp('/\|\s+edit-articles\s+\|\s+·\s+\|\s+·\s+\|/', $output);
+ }
+
+ Role::findByName('testRole')->givePermissionTo('edit-articles');
+ $this->reloadPermissions();
+
+ Artisan::call('permission:show');
+
+ $output = Artisan::output();
+
+ // | edit-articles | · | · |
+ 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 */
+ #[Test]
+ public function it_can_show_permissions_for_guard()
+ {
+ Artisan::call('permission:show', ['guard' => 'web']);
+
+ $output = Artisan::output();
+
+ $this->assertTrue(strpos($output, 'Guard: web') !== false);
+ $this->assertTrue(strpos($output, 'Guard: admin') === false);
+ }
+
+ /** @test */
+ #[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);
+
+ $AddTeamsFields = require $matchingFiles[count($matchingFiles) - 1];
+ $AddTeamsFields->up();
+ $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 */
+ #[Test]
+ public function it_can_show_roles_by_teams()
+ {
+ config()->set('permission.teams', true);
+ app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache();
+
+ Role::where('name', 'testRole2')->delete();
+ Role::create(['name' => 'testRole_2']);
+ Role::create(['name' => 'testRole_Team', 'team_test_id' => 1]);
+ Role::create(['name' => 'testRole_Team', '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 | testRole_2 | testRole_Team | testRole_Team |
+ 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+testRole_2\s+\|\s+testRole_Team\s+\|\s+testRole_Team\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+testRole_2\s+\|\s+testRole_Team\s+\|\s+testRole_Team\s+\|/', $output);
+ }
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_respond_to_about_command_with_default()
+ {
+ if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) {
+ $this->markTestSkipped();
+ }
+ if (! method_exists(AboutCommand::class, 'flushState')) {
+ $this->markTestSkipped();
+ }
+
+ app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache();
+
+ Artisan::call('about');
+ $output = str_replace("\r\n", "\n", Artisan::output());
+
+ $pattern = '/Spatie Permissions[ .\n]*Features Enabled[ .]*Default[ .\n]*Version/';
+ if (method_exists($this, 'assertMatchesRegularExpression')) {
+ $this->assertMatchesRegularExpression($pattern, $output);
+ } else { // phpUnit 9/8
+ $this->assertRegExp($pattern, $output);
+ }
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_respond_to_about_command_with_teams()
+ {
+ if (! class_exists(InstalledVersions::class) || ! class_exists(AboutCommand::class)) {
+ $this->markTestSkipped();
+ }
+ if (! method_exists(AboutCommand::class, 'flushState')) {
+ $this->markTestSkipped();
+ }
+
+ app(\Spatie\Permission\PermissionRegistrar::class)->initializeCache();
+
+ config()->set('permission.teams', true);
+
+ Artisan::call('about');
+ $output = str_replace("\r\n", "\n", Artisan::output());
+
+ $pattern = '/Spatie Permissions[ .\n]*Features Enabled[ .]*Teams[ .\n]*Version/';
+ if (method_exists($this, 'assertMatchesRegularExpression')) {
+ $this->assertMatchesRegularExpression($pattern, $output);
+ } else { // phpUnit 9/8
+ $this->assertRegExp($pattern, $output);
+ }
+ }
}
diff --git a/tests/CustomGateTest.php b/tests/CustomGateTest.php
new file mode 100644
index 000000000..498291ee8
--- /dev/null
+++ b/tests/CustomGateTest.php
@@ -0,0 +1,38 @@
+set('permission.register_permission_check_method', false);
+ }
+
+ /** @test */
+ #[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 */
+ #[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/GateTest.php b/tests/GateTest.php
index 154c15f1d..6ec0fdcb3 100644
--- a/tests/GateTest.php
+++ b/tests/GateTest.php
@@ -1,23 +1,29 @@
assertFalse($this->testUser->can('edit-articles'));
}
/** @test */
+ #[Test]
public function it_allows_other_gate_before_callbacks_to_run_if_a_user_does_not_have_a_permission()
{
$this->assertFalse($this->testUser->can('edit-articles'));
app(Gate::class)->before(function () {
+ // this Gate-before intercept overrides everything to true ... like a typical Super-Admin might use
return true;
});
@@ -25,6 +31,20 @@ public function it_allows_other_gate_before_callbacks_to_run_if_a_user_does_not_
}
/** @test */
+ #[Test]
+ public function it_allows_gate_after_callback_to_grant_denied_privileges()
+ {
+ $this->assertFalse($this->testUser->can('edit-articles'));
+
+ app(Gate::class)->after(function ($user, $ability, $result) {
+ return true;
+ });
+
+ $this->assertTrue($this->testUser->can('edit-articles'));
+ }
+
+ /** @test */
+ #[Test]
public function it_can_determine_if_a_user_has_a_direct_permission()
{
$this->testUser->givePermissionTo('edit-articles');
@@ -36,7 +56,32 @@ public function it_can_determine_if_a_user_has_a_direct_permission()
$this->assertFalse($this->testUser->can('admin-permission'));
}
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function it_can_determine_if_a_user_has_a_direct_permission_using_enums()
+ {
+ $enum = TestModels\TestRolePermissionsEnum::VIEWARTICLES;
+
+ $permission = app(Permission::class)->findOrCreate($enum->value, 'web');
+
+ $this->assertFalse($this->testUser->can($enum->value));
+ $this->assertFalse($this->testUser->canAny([$enum->value, 'some other permission']));
+
+ $this->testUser->givePermissionTo($enum);
+
+ $this->assertTrue($this->testUser->hasPermissionTo($enum));
+
+ $this->assertTrue($this->testUser->can($enum->value));
+ $this->assertTrue($this->testUser->canAny([$enum->value, 'some other permission']));
+ }
+
/** @test */
+ #[Test]
public function it_can_determine_if_a_user_has_a_permission_through_roles()
{
$this->testUserRole->givePermissionTo($this->testUserPermission);
@@ -53,6 +98,7 @@ public function it_can_determine_if_a_user_has_a_permission_through_roles()
}
/** @test */
+ #[Test]
public function it_can_determine_if_a_user_with_a_different_guard_has_a_permission_when_using_roles()
{
$this->testAdminRole->givePermissionTo($this->testAdminPermission);
diff --git a/tests/HasPermissionsTest.php b/tests/HasPermissionsTest.php
index 47bdf4591..95ad3a473 100644
--- a/tests/HasPermissionsTest.php
+++ b/tests/HasPermissionsTest.php
@@ -1,15 +1,25 @@
testUser->givePermissionTo($this->testUserPermission);
@@ -18,6 +28,21 @@ public function it_can_assign_a_permission_to_a_user()
}
/** @test */
+ #[Test]
+ public function it_can_assign_a_permission_to_a_user_with_a_non_default_guard()
+ {
+ $testUserPermission = app(Permission::class)->create([
+ 'name' => 'edit-articles',
+ 'guard_name' => 'api',
+ ]);
+
+ $this->testUser->givePermissionTo($testUserPermission);
+
+ $this->assertTrue($this->testUser->hasPermissionTo($testUserPermission));
+ }
+
+ /** @test */
+ #[Test]
public function it_throws_an_exception_when_assigning_a_permission_that_does_not_exist()
{
$this->expectException(PermissionDoesNotExist::class);
@@ -26,6 +51,7 @@ public function it_throws_an_exception_when_assigning_a_permission_that_does_not
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_assigning_a_permission_to_a_user_from_a_different_guard()
{
$this->expectException(GuardDoesNotMatch::class);
@@ -38,6 +64,7 @@ public function it_throws_an_exception_when_assigning_a_permission_to_a_user_fro
}
/** @test */
+ #[Test]
public function it_can_revoke_a_permission_from_a_user()
{
$this->testUser->givePermissionTo($this->testUserPermission);
@@ -49,107 +76,221 @@ public function it_can_revoke_a_permission_from_a_user()
$this->assertFalse($this->testUser->hasPermissionTo($this->testUserPermission));
}
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function it_can_assign_and_remove_a_permission_using_enums()
+ {
+ $enum = TestModels\TestRolePermissionsEnum::VIEWARTICLES;
+
+ $permission = app(Permission::class)->findOrCreate($enum->value, 'web');
+
+ $this->testUser->givePermissionTo($enum);
+
+ $this->assertTrue($this->testUser->hasPermissionTo($enum));
+ $this->assertTrue($this->testUser->hasAnyPermission($enum));
+ $this->assertTrue($this->testUser->hasDirectPermission($enum));
+
+ $this->testUser->revokePermissionTo($enum);
+
+ $this->assertFalse($this->testUser->hasPermissionTo($enum));
+ $this->assertFalse($this->testUser->hasAnyPermission($enum));
+ $this->assertFalse($this->testUser->hasDirectPermission($enum));
+ }
+
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function it_can_scope_users_using_enums()
+ {
+ $enum1 = TestModels\TestRolePermissionsEnum::VIEWARTICLES;
+ $enum2 = TestModels\TestRolePermissionsEnum::EDITARTICLES;
+ $permission1 = app(Permission::class)->findOrCreate($enum1->value, 'web');
+ $permission2 = app(Permission::class)->findOrCreate($enum2->value, 'web');
+
+ User::all()->each(fn ($item) => $item->delete());
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
+ $user1->givePermissionTo([$enum1, $enum2]);
+ $this->testUserRole->givePermissionTo($enum2);
+ $user2->assignRole('testRole');
+
+ $scopedUsers1 = User::permission($enum2)->get();
+ $scopedUsers2 = User::permission([$enum1])->get();
+ $scopedUsers3 = User::withoutPermission([$enum1])->get();
+ $scopedUsers4 = User::withoutPermission([$enum2])->get();
+
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ $this->assertEquals(2, $scopedUsers3->count());
+ $this->assertEquals(1, $scopedUsers4->count());
+ }
+
/** @test */
+ #[Test]
public function it_can_scope_users_using_a_string()
{
+ User::all()->each(fn ($item) => $item->delete());
$user1 = User::create(['email' => 'user1@test.com']);
$user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
$user1->givePermissionTo(['edit-articles', 'edit-news']);
$this->testUserRole->givePermissionTo('edit-articles');
$user2->assignRole('testRole');
$scopedUsers1 = User::permission('edit-articles')->get();
$scopedUsers2 = User::permission(['edit-news'])->get();
+ $scopedUsers3 = User::withoutPermission('edit-news')->get();
- $this->assertEquals($scopedUsers1->count(), 2);
- $this->assertEquals($scopedUsers2->count(), 1);
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ $this->assertEquals(2, $scopedUsers3->count());
}
/** @test */
+ #[Test]
+ public function it_can_scope_users_using_a_int()
+ {
+ User::all()->each(fn ($item) => $item->delete());
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
+ $user1->givePermissionTo([1, 2]);
+ $this->testUserRole->givePermissionTo(1);
+ $user2->assignRole('testRole');
+
+ $scopedUsers1 = User::permission(1)->get();
+ $scopedUsers2 = User::permission([2])->get();
+ $scopedUsers3 = User::withoutPermission([2])->get();
+
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ $this->assertEquals(2, $scopedUsers3->count());
+ }
+
+ /** @test */
+ #[Test]
public function it_can_scope_users_using_an_array()
{
+ User::all()->each(fn ($item) => $item->delete());
$user1 = User::create(['email' => 'user1@test.com']);
$user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
$user1->givePermissionTo(['edit-articles', 'edit-news']);
$this->testUserRole->givePermissionTo('edit-articles');
$user2->assignRole('testRole');
+ $user3->assignRole('testRole2');
$scopedUsers1 = User::permission(['edit-articles', 'edit-news'])->get();
$scopedUsers2 = User::permission(['edit-news'])->get();
+ $scopedUsers3 = User::withoutPermission(['edit-news'])->get();
- $this->assertEquals($scopedUsers1->count(), 2);
- $this->assertEquals($scopedUsers2->count(), 1);
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ $this->assertEquals(2, $scopedUsers3->count());
}
/** @test */
+ #[Test]
public function it_can_scope_users_using_a_collection()
{
+ User::all()->each(fn ($item) => $item->delete());
$user1 = User::create(['email' => 'user1@test.com']);
$user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
$user1->givePermissionTo(['edit-articles', 'edit-news']);
$this->testUserRole->givePermissionTo('edit-articles');
$user2->assignRole('testRole');
+ $user3->assignRole('testRole2');
$scopedUsers1 = User::permission(collect(['edit-articles', 'edit-news']))->get();
$scopedUsers2 = User::permission(collect(['edit-news']))->get();
+ $scopedUsers3 = User::withoutPermission(collect(['edit-news']))->get();
- $this->assertEquals($scopedUsers1->count(), 2);
- $this->assertEquals($scopedUsers2->count(), 1);
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ $this->assertEquals(2, $scopedUsers3->count());
}
/** @test */
+ #[Test]
public function it_can_scope_users_using_an_object()
{
+ User::all()->each(fn ($item) => $item->delete());
$user1 = User::create(['email' => 'user1@test.com']);
$user1->givePermissionTo($this->testUserPermission->name);
$scopedUsers1 = User::permission($this->testUserPermission)->get();
$scopedUsers2 = User::permission([$this->testUserPermission])->get();
$scopedUsers3 = User::permission(collect([$this->testUserPermission]))->get();
+ $scopedUsers4 = User::withoutPermission(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());
+ $this->assertEquals(0, $scopedUsers4->count());
}
/** @test */
- public function it_can_scope_users_without_permissions_only_role()
+ #[Test]
+ public function it_can_scope_users_without_direct_permissions_only_role()
{
+ User::all()->each(fn ($item) => $item->delete());
$user1 = User::create(['email' => 'user1@test.com']);
$user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
$this->testUserRole->givePermissionTo('edit-articles');
$user1->assignRole('testRole');
$user2->assignRole('testRole');
+ $user3->assignRole('testRole2');
- $scopedUsers = User::permission('edit-articles')->get();
+ $scopedUsers1 = User::permission('edit-articles')->get();
+ $scopedUsers2 = User::withoutPermission('edit-articles')->get();
- $this->assertEquals($scopedUsers->count(), 2);
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
}
/** @test */
- public function it_can_scope_users_without_permissions_only_permission()
+ #[Test]
+ public function it_can_scope_users_with_only_direct_permission()
{
+ User::all()->each(fn ($item) => $item->delete());
$user1 = User::create(['email' => 'user1@test.com']);
$user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
$user1->givePermissionTo(['edit-news']);
$user2->givePermissionTo(['edit-articles', 'edit-news']);
- $scopedUsers = User::permission('edit-news')->get();
+ $scopedUsers1 = User::permission('edit-news')->get();
+ $scopedUsers2 = User::withoutPermission('edit-news')->get();
- $this->assertEquals($scopedUsers->count(), 2);
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_calling_hasPermissionTo_with_an_invalid_type()
{
$user = User::create(['email' => 'user1@test.com']);
$this->expectException(PermissionDoesNotExist::class);
- $user->hasPermissionTo(new \stdClass());
+ $user->hasPermissionTo(new \stdClass);
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_calling_hasPermissionTo_with_null()
{
$user = User::create(['email' => 'user1@test.com']);
@@ -160,35 +301,64 @@ public function it_throws_an_exception_when_calling_hasPermissionTo_with_null()
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_calling_hasDirectPermission_with_an_invalid_type()
{
$user = User::create(['email' => 'user1@test.com']);
- $this->assertFalse($user->hasDirectPermission(new \stdClass()));
+ $this->expectException(PermissionDoesNotExist::class);
+
+ $user->hasDirectPermission(new \stdClass);
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_calling_hasDirectPermission_with_null()
{
$user = User::create(['email' => 'user1@test.com']);
- $this->assertFalse($user->hasDirectPermission(null));
+ $this->expectException(PermissionDoesNotExist::class);
+
+ $user->hasDirectPermission(null);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_throws_an_exception_when_trying_to_scope_a_non_existing_permission()
+ {
+ $this->expectException(PermissionDoesNotExist::class);
+
+ User::permission('not defined permission')->get();
+
+ $this->expectException(PermissionDoesNotExist::class);
+
+ User::withoutPermission('not defined permission')->get();
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_trying_to_scope_a_permission_from_another_guard()
{
$this->expectException(PermissionDoesNotExist::class);
User::permission('testAdminPermission')->get();
+ $this->expectException(PermissionDoesNotExist::class);
+
+ User::withoutPermission('testAdminPermission')->get();
+
$this->expectException(GuardDoesNotMatch::class);
User::permission($this->testAdminPermission)->get();
+
+ $this->expectException(GuardDoesNotMatch::class);
+
+ User::withoutPermission($this->testAdminPermission)->get();
}
/** @test */
- public function it_doesnt_detach_permissions_when_soft_deleting()
+ #[Test]
+ public function it_doesnt_detach_permissions_when_user_soft_deleting()
{
$user = SoftDeletingUser::create(['email' => 'test@example.com']);
$user->givePermissionTo(['edit-news']);
@@ -200,6 +370,7 @@ public function it_doesnt_detach_permissions_when_soft_deleting()
}
/** @test */
+ #[Test]
public function it_can_give_and_revoke_multiple_permissions()
{
$this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']);
@@ -212,12 +383,44 @@ public function it_can_give_and_revoke_multiple_permissions()
}
/** @test */
+ #[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 */
+ #[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 */
+ #[Test]
public function it_can_determine_that_the_user_does_not_have_a_permission()
{
$this->assertFalse($this->testUser->hasPermissionTo('edit-articles'));
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_the_permission_does_not_exist()
{
$this->expectException(PermissionDoesNotExist::class);
@@ -226,22 +429,25 @@ public function it_throws_an_exception_when_the_permission_does_not_exist()
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_the_permission_does_not_exist_for_this_guard()
{
$this->expectException(PermissionDoesNotExist::class);
- $this->testUser->hasPermissionTo('admin-permission');
+ $this->testUser->hasPermissionTo('does-not-exist', 'web');
}
/** @test */
- public function it_can_work_with_a_user_that_does_not_have_any_permissions_at_all()
+ #[Test]
+ public function it_can_reject_a_user_that_does_not_have_any_permissions_at_all()
{
- $user = new User();
+ $user = new User;
$this->assertFalse($user->hasPermissionTo('edit-articles'));
}
/** @test */
+ #[Test]
public function it_can_determine_that_the_user_has_any_of_the_permissions_directly()
{
$this->assertFalse($this->testUser->hasAnyPermission('edit-articles'));
@@ -255,9 +461,11 @@ public function it_can_determine_that_the_user_has_any_of_the_permissions_direct
$this->testUser->revokePermissionTo($this->testUserPermission);
$this->assertTrue($this->testUser->hasAnyPermission('edit-articles', 'edit-news'));
+ $this->assertFalse($this->testUser->hasAnyPermission('edit-blog', 'Edit News', ['Edit News']));
}
/** @test */
+ #[Test]
public function it_can_determine_that_the_user_has_any_of_the_permissions_directly_using_an_array()
{
$this->assertFalse($this->testUser->hasAnyPermission(['edit-articles']));
@@ -274,6 +482,7 @@ public function it_can_determine_that_the_user_has_any_of_the_permissions_direct
}
/** @test */
+ #[Test]
public function it_can_determine_that_the_user_has_any_of_the_permissions_via_role()
{
$this->testUserRole->givePermissionTo('edit-articles');
@@ -281,9 +490,11 @@ public function it_can_determine_that_the_user_has_any_of_the_permissions_via_ro
$this->testUser->assignRole('testRole');
$this->assertTrue($this->testUser->hasAnyPermission('edit-news', 'edit-articles'));
+ $this->assertFalse($this->testUser->hasAnyPermission('edit-blog', 'Edit News', ['Edit News']));
}
/** @test */
+ #[Test]
public function it_can_determine_that_the_user_has_all_of_the_permissions_directly()
{
$this->testUser->givePermissionTo('edit-articles', 'edit-news');
@@ -293,9 +504,11 @@ public function it_can_determine_that_the_user_has_all_of_the_permissions_direct
$this->testUser->revokePermissionTo('edit-articles');
$this->assertFalse($this->testUser->hasAllPermissions('edit-articles', 'edit-news'));
+ $this->assertFalse($this->testUser->hasAllPermissions(['edit-articles', 'edit-news'], 'edit-blog'));
}
/** @test */
+ #[Test]
public function it_can_determine_that_the_user_has_all_of_the_permissions_directly_using_an_array()
{
$this->assertFalse($this->testUser->hasAllPermissions(['edit-articles', 'edit-news']));
@@ -312,6 +525,7 @@ public function it_can_determine_that_the_user_has_all_of_the_permissions_direct
}
/** @test */
+ #[Test]
public function it_can_determine_that_the_user_has_all_of_the_permissions_via_role()
{
$this->testUserRole->givePermissionTo('edit-articles', 'edit-news');
@@ -322,6 +536,7 @@ public function it_can_determine_that_the_user_has_all_of_the_permissions_via_ro
}
/** @test */
+ #[Test]
public function it_can_determine_that_user_has_direct_permission()
{
$this->testUser->givePermissionTo('edit-articles');
@@ -340,6 +555,7 @@ public function it_can_determine_that_user_has_direct_permission()
}
/** @test */
+ #[Test]
public function it_can_list_all_the_permissions_via_roles_of_user()
{
$roleModel = app(Role::class);
@@ -350,11 +566,12 @@ 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()
);
}
/** @test */
+ #[Test]
public function it_can_list_all_the_coupled_permissions_both_directly_and_via_roles()
{
$this->testUser->givePermissionTo('edit-news');
@@ -364,11 +581,12 @@ public function it_can_list_all_the_coupled_permissions_both_directly_and_via_ro
$this->assertEquals(
collect(['edit-articles', 'edit-news']),
- $this->testUser->getAllPermissions()->pluck('name')
+ $this->testUser->getAllPermissions()->pluck('name')->sort()->values()
);
}
/** @test */
+ #[Test]
public function it_can_sync_multiple_permissions()
{
$this->testUser->givePermissionTo('edit-news');
@@ -383,11 +601,40 @@ public function it_can_sync_multiple_permissions()
}
/** @test */
+ #[Test]
+ public function it_can_avoid_sync_duplicated_permissions()
+ {
+ $this->testUser->syncPermissions('edit-articles', 'edit-blog', 'edit-blog');
+
+ $this->assertTrue($this->testUser->hasDirectPermission('edit-articles'));
+
+ $this->assertTrue($this->testUser->hasDirectPermission('edit-blog'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_avoid_detach_on_permission_that_does_not_exist_sync()
+ {
+ $this->testUser->syncPermissions('edit-articles');
+
+ try {
+ $this->testUser->syncPermissions('permission-does-not-exist');
+ $this->fail('Expected PermissionDoesNotExist exception was not thrown.');
+ } catch (PermissionDoesNotExist $e) {
+ //
+ }
+
+ $this->assertTrue($this->testUser->hasDirectPermission('edit-articles'));
+ $this->assertFalse($this->testUser->checkPermissionTo('permission-does-not-exist'));
+ }
+
+ /** @test */
+ #[Test]
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);
@@ -399,11 +646,12 @@ public function it_can_sync_multiple_permissions_by_id()
}
/** @test */
+ #[Test]
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);
@@ -417,6 +665,20 @@ public function sync_permission_ignores_null_inputs()
}
/** @test */
+ #[Test]
+ public function sync_permission_error_does_not_detach_permissions()
+ {
+ $this->testUser->givePermissionTo('edit-news');
+
+ $this->expectException(PermissionDoesNotExist::class);
+
+ $this->testUser->syncPermissions('edit-articles', 'permission-that-does-not-exist');
+
+ $this->assertTrue($this->testUser->fresh()->hasDirectPermission('edit-news'));
+ }
+
+ /** @test */
+ #[Test]
public function it_does_not_remove_already_associated_permissions_when_assigning_new_permissions()
{
$this->testUser->givePermissionTo('edit-news');
@@ -427,6 +689,7 @@ public function it_does_not_remove_already_associated_permissions_when_assigning
}
/** @test */
+ #[Test]
public function it_does_not_throw_an_exception_when_assigning_a_permission_that_is_already_assigned()
{
$this->testUser->givePermissionTo('edit-news');
@@ -437,11 +700,13 @@ public function it_does_not_throw_an_exception_when_assigning_a_permission_that_
}
/** @test */
+ #[Test]
public function it_can_sync_permissions_to_a_model_that_is_not_persisted()
{
$user = new User(['email' => 'test@user.com']);
$user->syncPermissions('edit-articles');
$user->save();
+ $user->save(); // test save same model twice
$this->assertTrue($user->hasPermissionTo('edit-articles'));
@@ -451,6 +716,22 @@ public function it_can_sync_permissions_to_a_model_that_is_not_persisted()
}
/** @test */
+ #[Test]
+ public function it_does_not_run_unnecessary_sqls_when_assigning_new_permissions()
+ {
+ $permission2 = app(Permission::class)->where('name', ['edit-news'])->first();
+
+ DB::enableQueryLog();
+ $this->testUser->syncPermissions($this->testUserPermission, $permission2);
+ DB::disableQueryLog();
+
+ $necessaryQueriesCount = 2;
+
+ $this->assertCount($necessaryQueriesCount, DB::getQueryLog());
+ }
+
+ /** @test */
+ #[Test]
public function calling_givePermissionTo_before_saving_object_doesnt_interfere_with_other_objects()
{
$user = new User(['email' => 'test@user.com']);
@@ -459,13 +740,21 @@ 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(2, count(DB::getQueryLog())); // avoid unnecessary sync
}
/** @test */
+ #[Test]
public function calling_syncPermissions_before_saving_object_doesnt_interfere_with_other_objects()
{
$user = new User(['email' => 'test@user.com']);
@@ -474,9 +763,179 @@ 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(2, count(DB::getQueryLog())); // avoid unnecessary sync
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_retrieve_permission_names()
+ {
+ $this->testUser->givePermissionTo('edit-news', 'edit-articles');
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-news']),
+ $this->testUser->getPermissionNames()->sort()->values()
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_many_direct_permissions()
+ {
+ $this->testUser->givePermissionTo(['edit-articles', 'edit-news']);
+ $this->assertTrue($this->testUser->hasAllDirectPermissions(['edit-news', 'edit-articles']));
+ $this->assertTrue($this->testUser->hasAllDirectPermissions('edit-news', 'edit-articles'));
+ $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news', 'edit-blog']));
+ $this->assertFalse($this->testUser->hasAllDirectPermissions(['edit-articles', 'edit-news'], 'edit-blog'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_if_there_is_any_of_the_direct_permissions_given()
+ {
+ $this->testUser->givePermissionTo(['edit-articles', 'edit-news']);
+ $this->assertTrue($this->testUser->hasAnyDirectPermission(['edit-news', 'edit-blog']));
+ $this->assertTrue($this->testUser->hasAnyDirectPermission('edit-news', 'edit-blog'));
+ $this->assertFalse($this->testUser->hasAnyDirectPermission('edit-blog', 'Edit News', ['Edit News']));
+ }
+
+ /** @test */
+ #[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 */
+ #[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,
+ ]);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_fires_an_event_when_a_permission_is_added()
+ {
+ Event::fake();
+ app('config')->set('permission.events_enabled', true);
+
+ $this->testUser->givePermissionTo(['edit-articles', 'edit-news']);
+
+ $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])
+ ->pluck($this->testUserPermission->getKeyName())
+ ->toArray();
+
+ Event::assertDispatched(PermissionAttached::class, function ($event) use ($ids) {
+ return $event->model instanceof User
+ && $event->model->hasPermissionTo('edit-news')
+ && $event->model->hasPermissionTo('edit-articles')
+ && $ids === $event->permissionsOrIds;
+ });
+ }
+
+ /** @test */
+ #[Test]
+ public function it_does_not_fire_an_event_when_events_are_not_enabled()
+ {
+ Event::fake();
+ app('config')->set('permission.events_enabled', false);
+
+ $this->testUser->givePermissionTo(['edit-articles', 'edit-news']);
+
+ $ids = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])
+ ->pluck($this->testUserPermission->getKeyName())
+ ->toArray();
+
+ Event::assertNotDispatched(PermissionAttached::class);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_fires_an_event_when_a_permission_is_removed()
+ {
+ Event::fake();
+ app('config')->set('permission.events_enabled', true);
+
+ $permissions = app(Permission::class)::whereIn('name', ['edit-articles', 'edit-news'])->get();
+
+ $this->testUser->givePermissionTo($permissions);
+
+ $this->testUser->revokePermissionTo($permissions);
+
+ Event::assertDispatched(PermissionDetached::class, function ($event) use ($permissions) {
+ return $event->model instanceof User
+ && ! $event->model->hasPermissionTo('edit-news')
+ && ! $event->model->hasPermissionTo('edit-articles')
+ && $event->permissionsOrIds === $permissions;
+ });
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_be_given_a_permission_on_role_when_lazy_loading_is_restricted()
+ {
+ $this->assertTrue(Model::preventsLazyLoading());
+
+ try {
+ $testRole = app(Role::class)->with('permissions')->get()->first();
+
+ $testRole->givePermissionTo('edit-articles');
+
+ $this->assertTrue($testRole->hasPermissionTo('edit-articles'));
+ } catch (Exception $e) {
+ $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage());
+ }
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_be_given_a_permission_on_user_when_lazy_loading_is_restricted()
+ {
+ $this->assertTrue(Model::preventsLazyLoading());
+
+ try {
+ User::create(['email' => 'other@user.com']);
+ $testUser = User::with('permissions')->get()->first();
+
+ $testUser->givePermissionTo('edit-articles');
+
+ $this->assertTrue($testUser->hasPermissionTo('edit-articles'));
+ } catch (Exception $e) {
+ $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage());
+ }
}
}
diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php
new file mode 100644
index 000000000..acbd53fcc
--- /dev/null
+++ b/tests/HasPermissionsWithCustomModelsTest.php
@@ -0,0 +1,162 @@
+get('cache.default') == 'database') {
+ $this->resetDatabaseQuery = 1;
+ }
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_use_custom_model_permission()
+ {
+ $this->assertSame(get_class($this->testUserPermission), Permission::class);
+ }
+
+ /** @test */
+ #[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()));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_scope_users_using_a_int()
+ {
+ // Skipped because custom model uses uuid,
+ // replacement "it_can_scope_users_using_a_uuid"
+ $this->assertTrue(true);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_scope_users_using_a_uuid()
+ {
+ $uuid1 = $this->testUserPermission->getKey();
+ $uuid2 = app(Permission::class)::where('name', 'edit-news')->first()->getKey();
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user1->givePermissionTo([$uuid1, $uuid2]);
+ $this->testUserRole->givePermissionTo($uuid1);
+ $user2->assignRole('testRole');
+
+ $scopedUsers1 = User::permission($uuid1)->get();
+ $scopedUsers2 = User::permission([$uuid2])->get();
+
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_doesnt_detach_roles_when_soft_deleting()
+ {
+ $this->testUserRole->givePermissionTo($this->testUserPermission);
+
+ DB::enableQueryLog();
+ $this->testUserPermission->delete();
+ DB::disableQueryLog();
+
+ $this->assertSame(1 + $this->resetDatabaseQuery, count(DB::getQueryLog()));
+
+ $permission = Permission::onlyTrashed()->find($this->testUserPermission->getKey());
+
+ $this->assertEquals(1, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $permission->getKey())->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_doesnt_detach_users_when_soft_deleting()
+ {
+ $this->testUser->givePermissionTo($this->testUserPermission);
+
+ DB::enableQueryLog();
+ $this->testUserPermission->delete();
+ DB::disableQueryLog();
+
+ $this->assertSame(1 + $this->resetDatabaseQuery, count(DB::getQueryLog()));
+
+ $permission = Permission::onlyTrashed()->find($this->testUserPermission->getKey());
+
+ $this->assertEquals(1, DB::table(config('permission.table_names.model_has_permissions'))->where('permission_test_id', $permission->getKey())->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_does_detach_roles_and_users_when_force_deleting()
+ {
+ $permission_id = $this->testUserPermission->getKey();
+ $this->testUserRole->givePermissionTo($permission_id);
+ $this->testUser->givePermissionTo($permission_id);
+
+ DB::enableQueryLog();
+ $this->testUserPermission->forceDelete();
+ DB::disableQueryLog();
+
+ $this->assertSame(3 + $this->resetDatabaseQuery, count(DB::getQueryLog())); // avoid detach permissions on permissions
+
+ $permission = Permission::withTrashed()->find($permission_id);
+
+ $this->assertNull($permission);
+ $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('permission_test_id', $permission_id)->count());
+ $this->assertEquals(0, DB::table(config('permission.table_names.model_has_permissions'))->where('permission_test_id', $permission_id)->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_should_touch_when_assigning_new_permissions()
+ {
+ Carbon::setTestNow('2021-07-19 10:13:14');
+
+ $user = Admin::create(['email' => 'user1@test.com']);
+ $permission1 = Permission::create(['name' => 'edit-news', 'guard_name' => 'admin']);
+ $permission2 = Permission::create(['name' => 'edit-blog', 'guard_name' => 'admin']);
+
+ $this->assertSame('2021-07-19 10:13:14', $permission1->updated_at->format('Y-m-d H:i:s'));
+
+ Carbon::setTestNow('2021-07-20 19:13:14');
+
+ $user->syncPermissions([$permission1->getKey(), $permission2->getKey()]);
+
+ $this->assertSame('2021-07-20 19:13:14', $permission1->refresh()->updated_at->format('Y-m-d H:i:s'));
+ $this->assertSame('2021-07-20 19:13:14', $permission2->refresh()->updated_at->format('Y-m-d H:i:s'));
+ }
+}
diff --git a/tests/HasRolesTest.php b/tests/HasRolesTest.php
index 58cce0283..cf49e3dd6 100644
--- a/tests/HasRolesTest.php
+++ b/tests/HasRolesTest.php
@@ -1,20 +1,141 @@
assertFalse($this->testUser->hasRole('testRole'));
+
+ $role = app(Role::class)->findOrCreate('testRoleInWebGuard', 'web');
+
+ $this->assertFalse($this->testUser->hasRole($role));
+
+ $this->testUser->assignRole($role);
+ $this->assertTrue($this->testUser->hasRole($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->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->getKey(), 'fakeGuard'));
+ $this->assertFalse($this->testUser->hasRole([$role->getKey(), 'fakeRole'], 'fakeGuard'));
+
+ $role = app(Role::class)->findOrCreate('testRoleInWebGuard2', 'web');
+ $this->assertFalse($this->testUser->hasRole($role));
+ }
+
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function it_can_assign_and_remove_a_role_using_enums()
+ {
+ $enum1 = TestModels\TestRolePermissionsEnum::USERMANAGER;
+ $enum2 = TestModels\TestRolePermissionsEnum::WRITER;
+ $enum3 = TestModels\TestRolePermissionsEnum::CASTED_ENUM_1;
+ $enum4 = TestModels\TestRolePermissionsEnum::CASTED_ENUM_2;
+
+ app(Role::class)->findOrCreate($enum1->value, 'web');
+ app(Role::class)->findOrCreate($enum2->value, 'web');
+ app(Role::class)->findOrCreate($enum3->value, 'web');
+ app(Role::class)->findOrCreate($enum4->value, 'web');
+
+ $this->assertFalse($this->testUser->hasRole($enum1));
+ $this->assertFalse($this->testUser->hasRole($enum2));
+ $this->assertFalse($this->testUser->hasRole($enum3));
+ $this->assertFalse($this->testUser->hasRole($enum4));
+ $this->assertFalse($this->testUser->hasRole('user-manager'));
+ $this->assertFalse($this->testUser->hasRole('writer'));
+ $this->assertFalse($this->testUser->hasRole('casted_enum-1'));
+ $this->assertFalse($this->testUser->hasRole('casted_enum-2'));
+
+ $this->testUser->assignRole($enum1);
+ $this->testUser->assignRole($enum2);
+ $this->testUser->assignRole($enum3);
+ $this->testUser->assignRole($enum4);
+
+ $this->assertTrue($this->testUser->hasRole($enum1));
+ $this->assertTrue($this->testUser->hasRole($enum2));
+ $this->assertTrue($this->testUser->hasRole($enum3));
+ $this->assertTrue($this->testUser->hasRole($enum4));
+
+ $this->assertTrue($this->testUser->hasRole([$enum1, 'writer']));
+ $this->assertTrue($this->testUser->hasRole([$enum3, 'casted_enum-2']));
+
+ $this->assertTrue($this->testUser->hasAllRoles([$enum1, $enum2, $enum3, $enum4]));
+ $this->assertTrue($this->testUser->hasAllRoles(['user-manager', 'writer', 'casted_enum-1', 'casted_enum-2']));
+ $this->assertFalse($this->testUser->hasAllRoles([$enum1, $enum2, $enum3, $enum4, 'not exist']));
+ $this->assertFalse($this->testUser->hasAllRoles(['user-manager', 'writer', 'casted_enum-1', 'casted_enum-2', 'not exist']));
+
+ $this->assertTrue($this->testUser->hasExactRoles([$enum4, $enum3, $enum2, $enum1]));
+ $this->assertTrue($this->testUser->hasExactRoles(['user-manager', 'writer', 'casted_enum-1', 'casted_enum-2']));
+
+ $this->testUser->removeRole($enum1);
+
+ $this->assertFalse($this->testUser->hasRole($enum1));
+ }
+
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function it_can_scope_a_role_using_enums()
+ {
+ $enum1 = TestModels\TestRolePermissionsEnum::USERMANAGER;
+ $enum2 = TestModels\TestRolePermissionsEnum::WRITER;
+ $role1 = app(Role::class)->findOrCreate($enum1->value, 'web');
+ $role2 = app(Role::class)->findOrCreate($enum2->value, 'web');
+
+ User::all()->each(fn ($item) => $item->delete());
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
+
+ // assign only one user to a role
+ $user2->assignRole($enum1);
+ $this->assertTrue($user2->hasRole($enum1));
+ $this->assertFalse($user2->hasRole($enum2));
+
+ $scopedUsers1 = User::role($enum1)->get();
+ $scopedUsers2 = User::role($enum2)->get();
+ $scopedUsers3 = User::withoutRole($enum2)->get();
+
+ $this->assertEquals(1, $scopedUsers1->count());
+ $this->assertEquals(0, $scopedUsers2->count());
+ $this->assertEquals(3, $scopedUsers3->count());
}
/** @test */
+ #[Test]
public function it_can_assign_and_remove_a_role()
{
$this->assertFalse($this->testUser->hasRole('testRole'));
@@ -29,6 +150,24 @@ public function it_can_assign_and_remove_a_role()
}
/** @test */
+ #[Test]
+ public function it_removes_a_role_and_returns_roles()
+ {
+ $this->testUser->assignRole('testRole');
+
+ $this->testUser->assignRole('testRole2');
+
+ $this->assertTrue($this->testUser->hasRole(['testRole', 'testRole2']));
+
+ $roles = $this->testUser->removeRole('testRole');
+
+ $this->assertFalse($roles->hasRole('testRole'));
+
+ $this->assertTrue($roles->hasRole('testRole2'));
+ }
+
+ /** @test */
+ #[Test]
public function it_can_assign_and_remove_a_role_on_a_permission()
{
$this->testUserPermission->assignRole('testRole');
@@ -41,45 +180,70 @@ public function it_can_assign_and_remove_a_role_on_a_permission()
}
/** @test */
- public function it_can_assign_a_role_using_an_object()
+ #[Test]
+ public function it_can_assign_and_remove_a_role_using_an_object()
{
$this->testUser->assignRole($this->testUserRole);
$this->assertTrue($this->testUser->hasRole($this->testUserRole));
+
+ $this->testUser->removeRole($this->testUserRole);
+
+ $this->assertFalse($this->testUser->hasRole($this->testUserRole));
}
/** @test */
- public function it_can_assign_a_role_using_an_id()
+ #[Test]
+ public function it_can_assign_and_remove_a_role_using_an_id()
{
- $this->testUser->assignRole($this->testUserRole->id);
+ $this->testUser->assignRole($this->testUserRole->getKey());
$this->assertTrue($this->testUser->hasRole($this->testUserRole));
+
+ $this->testUser->removeRole($this->testUserRole->getKey());
+
+ $this->assertFalse($this->testUser->hasRole($this->testUserRole));
}
/** @test */
- public function it_can_assign_multiple_roles_at_once()
+ #[Test]
+ public function it_can_assign_and_remove_multiple_roles_at_once()
{
- $this->testUser->assignRole($this->testUserRole->id, 'testRole2');
+ $this->testUser->assignRole($this->testUserRole->getKey(), 'testRole2');
$this->assertTrue($this->testUser->hasRole('testRole'));
$this->assertTrue($this->testUser->hasRole('testRole2'));
+
+ $this->testUser->removeRole($this->testUserRole->getKey(), 'testRole2');
+
+ $this->assertFalse($this->testUser->hasRole('testRole'));
+
+ $this->assertFalse($this->testUser->hasRole('testRole2'));
}
/** @test */
- public function it_can_assign_multiple_roles_using_an_array()
+ #[Test]
+ public function it_can_assign_and_remove_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'));
$this->assertTrue($this->testUser->hasRole('testRole2'));
+
+ $this->testUser->removeRole([$this->testUserRole->getKey(), 'testRole2']);
+
+ $this->assertFalse($this->testUser->hasRole('testRole'));
+
+ $this->assertFalse($this->testUser->hasRole('testRole2'));
}
/** @test */
+ #[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');
@@ -87,16 +251,18 @@ public function it_does_not_remove_already_associated_roles_when_assigning_new_r
}
/** @test */
+ #[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'));
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_assigning_a_role_that_does_not_exist()
{
$this->expectException(RoleDoesNotExist::class);
@@ -105,6 +271,7 @@ public function it_throws_an_exception_when_assigning_a_role_that_does_not_exist
}
/** @test */
+ #[Test]
public function it_can_only_assign_roles_from_the_correct_guard()
{
$this->expectException(RoleDoesNotExist::class);
@@ -113,6 +280,7 @@ public function it_can_only_assign_roles_from_the_correct_guard()
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_assigning_a_role_from_a_different_guard()
{
$this->expectException(GuardDoesNotMatch::class);
@@ -121,6 +289,7 @@ public function it_throws_an_exception_when_assigning_a_role_from_a_different_gu
}
/** @test */
+ #[Test]
public function it_ignores_null_roles_when_syncing()
{
$this->testUser->assignRole('testRole');
@@ -133,6 +302,7 @@ public function it_ignores_null_roles_when_syncing()
}
/** @test */
+ #[Test]
public function it_can_sync_roles_from_a_string()
{
$this->testUser->assignRole('testRole');
@@ -145,6 +315,7 @@ public function it_can_sync_roles_from_a_string()
}
/** @test */
+ #[Test]
public function it_can_sync_roles_from_a_string_on_a_permission()
{
$this->testUserPermission->assignRole('testRole');
@@ -157,6 +328,35 @@ public function it_can_sync_roles_from_a_string_on_a_permission()
}
/** @test */
+ #[Test]
+ public function it_can_avoid_sync_duplicated_roles()
+ {
+ $this->testUser->syncRoles('testRole', 'testRole', 'testRole2');
+
+ $this->assertTrue($this->testUser->hasRole('testRole'));
+
+ $this->assertTrue($this->testUser->hasRole('testRole2'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_avoid_detach_on_role_that_does_not_exist_sync()
+ {
+ $this->testUser->syncRoles('testRole');
+
+ try {
+ $this->testUser->syncRoles('role-does-not-exist');
+ $this->fail('Expected RoleDoesNotExist exception was not thrown.');
+ } catch (RoleDoesNotExist $e) {
+ //
+ }
+
+ $this->assertTrue($this->testUser->hasRole('testRole'));
+ $this->assertFalse($this->testUser->hasRole('role-does-not-exist'));
+ }
+
+ /** @test */
+ #[Test]
public function it_can_sync_multiple_roles()
{
$this->testUser->syncRoles('testRole', 'testRole2');
@@ -167,6 +367,7 @@ public function it_can_sync_multiple_roles()
}
/** @test */
+ #[Test]
public function it_can_sync_multiple_roles_from_an_array()
{
$this->testUser->syncRoles(['testRole', 'testRole2']);
@@ -177,6 +378,7 @@ public function it_can_sync_multiple_roles_from_an_array()
}
/** @test */
+ #[Test]
public function it_will_remove_all_roles_when_an_empty_array_is_passed_to_sync_roles()
{
$this->testUser->assignRole('testRole');
@@ -191,16 +393,56 @@ public function it_will_remove_all_roles_when_an_empty_array_is_passed_to_sync_r
}
/** @test */
+ #[Test]
+ public function sync_roles_error_does_not_detach_roles()
+ {
+ $this->testUser->assignRole('testRole');
+
+ $this->expectException(RoleDoesNotExist::class);
+
+ $this->testUser->syncRoles('testRole2', 'role-that-does-not-exist');
+
+ $this->assertTrue($this->testUser->fresh()->hasRole('testRole'));
+ }
+
+ /** @test */
+ #[Test]
public function it_will_sync_roles_to_a_model_that_is_not_persisted()
{
$user = new User(['email' => 'test@user.com']);
$user->syncRoles([$this->testUserRole]);
$user->save();
+ $user->save(); // test save same model twice
$this->assertTrue($user->hasRole($this->testUserRole));
+
+ $user->syncRoles([$this->testUserRole]);
+ $this->assertTrue($user->hasRole($this->testUserRole));
+ $this->assertTrue($user->fresh()->hasRole($this->testUserRole));
}
/** @test */
+ #[Test]
+ public function it_does_not_run_unnecessary_sqls_when_assigning_new_roles()
+ {
+ $role2 = app(Role::class)->where('name', ['testRole2'])->first();
+
+ DB::enableQueryLog();
+ $this->testUser->syncRoles($this->testUserRole, $role2);
+ DB::disableQueryLog();
+
+ $necessaryQueriesCount = 2;
+
+ // Teams reloads relation, adding an extra query
+ if (app(PermissionRegistrar::class)->teams) {
+ $necessaryQueriesCount++;
+ }
+
+ $this->assertCount($necessaryQueriesCount, DB::getQueryLog());
+ }
+
+ /** @test */
+ #[Test]
public function calling_syncRoles_before_saving_object_doesnt_interfere_with_other_objects()
{
$user = new User(['email' => 'test@user.com']);
@@ -209,13 +451,21 @@ 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(2, count(DB::getQueryLog())); // avoid unnecessary sync
}
/** @test */
+ #[Test]
public function calling_assignRole_before_saving_object_doesnt_interfere_with_other_objects()
{
$user = new User(['email' => 'test@user.com']);
@@ -224,13 +474,21 @@ 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(2, count(DB::getQueryLog())); // avoid unnecessary sync
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_syncing_a_role_from_another_guard()
{
$this->expectException(RoleDoesNotExist::class);
@@ -243,6 +501,7 @@ public function it_throws_an_exception_when_syncing_a_role_from_another_guard()
}
/** @test */
+ #[Test]
public function it_deletes_pivot_table_entries_when_deleting_models()
{
$user = User::create(['email' => 'user@test.com']);
@@ -260,6 +519,7 @@ public function it_deletes_pivot_table_entries_when_deleting_models()
}
/** @test */
+ #[Test]
public function it_can_scope_users_using_a_string()
{
$user1 = User::create(['email' => 'user1@test.com']);
@@ -269,10 +529,28 @@ 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 */
+ #[Test]
+ public function it_can_withoutscope_users_using_a_string()
+ {
+ User::all()->each(fn ($item) => $item->delete());
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
+ $user1->assignRole('testRole');
+ $user2->assignRole('testRole2');
+ $user3->assignRole('testRole2');
+
+ $scopedUsers = User::withoutRole('testRole2')->get();
+
+ $this->assertEquals(1, $scopedUsers->count());
}
/** @test */
+ #[Test]
public function it_can_scope_users_using_an_array()
{
$user1 = User::create(['email' => 'user1@test.com']);
@@ -281,33 +559,72 @@ public function it_can_scope_users_using_an_array()
$user2->assignRole('testRole2');
$scopedUsers1 = User::role([$this->testUserRole])->get();
-
$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 */
- public function it_can_scope_users_using_an_array_of_ids_and_names()
+ #[Test]
+ public function it_can_withoutscope_users_using_an_array()
{
+ User::all()->each(fn ($item) => $item->delete());
$user1 = User::create(['email' => 'user1@test.com']);
$user2 = User::create(['email' => 'user2@test.com']);
-
+ $user3 = User::create(['email' => 'user3@test.com']);
$user1->assignRole($this->testUserRole);
+ $user2->assignRole('testRole2');
+ $user3->assignRole('testRole2');
+
+ $scopedUsers1 = User::withoutRole([$this->testUserRole])->get();
+ $scopedUsers2 = User::withoutRole([$this->testUserRole->name, 'testRole2'])->get();
+
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(0, $scopedUsers2->count());
+ }
+ /** @test */
+ #[Test]
+ public function it_can_scope_users_using_an_array_of_ids_and_names()
+ {
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user1->assignRole($this->testUserRole);
$user2->assignRole('testRole2');
- $roleName = $this->testUserRole->name;
+ $firstAssignedRoleName = $this->testUserRole->name;
+ $secondAssignedRoleId = app(Role::class)->findByName('testRole2')->getKey();
+
+ $scopedUsers = User::role([$firstAssignedRoleName, $secondAssignedRoleId])->get();
+
+ $this->assertEquals(2, $scopedUsers->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_withoutscope_users_using_an_array_of_ids_and_names()
+ {
+ app(Role::class)->create(['name' => 'testRole3']);
+
+ User::all()->each(fn ($item) => $item->delete());
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
+ $user1->assignRole($this->testUserRole);
+ $user2->assignRole('testRole2');
+ $user3->assignRole('testRole2');
- $otherRoleId = app(Role::class)->find(2)->id;
+ $firstAssignedRoleName = $this->testUserRole->name;
+ $unassignedRoleId = app(Role::class)->findByName('testRole3')->getKey();
- $scopedUsers = User::role([$roleName, $otherRoleId])->get();
+ $scopedUsers = User::withoutRole([$firstAssignedRoleName, $unassignedRoleId])->get();
- $this->assertEquals($scopedUsers->count(), 2);
+ $this->assertEquals(2, $scopedUsers->count());
}
/** @test */
+ #[Test]
public function it_can_scope_users_using_a_collection()
{
$user1 = User::create(['email' => 'user1@test.com']);
@@ -318,11 +635,33 @@ 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 */
+ #[Test]
+ public function it_can_withoutscope_users_using_a_collection()
+ {
+ app(Role::class)->create(['name' => 'testRole3']);
+
+ User::all()->each(fn ($item) => $item->delete());
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
+ $user1->assignRole($this->testUserRole);
+ $user2->assignRole('testRole');
+ $user3->assignRole('testRole2');
+
+ $scopedUsers1 = User::withoutRole([$this->testUserRole])->get();
+ $scopedUsers2 = User::withoutRole(collect(['testRole', 'testRole3']))->get();
+
+ $this->assertEquals(1, $scopedUsers1->count());
+ $this->assertEquals(1, $scopedUsers2->count());
}
/** @test */
+ #[Test]
public function it_can_scope_users_using_an_object()
{
$user1 = User::create(['email' => 'user1@test.com']);
@@ -334,12 +673,92 @@ 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 */
+ #[Test]
+ public function it_can_withoutscope_users_using_an_object()
+ {
+ User::all()->each(fn ($item) => $item->delete());
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
+ $user1->assignRole($this->testUserRole);
+ $user2->assignRole('testRole2');
+ $user3->assignRole('testRole2');
+
+ $scopedUsers1 = User::withoutRole($this->testUserRole)->get();
+ $scopedUsers2 = User::withoutRole([$this->testUserRole])->get();
+ $scopedUsers3 = User::withoutRole(collect([$this->testUserRole]))->get();
+
+ $this->assertEquals(2, $scopedUsers1->count());
+ $this->assertEquals(2, $scopedUsers2->count());
+ $this->assertEquals(2, $scopedUsers3->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_scope_against_a_specific_guard()
+ {
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user1->assignRole('testRole');
+ $user2->assignRole('testRole2');
+
+ $scopedUsers1 = User::role('testRole', 'web')->get();
+
+ $this->assertEquals(1, $scopedUsers1->count());
+
+ $user3 = Admin::create(['email' => 'user3@test.com']);
+ $user4 = Admin::create(['email' => 'user4@test.com']);
+ $user5 = Admin::create(['email' => 'user5@test.com']);
+ $testAdminRole2 = app(Role::class)->create(['name' => 'testAdminRole2', 'guard_name' => 'admin']);
+ $user3->assignRole($this->testAdminRole);
+ $user4->assignRole($this->testAdminRole);
+ $user5->assignRole($testAdminRole2);
+ $scopedUsers2 = Admin::role('testAdminRole', 'admin')->get();
+ $scopedUsers3 = Admin::role('testAdminRole2', 'admin')->get();
+
+ $this->assertEquals(2, $scopedUsers2->count());
+ $this->assertEquals(1, $scopedUsers3->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_withoutscope_against_a_specific_guard()
+ {
+ User::all()->each(fn ($item) => $item->delete());
+ $user1 = User::create(['email' => 'user1@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+ $user3 = User::create(['email' => 'user3@test.com']);
+ $user1->assignRole('testRole');
+ $user2->assignRole('testRole2');
+ $user3->assignRole('testRole2');
+
+ $scopedUsers1 = User::withoutRole('testRole', 'web')->get();
+
+ $this->assertEquals(2, $scopedUsers1->count());
+
+ Admin::all()->each(fn ($item) => $item->delete());
+ $user4 = Admin::create(['email' => 'user4@test.com']);
+ $user5 = Admin::create(['email' => 'user5@test.com']);
+ $user6 = Admin::create(['email' => 'user6@test.com']);
+ $testAdminRole2 = app(Role::class)->create(['name' => 'testAdminRole2', 'guard_name' => 'admin']);
+ $user4->assignRole($this->testAdminRole);
+ $user5->assignRole($this->testAdminRole);
+ $user6->assignRole($testAdminRole2);
+ $scopedUsers2 = Admin::withoutRole('testAdminRole', 'admin')->get();
+ $scopedUsers3 = Admin::withoutRole('testAdminRole2', 'admin')->get();
+
+ $this->assertEquals(1, $scopedUsers2->count());
+ $this->assertEquals(2, $scopedUsers3->count());
+ }
+
+ /** @test */
+ #[Test]
public function it_throws_an_exception_when_trying_to_scope_a_role_from_another_guard()
{
$this->expectException(RoleDoesNotExist::class);
@@ -352,6 +771,38 @@ public function it_throws_an_exception_when_trying_to_scope_a_role_from_another_
}
/** @test */
+ #[Test]
+ public function it_throws_an_exception_when_trying_to_call_withoutscope_on_a_role_from_another_guard()
+ {
+ $this->expectException(RoleDoesNotExist::class);
+
+ User::withoutRole('testAdminRole')->get();
+
+ $this->expectException(GuardDoesNotMatch::class);
+
+ User::withoutRole($this->testAdminRole)->get();
+ }
+
+ /** @test */
+ #[Test]
+ public function it_throws_an_exception_when_trying_to_scope_a_non_existing_role()
+ {
+ $this->expectException(RoleDoesNotExist::class);
+
+ User::role('role not defined')->get();
+ }
+
+ /** @test */
+ #[Test]
+ public function it_throws_an_exception_when_trying_to_use_withoutscope_on_a_non_existing_role()
+ {
+ $this->expectException(RoleDoesNotExist::class);
+
+ User::withoutRole('role not defined')->get();
+ }
+
+ /** @test */
+ #[Test]
public function it_can_determine_that_a_user_has_one_of_the_given_roles()
{
$roleModel = app(Role::class);
@@ -380,6 +831,7 @@ public function it_can_determine_that_a_user_has_one_of_the_given_roles()
}
/** @test */
+ #[Test]
public function it_can_determine_that_a_user_has_all_of_the_given_roles()
{
$roleModel = app(Role::class);
@@ -394,14 +846,62 @@ public function it_can_determine_that_a_user_has_all_of_the_given_roles()
$this->testUser->assignRole($this->testUserRole);
+ $this->assertTrue($this->testUser->hasAllRoles('testRole'));
+ $this->assertTrue($this->testUser->hasAllRoles('testRole', 'web'));
+ $this->assertFalse($this->testUser->hasAllRoles('testRole', 'fakeGuard'));
+
$this->assertFalse($this->testUser->hasAllRoles(['testRole', 'second role']));
+ $this->assertFalse($this->testUser->hasAllRoles(['testRole', 'second role'], 'web'));
$this->testUser->assignRole('second role');
$this->assertTrue($this->testUser->hasAllRoles(['testRole', 'second role']));
+ $this->assertTrue($this->testUser->hasAllRoles(['testRole', 'second role'], 'web'));
+ $this->assertFalse($this->testUser->hasAllRoles(['testRole', 'second role'], 'fakeGuard'));
}
/** @test */
+ #[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 */
+ #[Test]
public function it_can_determine_that_a_user_does_not_have_a_role_from_another_guard()
{
$this->assertFalse($this->testUser->hasRole('testAdminRole'));
@@ -416,18 +916,45 @@ public function it_can_determine_that_a_user_does_not_have_a_role_from_another_g
}
/** @test */
+ #[Test]
+ public function it_can_check_against_any_multiple_roles_using_multiple_arguments()
+ {
+ $this->testUser->assignRole('testRole');
+
+ $this->assertTrue($this->testUser->hasAnyRole($this->testAdminRole, ['testRole'], 'This Role Does Not Even Exist'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_returns_false_instead_of_an_exception_when_checking_against_any_undefined_roles_using_multiple_arguments()
+ {
+ $this->assertFalse($this->testUser->hasAnyRole('This Role Does Not Even Exist', $this->testAdminRole));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_throws_an_exception_if_an_unsupported_type_is_passed_to_hasRoles()
+ {
+ $this->expectException(\TypeError::class);
+
+ $this->testUser->hasRole(new class {});
+ }
+
+ /** @test */
+ #[Test]
public function it_can_retrieve_role_names()
{
$this->testUser->assignRole('testRole', 'testRole2');
$this->assertEquals(
collect(['testRole', 'testRole2']),
- $this->testUser->getRoleNames()
+ $this->testUser->getRoleNames()->sort()->values()
);
}
/** @test */
- public function it_does_not_detach_roles_when_soft_deleting()
+ #[Test]
+ public function it_does_not_detach_roles_when_user_soft_deleting()
{
$user = SoftDeletingUser::create(['email' => 'test@example.com']);
$user->assignRole('testRole');
@@ -437,4 +964,121 @@ public function it_does_not_detach_roles_when_soft_deleting()
$this->assertTrue($user->hasRole('testRole'));
}
+
+ /** @test */
+ #[Test]
+ public function it_fires_an_event_when_a_role_is_added()
+ {
+ Event::fake();
+ app('config')->set('permission.events_enabled', true);
+
+ $this->testUser->assignRole(['testRole', 'testRole2']);
+
+ $roleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2'])
+ ->pluck($this->testUserRole->getKeyName())
+ ->toArray();
+
+ Event::assertDispatched(RoleAttached::class, function ($event) use ($roleIds) {
+ return $event->model instanceof User
+ && $event->model->hasRole('testRole')
+ && $event->model->hasRole('testRole2')
+ && $event->rolesOrIds === $roleIds;
+ });
+ }
+
+ /** @test */
+ #[Test]
+ public function it_fires_an_event_when_a_role_is_removed()
+ {
+ Event::fake();
+ app('config')->set('permission.events_enabled', true);
+
+ $this->testUser->assignRole('testRole', 'testRole2');
+
+ $this->testUser->removeRole('testRole', 'testRole2');
+
+ $roleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2'])
+ ->pluck($this->testUserRole->getKeyName())
+ ->toArray();
+
+ Event::assertDispatched(RoleDetached::class, function ($event) use ($roleIds) {
+ return $event->model instanceof User
+ && ! $event->model->hasRole('testRole')
+ && ! $event->model->hasRole('testRole2')
+ && $event->rolesOrIds === $roleIds;
+ });
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_be_given_a_role_on_permission_when_lazy_loading_is_restricted()
+ {
+ $this->assertTrue(Model::preventsLazyLoading());
+
+ try {
+ $testPermission = app(Permission::class)->with('roles')->get()->first();
+
+ $testPermission->assignRole('testRole');
+
+ $this->assertTrue($testPermission->hasRole('testRole'));
+ } catch (Exception $e) {
+ $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage());
+ }
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_be_given_a_role_on_user_when_lazy_loading_is_restricted()
+ {
+ $this->assertTrue(Model::preventsLazyLoading());
+
+ try {
+ User::create(['email' => 'other@user.com']);
+ $user = User::with('roles')->get()->first();
+ $user->assignRole('testRole');
+
+ $this->assertTrue($user->hasRole('testRole'));
+ } catch (Exception $e) {
+ $this->fail('Lazy loading detected in the givePermissionTo method: '.$e->getMessage());
+ }
+ }
+
+ /** @test */
+ #[Test]
+ public function it_fires_detach_event_when_syncing_roles()
+ {
+ Event::fake([RoleDetached::class, RoleAttached::class]);
+ app('config')->set('permission.events_enabled', true);
+
+ $this->testUser->assignRole('testRole', 'testRole2');
+
+ app(Role::class)->create(['name' => 'testRole3']);
+
+ $this->testUser->syncRoles('testRole3');
+
+ $this->assertFalse($this->testUser->hasRole('testRole'));
+ $this->assertFalse($this->testUser->hasRole('testRole2'));
+ $this->assertTrue($this->testUser->hasRole('testRole3'));
+
+ $removedRoleIds = app(Role::class)::whereIn('name', ['testRole', 'testRole2'])
+ ->pluck($this->testUserRole->getKeyName())
+ ->toArray();
+
+ Event::assertDispatched(RoleDetached::class, function ($event) use ($removedRoleIds) {
+ return $event->model instanceof User
+ && ! $event->model->hasRole('testRole')
+ && ! $event->model->hasRole('testRole2')
+ && $event->rolesOrIds === $removedRoleIds;
+ });
+
+ $attachedRoleIds = app(Role::class)::whereIn('name', ['testRole3'])
+ ->pluck($this->testUserRole->getKeyName())
+ ->toArray();
+
+ Event::assertDispatched(RoleAttached::class, function ($event) use ($attachedRoleIds) {
+ return $event->model instanceof User
+ && $event->model->hasRole('testRole3')
+ && $event->rolesOrIds === $attachedRoleIds;
+ });
+ }
}
diff --git a/tests/HasRolesWithCustomModelsTest.php b/tests/HasRolesWithCustomModelsTest.php
new file mode 100644
index 000000000..767ab6752
--- /dev/null
+++ b/tests/HasRolesWithCustomModelsTest.php
@@ -0,0 +1,109 @@
+get('cache.default') == 'database') {
+ $this->resetDatabaseQuery = 1;
+ }
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_use_custom_model_role()
+ {
+ $this->assertSame(get_class($this->testUserRole), Role::class);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_doesnt_detach_permissions_when_soft_deleting()
+ {
+ $this->testUserRole->givePermissionTo($this->testUserPermission);
+
+ DB::enableQueryLog();
+ $this->testUserRole->delete();
+ DB::disableQueryLog();
+
+ $this->assertSame(1 + $this->resetDatabaseQuery, count(DB::getQueryLog()));
+
+ $role = Role::onlyTrashed()->find($this->testUserRole->getKey());
+
+ $this->assertEquals(1, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $role->getKey())->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_doesnt_detach_users_when_soft_deleting()
+ {
+ $this->testUser->assignRole($this->testUserRole);
+
+ DB::enableQueryLog();
+ $this->testUserRole->delete();
+ DB::disableQueryLog();
+
+ $this->assertSame(1 + $this->resetDatabaseQuery, count(DB::getQueryLog()));
+
+ $role = Role::onlyTrashed()->find($this->testUserRole->getKey());
+
+ $this->assertEquals(1, DB::table(config('permission.table_names.model_has_roles'))->where('role_test_id', $role->getKey())->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_does_detach_permissions_and_users_when_force_deleting()
+ {
+ $role_id = $this->testUserRole->getKey();
+ $this->testUserPermission->assignRole($role_id);
+ $this->testUser->assignRole($role_id);
+
+ DB::enableQueryLog();
+ $this->testUserRole->forceDelete();
+ DB::disableQueryLog();
+
+ $this->assertSame(3 + $this->resetDatabaseQuery, count(DB::getQueryLog()));
+
+ $role = Role::withTrashed()->find($role_id);
+
+ $this->assertNull($role);
+ $this->assertEquals(0, DB::table(config('permission.table_names.role_has_permissions'))->where('role_test_id', $role_id)->count());
+ $this->assertEquals(0, DB::table(config('permission.table_names.model_has_roles'))->where('role_test_id', $role_id)->count());
+ }
+
+ /** @test */
+ #[Test]
+ public function it_should_touch_when_assigning_new_roles()
+ {
+ Carbon::setTestNow('2021-07-19 10:13:14');
+
+ $user = Admin::create(['email' => 'user1@test.com']);
+ $role1 = app(Role::class)->create(['name' => 'testRoleInWebGuard', 'guard_name' => 'admin']);
+ $role2 = app(Role::class)->create(['name' => 'testRoleInWebGuard1', 'guard_name' => 'admin']);
+
+ $this->assertSame('2021-07-19 10:13:14', $role1->updated_at->format('Y-m-d H:i:s'));
+
+ Carbon::setTestNow('2021-07-20 19:13:14');
+
+ $user->syncRoles([$role1->getKey(), $role2->getKey()]);
+
+ $this->assertSame('2021-07-20 19:13:14', $role1->refresh()->updated_at->format('Y-m-d H:i:s'));
+ $this->assertSame('2021-07-20 19:13:14', $role2->refresh()->updated_at->format('Y-m-d H:i:s'));
+ }
+}
diff --git a/tests/MiddlewareTest.php b/tests/MiddlewareTest.php
deleted file mode 100644
index 87f922fd1..000000000
--- a/tests/MiddlewareTest.php
+++ /dev/null
@@ -1,260 +0,0 @@
-roleMiddleware = new RoleMiddleware($this->app);
-
- $this->permissionMiddleware = new PermissionMiddleware($this->app);
-
- $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware($this->app);
- }
-
- /** @test */
- public function a_guest_cannot_access_a_route_protected_by_the_role_middleware()
- {
- $this->assertEquals(
- $this->runMiddleware(
- $this->roleMiddleware, 'testRole'
- ), 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_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
- );
- }
-
- /** @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);
- }
-
- protected function runMiddleware($middleware, $parameter)
- {
- try {
- return $middleware->handle(new Request(), function () {
- return (new Response())->setContent('');
- }, $parameter)->status();
- } catch (UnauthorizedException $e) {
- return $e->getStatusCode();
- }
- }
-}
diff --git a/tests/MultipleGuardsTest.php b/tests/MultipleGuardsTest.php
index ad9449df5..cd57639cf 100644
--- a/tests/MultipleGuardsTest.php
+++ b/tests/MultipleGuardsTest.php
@@ -1,36 +1,118 @@
set('auth.guards', [
+ 'web' => ['driver' => 'session', 'provider' => 'users'],
+ 'api' => ['driver' => 'token', 'provider' => 'users'],
+ 'jwt' => ['driver' => 'token', 'provider' => 'users'],
+ 'abc' => ['driver' => 'abc'],
+ 'admin' => ['driver' => 'session', 'provider' => 'admins'],
+ ]);
+
+ $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 */
+ #[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'));
+ $this->assertTrue($this->testUser->checkPermissionTo('do_this', 'web'));
+ $this->assertTrue($this->testUser->checkPermissionTo('do_that', 'api'));
+ $this->assertFalse($this->testUser->checkPermissionTo('do_that', 'web'));
}
- protected function getEnvironmentSetUp($app)
+ /** @test */
+ #[Test]
+ public function the_gate_can_grant_permission_to_a_user_by_passing_a_guard_name()
{
- parent::getEnvironmentSetUp($app);
+ $this->testUser->givePermissionTo(app(Permission::class)::create([
+ 'name' => 'do_this',
+ 'guard_name' => 'web',
+ ]));
- $app['config']->set('auth.guards', [
- 'web' => ['driver' => 'session', 'provider' => 'users'],
- 'api' => ['driver' => 'jwt', 'provider' => 'users'],
- 'abc' => ['driver' => 'abc'],
- ]);
+ $this->testUser->givePermissionTo(app(Permission::class)::create([
+ 'name' => 'do_that',
+ 'guard_name' => 'api',
+ ]));
+
+ $this->assertTrue($this->testUser->can('do_this', 'web'));
+ $this->assertTrue($this->testUser->can('do_that', 'api'));
+ $this->assertFalse($this->testUser->can('do_that', 'web'));
+
+ $this->assertTrue($this->testUser->cannot('do_that', 'web'));
+ $this->assertTrue($this->testUser->canAny(['do_this', 'do_that'], 'web'));
+
+ $this->testAdminRole->givePermissionTo($this->testAdminPermission);
+ $this->testAdmin->assignRole($this->testAdminRole);
+
+ $this->assertTrue($this->testAdmin->hasPermissionTo($this->testAdminPermission));
+ $this->assertTrue($this->testAdmin->can('admin-permission'));
+ $this->assertTrue($this->testAdmin->can('admin-permission', 'admin'));
+ $this->assertTrue($this->testAdmin->cannot('admin-permission', 'web'));
+
+ $this->assertTrue($this->testAdmin->cannot('non-existing-permission'));
+ $this->assertTrue($this->testAdmin->cannot('non-existing-permission', 'web'));
+ $this->assertTrue($this->testAdmin->cannot('non-existing-permission', 'admin'));
+ $this->assertTrue($this->testAdmin->cannot(['admin-permission', 'non-existing-permission'], 'web'));
+
+ $this->assertFalse($this->testAdmin->can('edit-articles', 'web'));
+ $this->assertFalse($this->testAdmin->can('edit-articles', 'admin'));
+
+ $this->assertTrue($this->testUser->cannot('edit-articles', 'admin'));
+ $this->assertTrue($this->testUser->cannot('admin-permission', 'admin'));
+ $this->assertTrue($this->testUser->cannot('admin-permission', 'web'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_honour_guardName_function_on_model_for_overriding_guard_name_property()
+ {
+ $user = Manager::create(['email' => 'manager@test.com']);
+ $user->givePermissionTo(app(Permission::class)::create([
+ 'name' => 'do_jwt',
+ 'guard_name' => 'jwt',
+ ]));
+
+ // 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_jwt', 'web'));
}
}
diff --git a/tests/PermissionMiddlewareTest.php b/tests/PermissionMiddlewareTest.php
new file mode 100644
index 000000000..2aa233307
--- /dev/null
+++ b/tests/PermissionMiddlewareTest.php
@@ -0,0 +1,488 @@
+permissionMiddleware = new PermissionMiddleware;
+ }
+
+ /** @test */
+ #[Test]
+ public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware()
+ {
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles')
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_cannot_access_a_route_protected_by_the_permission_middleware_of_a_different_guard(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ // 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' => 'api']);
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->givePermissionTo($p1);
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'admin-permission2', 'api', true)
+ );
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles2', 'web', true)
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function a_super_admin_user_can_access_a_route_protected_by_permission_middleware()
+ {
+ Auth::login($this->testUser);
+
+ Gate::before(function ($user, $ability) {
+ return $user->getKey() === $this->testUser->getKey() ? true : null;
+ });
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles')
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_can_access_a_route_protected_by_permission_middleware_if_have_this_permission(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*'], 'api');
+
+ $this->testClient->givePermissionTo('edit-posts');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-posts', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->givePermissionTo('edit-posts');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-news|edit-posts', null, true)
+ );
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, ['edit-news', 'edit-posts'], null, true)
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_not_has_roles_trait()
+ {
+ $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']);
+
+ Auth::login($userWithoutHasRoles);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-news')
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->givePermissionTo('edit-posts');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-news', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_cannot_access_a_route_protected_by_permission_middleware_if_have_not_permissions(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles|edit-posts', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_can_access_a_route_protected_by_permission_middleware_if_has_permission_via_role(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-articles', null, true)
+ );
+
+ $this->testClientRole->givePermissionTo('edit-posts');
+ $this->testClient->assignRole('clientRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-posts', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[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 */
+ #[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 */
+ #[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 */
+ #[Test]
+ public function client_can_not_access_permission_with_guard_admin_while_login_using_default_guard(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->givePermissionTo('edit-posts');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'edit-posts', 'admin', true)
+ );
+ }
+
+ /** @test */
+ #[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')
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function the_middleware_can_be_created_with_static_using_method()
+ {
+ $this->assertSame(
+ 'Spatie\Permission\Middleware\PermissionMiddleware:edit-articles',
+ PermissionMiddleware::using('edit-articles')
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\PermissionMiddleware:edit-articles,my-guard',
+ PermissionMiddleware::using('edit-articles', 'my-guard')
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\PermissionMiddleware:edit-articles|edit-news',
+ PermissionMiddleware::using(['edit-articles', 'edit-news'])
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function the_middleware_can_handle_enum_based_permissions_with_static_using_method()
+ {
+ $this->assertSame(
+ 'Spatie\Permission\Middleware\PermissionMiddleware:view articles',
+ PermissionMiddleware::using(TestModels\TestRolePermissionsEnum::VIEWARTICLES)
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\PermissionMiddleware:view articles,my-guard',
+ PermissionMiddleware::using(TestModels\TestRolePermissionsEnum::VIEWARTICLES, 'my-guard')
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\PermissionMiddleware:view articles|edit articles',
+ PermissionMiddleware::using([TestModels\TestRolePermissionsEnum::VIEWARTICLES, TestModels\TestRolePermissionsEnum::EDITARTICLES])
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function the_middleware_can_handle_enum_based_permissions_with_handle_method()
+ {
+ app(Permission::class)->create(['name' => TestModels\TestRolePermissionsEnum::VIEWARTICLES->value]);
+ app(Permission::class)->create(['name' => TestModels\TestRolePermissionsEnum::EDITARTICLES->value]);
+
+ Auth::login($this->testUser);
+ $this->testUser->givePermissionTo(TestModels\TestRolePermissionsEnum::VIEWARTICLES);
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, TestModels\TestRolePermissionsEnum::VIEWARTICLES)
+ );
+
+ $this->testUser->givePermissionTo(TestModels\TestRolePermissionsEnum::EDITARTICLES);
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, [TestModels\TestRolePermissionsEnum::VIEWARTICLES, TestModels\TestRolePermissionsEnum::EDITARTICLES])
+ );
+ }
+}
diff --git a/tests/PermissionRegistrarTest.php b/tests/PermissionRegistrarTest.php
new file mode 100644
index 000000000..c9df969e4
--- /dev/null
+++ b/tests/PermissionRegistrarTest.php
@@ -0,0 +1,133 @@
+getProperty('permissions');
+ $reflectedProperty->setAccessible(true);
+
+ app(PermissionRegistrar::class)->getPermissions();
+
+ $this->assertNotNull($reflectedProperty->getValue(app(PermissionRegistrar::class)));
+
+ app(PermissionRegistrar::class)->clearPermissionsCollection();
+
+ $this->assertNull($reflectedProperty->getValue(app(PermissionRegistrar::class)));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_uids()
+ {
+ $uids = [
+ // UUIDs
+ '00000000-0000-0000-0000-000000000000',
+ '9be37b52-e1fa-4e86-b65f-cbfcbedde838',
+ 'fc458041-fb21-4eea-a04b-b55c87a7224a',
+ '78144b52-a889-11ed-afa1-0242ac120002',
+ '78144f4e-a889-11ed-afa1-0242ac120002',
+ // GUIDs
+ '4b8590bb-90a2-4f38-8dc9-70e663a5b0e5',
+ 'A98C5A1E-A742-4808-96FA-6F409E799937',
+ '1f01164a-98e9-4246-93ec-7941aefb1da6',
+ '91b73d20-89e6-46b0-b39b-632706cc3ed7',
+ '0df4a5b8-7c2e-484f-ad1d-787d1b83aacc',
+ // ULIDs
+ '01GRVB3DREB63KNN4G2QVV99DF',
+ '01GRVB3DRECY317SJCJ6DMTFCA',
+ '01GRVB3DREGGPBXNH1M24GX1DS',
+ '01GRVB3DRESRM2K9AVQSW1JCKA',
+ '01GRVB3DRES5CQ31PB24MP4CSV',
+ ];
+
+ $not_uids = [
+ '9be37b52-e1fa',
+ '9be37b52-e1fa-4e86',
+ '9be37b52-e1fa-4e86-b65f',
+ '01GRVB3DREB63KNN4G2',
+ 'TEST STRING',
+ '00-00-00-00-00-00',
+ '91GRVB3DRES5CQ31PB24MP4CSV',
+ ];
+
+ foreach ($uids as $uid) {
+ $this->assertTrue(PermissionRegistrar::isUid($uid));
+ }
+
+ foreach ($not_uids as $not_uid) {
+ $this->assertFalse(PermissionRegistrar::isUid($not_uid));
+ }
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_get_permission_class()
+ {
+ $this->assertSame(SpatiePermission::class, app(PermissionRegistrar::class)->getPermissionClass());
+ $this->assertSame(SpatiePermission::class, get_class(app(PermissionContract::class)));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_change_permission_class()
+ {
+ $this->assertSame(SpatiePermission::class, config('permission.models.permission'));
+ $this->assertSame(SpatiePermission::class, app(PermissionRegistrar::class)->getPermissionClass());
+ $this->assertSame(SpatiePermission::class, get_class(app(PermissionContract::class)));
+
+ app(PermissionRegistrar::class)->setPermissionClass(TestPermission::class);
+
+ $this->assertSame(TestPermission::class, config('permission.models.permission'));
+ $this->assertSame(TestPermission::class, app(PermissionRegistrar::class)->getPermissionClass());
+ $this->assertSame(TestPermission::class, get_class(app(PermissionContract::class)));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_get_role_class()
+ {
+ $this->assertSame(SpatieRole::class, app(PermissionRegistrar::class)->getRoleClass());
+ $this->assertSame(SpatieRole::class, get_class(app(RoleContract::class)));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_change_role_class()
+ {
+ $this->assertSame(SpatieRole::class, config('permission.models.role'));
+ $this->assertSame(SpatieRole::class, app(PermissionRegistrar::class)->getRoleClass());
+ $this->assertSame(SpatieRole::class, get_class(app(RoleContract::class)));
+
+ app(PermissionRegistrar::class)->setRoleClass(TestRole::class);
+
+ $this->assertSame(TestRole::class, config('permission.models.role'));
+ $this->assertSame(TestRole::class, app(PermissionRegistrar::class)->getRoleClass());
+ $this->assertSame(TestRole::class, get_class(app(RoleContract::class)));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_change_team_id()
+ {
+ $team_id = '00000000-0000-0000-0000-000000000000';
+
+ app(PermissionRegistrar::class)->setPermissionsTeamId($team_id);
+
+ $this->assertSame($team_id, app(PermissionRegistrar::class)->getPermissionsTeamId());
+ }
+}
diff --git a/tests/PermissionTest.php b/tests/PermissionTest.php
index f1f009400..553b3650a 100644
--- a/tests/PermissionTest.php
+++ b/tests/PermissionTest.php
@@ -1,13 +1,31 @@
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 */
+ #[Test]
public function it_throws_an_exception_when_the_permission_already_exists()
{
$this->expectException(PermissionAlreadyExists::class);
@@ -17,6 +35,7 @@ public function it_throws_an_exception_when_the_permission_already_exists()
}
/** @test */
+ #[Test]
public function it_belongs_to_a_guard()
{
$permission = app(Permission::class)->create(['name' => 'can-edit', 'guard_name' => 'admin']);
@@ -25,12 +44,17 @@ public function it_belongs_to_a_guard()
}
/** @test */
+ #[Test]
public function it_belongs_to_the_default_guard_by_default()
{
- $this->assertEquals($this->app['config']->get('auth.defaults.guard'), $this->testUserPermission->guard_name);
+ $this->assertEquals(
+ $this->app['config']->get('auth.defaults.guard'),
+ $this->testUserPermission->guard_name
+ );
}
/** @test */
+ #[Test]
public function it_has_user_models_of_the_right_class()
{
$this->testAdmin->givePermissionTo($this->testAdminPermission);
@@ -43,10 +67,23 @@ public function it_has_user_models_of_the_right_class()
}
/** @test */
+ #[Test]
public function it_is_retrievable_by_id()
{
$permission_by_id = app(Permission::class)->findById($this->testUserPermission->id);
$this->assertEquals($this->testUserPermission->id, $permission_by_id->id);
}
+
+ /** @test */
+ #[Test]
+ public function it_can_delete_hydrated_permissions()
+ {
+ $this->reloadPermissions();
+
+ $permission = app(Permission::class)->findByName($this->testUserPermission->name);
+ $permission->delete();
+
+ $this->assertCount(0, app(Permission::class)->where($this->testUserPermission->getKeyName(), $this->testUserPermission->getKey())->get());
+ }
}
diff --git a/tests/PolicyTest.php b/tests/PolicyTest.php
new file mode 100644
index 000000000..78490f997
--- /dev/null
+++ b/tests/PolicyTest.php
@@ -0,0 +1,51 @@
+ 'special admin content']);
+ $record2 = Content::create(['content' => 'viewable', 'user_id' => $this->testUser->id]);
+
+ app(Gate::class)->policy(Content::class, ContentPolicy::class);
+
+ $this->assertFalse($this->testUser->can('view', $record1)); // policy rule for 'view'
+ $this->assertFalse($this->testUser->can('update', $record1)); // policy rule for 'update'
+
+ $this->assertTrue($this->testUser->can('update', $record2)); // policy rule for 'update' when matching user_id
+
+ // test that the Admin cannot yet view 'special admin content', because doesn't have Role yet
+ $this->assertFalse($this->testAdmin->can('update', $record1));
+
+ $this->testAdmin->assignRole($this->testAdminRole);
+ // test that the Admin can view 'special admin content'
+ $this->assertTrue($this->testAdmin->can('update', $record1)); // policy override via 'before'
+ $this->assertTrue($this->testAdmin->can('update', $record2)); // policy override via 'before'
+ }
+}
+class ContentPolicy
+{
+ public function before(Authorizable $user, string $ability): ?bool
+ {
+ return $user->hasRole('testAdminRole', 'admin') ?: null;
+ }
+
+ public function view($user, $content)
+ {
+ return $user->id === $content->user_id;
+ }
+
+ public function update($user, $modelRecord): bool
+ {
+ return $user->id === $modelRecord->user_id || $user->can('edit-articles');
+ }
+}
diff --git a/tests/RoleMiddlewareTest.php b/tests/RoleMiddlewareTest.php
new file mode 100644
index 000000000..96adea0a9
--- /dev/null
+++ b/tests/RoleMiddlewareTest.php
@@ -0,0 +1,421 @@
+roleMiddleware = new RoleMiddleware;
+ }
+
+ /** @test */
+ #[Test]
+ public function a_guest_cannot_access_a_route_protected_by_rolemiddleware()
+ {
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testRole')
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_cannot_access_a_route_protected_by_role_middleware_of_another_guard(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->assignRole('clientRole');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testAdminRole', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_can_access_a_route_protected_by_role_middleware_if_have_this_role(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->assignRole('clientRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, 'clientRole', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_can_access_a_route_protected_by_this_role_middleware_if_have_one_of_the_roles(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->assignRole('clientRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, 'clientRole|testRole2', null, true)
+ );
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, ['testRole2', 'clientRole'], null, true)
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function a_user_cannot_access_a_route_protected_by_the_role_middleware_if_have_not_has_roles_trait()
+ {
+ $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']);
+
+ Auth::login($userWithoutHasRoles);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testRole')
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_cannot_access_a_route_protected_by_the_role_middleware_if_have_a_different_role(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->assignRole(['clientRole']);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'clientRole2', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_cannot_access_a_route_protected_by_role_middleware_if_have_not_roles(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'testRole|testRole2', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_cannot_access_a_route_protected_by_role_middleware_if_role_is_undefined(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, '', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[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 */
+ #[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 */
+ #[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 */
+ #[Test]
+ public function client_can_not_access_role_with_guard_admin_while_login_using_default_guard(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->assignRole('clientRole');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleMiddleware, 'clientRole', 'admin', true)
+ );
+ }
+
+ /** @test */
+ #[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')
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function the_middleware_can_be_created_with_static_using_method()
+ {
+ $this->assertSame(
+ 'Spatie\Permission\Middleware\RoleMiddleware:testAdminRole',
+ RoleMiddleware::using('testAdminRole')
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\RoleMiddleware:testAdminRole,my-guard',
+ RoleMiddleware::using('testAdminRole', 'my-guard')
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\RoleMiddleware:testAdminRole|anotherRole',
+ RoleMiddleware::using(['testAdminRole', 'anotherRole'])
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function the_middleware_can_handle_enum_based_roles_with_static_using_method()
+ {
+ $this->assertSame(
+ 'Spatie\Permission\Middleware\RoleMiddleware:writer',
+ RoleMiddleware::using(TestModels\TestRolePermissionsEnum::WRITER)
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\RoleMiddleware:writer,my-guard',
+ RoleMiddleware::using(TestModels\TestRolePermissionsEnum::WRITER, 'my-guard')
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\RoleMiddleware:writer|editor',
+ RoleMiddleware::using([TestModels\TestRolePermissionsEnum::WRITER, TestModels\TestRolePermissionsEnum::EDITOR])
+ );
+ }
+
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function the_middleware_can_handle_enum_based_roles_with_handle_method()
+ {
+ app(Role::class)->create(['name' => TestModels\TestRolePermissionsEnum::WRITER->value]);
+ app(Role::class)->create(['name' => TestModels\TestRolePermissionsEnum::EDITOR->value]);
+
+ Auth::login($this->testUser);
+ $this->testUser->assignRole(TestModels\TestRolePermissionsEnum::WRITER);
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, TestModels\TestRolePermissionsEnum::WRITER)
+ );
+
+ $this->testUser->assignRole(TestModels\TestRolePermissionsEnum::EDITOR);
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleMiddleware, [TestModels\TestRolePermissionsEnum::WRITER, TestModels\TestRolePermissionsEnum::EDITOR])
+ );
+ }
+}
diff --git a/tests/RoleOrPermissionMiddlewareTest.php b/tests/RoleOrPermissionMiddlewareTest.php
new file mode 100644
index 000000000..8a60e9f4f
--- /dev/null
+++ b/tests/RoleOrPermissionMiddlewareTest.php
@@ -0,0 +1,308 @@
+roleOrPermissionMiddleware = new RoleOrPermissionMiddleware;
+ }
+
+ /** @test */
+ #[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 */
+ #[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 */
+ #[Test]
+ public function a_client_can_access_a_route_protected_by_permission_or_role_middleware_if_has_this_permission_or_role(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->assignRole('clientRole');
+ $this->testClient->givePermissionTo('edit-posts');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'clientRole|edit-news|edit-posts', null, true)
+ );
+
+ $this->testClient->removeRole('clientRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'clientRole|edit-posts', null, true)
+ );
+
+ $this->testClient->revokePermissionTo('edit-posts');
+ $this->testClient->assignRole('clientRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'clientRole|edit-posts', null, true)
+ );
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, ['clientRole', 'edit-posts'], null, true)
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function a_super_admin_user_can_access_a_route_protected_by_permission_or_role_middleware()
+ {
+ Auth::login($this->testUser);
+
+ Gate::before(function ($user, $ability) {
+ return $user->getKey() === $this->testUser->getKey() ? true : null;
+ });
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles')
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function a_user_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_has_roles_trait()
+ {
+ $userWithoutHasRoles = UserWithoutHasRoles::create(['email' => 'test_not_has_roles@user.com']);
+
+ Auth::login($userWithoutHasRoles);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|edit-articles')
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[Test]
+ public function a_client_can_not_access_a_route_protected_by_permission_or_role_middleware_if_have_not_this_permission_and_role(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'clientRole|edit-posts', null, true)
+ );
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'missingRole|missingPermission', null, true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[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 */
+ #[Test]
+ public function client_can_not_access_permission_or_role_with_guard_admin_while_login_using_default_guard(): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ $this->markTestSkipped('requires laravel >= 9');
+ }
+
+ Passport::actingAsClient($this->testClient, ['*']);
+
+ $this->testClient->assignRole('clientRole');
+ $this->testClient->givePermissionTo('edit-posts');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'edit-posts|clientRole', 'admin', true)
+ );
+ }
+
+ /** @test */
+ #[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 */
+ #[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 */
+ #[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);
+ }
+
+ /** @test */
+ #[Test]
+ public function the_middleware_can_be_created_with_static_using_method()
+ {
+ $this->assertSame(
+ 'Spatie\Permission\Middleware\RoleOrPermissionMiddleware:edit-articles',
+ RoleOrPermissionMiddleware::using('edit-articles')
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\RoleOrPermissionMiddleware:edit-articles,my-guard',
+ RoleOrPermissionMiddleware::using('edit-articles', 'my-guard')
+ );
+ $this->assertEquals(
+ 'Spatie\Permission\Middleware\RoleOrPermissionMiddleware:edit-articles|testAdminRole',
+ RoleOrPermissionMiddleware::using(['edit-articles', 'testAdminRole'])
+ );
+ }
+}
diff --git a/tests/RoleTest.php b/tests/RoleTest.php
index 31d06c083..af88b0af6 100644
--- a/tests/RoleTest.php
+++ b/tests/RoleTest.php
@@ -1,17 +1,22 @@
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 */
+ #[Test]
public function it_has_user_models_of_the_right_class()
{
$this->testAdmin->assignRole($this->testAdminRole);
@@ -30,9 +50,14 @@ public function it_has_user_models_of_the_right_class()
$this->assertCount(1, $this->testUserRole->users);
$this->assertTrue($this->testUserRole->users->first()->is($this->testUser));
$this->assertInstanceOf(User::class, $this->testUserRole->users->first());
+
+ $this->assertCount(1, $this->testAdminRole->users);
+ $this->assertTrue($this->testAdminRole->users->first()->is($this->testAdmin));
+ $this->assertInstanceOf(Admin::class, $this->testAdminRole->users->first());
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_the_role_already_exists()
{
$this->expectException(RoleAlreadyExists::class);
@@ -42,6 +67,7 @@ public function it_throws_an_exception_when_the_role_already_exists()
}
/** @test */
+ #[Test]
public function it_can_be_given_a_permission()
{
$this->testUserRole->givePermissionTo('edit-articles');
@@ -50,6 +76,7 @@ public function it_can_be_given_a_permission()
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_given_a_permission_that_does_not_exist()
{
$this->expectException(PermissionDoesNotExist::class);
@@ -58,6 +85,7 @@ public function it_throws_an_exception_when_given_a_permission_that_does_not_exi
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_given_a_permission_that_belongs_to_another_guard()
{
$this->expectException(PermissionDoesNotExist::class);
@@ -70,6 +98,7 @@ public function it_throws_an_exception_when_given_a_permission_that_belongs_to_a
}
/** @test */
+ #[Test]
public function it_can_be_given_multiple_permissions_using_an_array()
{
$this->testUserRole->givePermissionTo(['edit-articles', 'edit-news']);
@@ -79,6 +108,7 @@ public function it_can_be_given_multiple_permissions_using_an_array()
}
/** @test */
+ #[Test]
public function it_can_be_given_multiple_permissions_using_multiple_arguments()
{
$this->testUserRole->givePermissionTo('edit-articles', 'edit-news');
@@ -88,6 +118,7 @@ public function it_can_be_given_multiple_permissions_using_multiple_arguments()
}
/** @test */
+ #[Test]
public function it_can_sync_permissions()
{
$this->testUserRole->givePermissionTo('edit-articles');
@@ -100,6 +131,7 @@ public function it_can_sync_permissions()
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_syncing_permissions_that_do_not_exist()
{
$this->testUserRole->givePermissionTo('edit-articles');
@@ -110,6 +142,7 @@ public function it_throws_an_exception_when_syncing_permissions_that_do_not_exis
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_syncing_permissions_that_belong_to_a_different_guard()
{
$this->testUserRole->givePermissionTo('edit-articles');
@@ -124,6 +157,7 @@ public function it_throws_an_exception_when_syncing_permissions_that_belong_to_a
}
/** @test */
+ #[Test]
public function it_will_remove_all_permissions_when_passing_an_empty_array_to_sync_permissions()
{
$this->testUserRole->givePermissionTo('edit-articles');
@@ -138,6 +172,20 @@ public function it_will_remove_all_permissions_when_passing_an_empty_array_to_sy
}
/** @test */
+ #[Test]
+ public function sync_permission_error_does_not_detach_permissions()
+ {
+ $this->testUserRole->givePermissionTo('edit-news');
+
+ $this->expectException(PermissionDoesNotExist::class);
+
+ $this->testUserRole->syncPermissions('edit-articles', 'permission-that-does-not-exist');
+
+ $this->assertTrue($this->testUserRole->fresh()->hasDirectPermission('edit-news'));
+ }
+
+ /** @test */
+ #[Test]
public function it_can_revoke_a_permission()
{
$this->testUserRole->givePermissionTo('edit-articles');
@@ -152,6 +200,7 @@ public function it_can_revoke_a_permission()
}
/** @test */
+ #[Test]
public function it_can_be_given_a_permission_using_objects()
{
$this->testUserRole->givePermissionTo($this->testUserPermission);
@@ -160,12 +209,14 @@ public function it_can_be_given_a_permission_using_objects()
}
/** @test */
+ #[Test]
public function it_returns_false_if_it_does_not_have_the_permission()
{
$this->assertFalse($this->testUserRole->hasPermissionTo('other-permission'));
}
/** @test */
+ #[Test]
public function it_throws_an_exception_if_the_permission_does_not_exist()
{
$this->expectException(PermissionDoesNotExist::class);
@@ -174,6 +225,7 @@ public function it_throws_an_exception_if_the_permission_does_not_exist()
}
/** @test */
+ #[Test]
public function it_returns_false_if_it_does_not_have_a_permission_object()
{
$permission = app(Permission::class)->findByName('other-permission');
@@ -182,6 +234,7 @@ public function it_returns_false_if_it_does_not_have_a_permission_object()
}
/** @test */
+ #[Test]
public function it_creates_permission_object_with_findOrCreate_if_it_does_not_have_a_permission_object()
{
$permission = app(Permission::class)->findOrCreate('another-permission');
@@ -196,6 +249,7 @@ public function it_creates_permission_object_with_findOrCreate_if_it_does_not_ha
}
/** @test */
+ #[Test]
public function it_creates_a_role_with_findOrCreate_if_the_named_role_does_not_exist()
{
$this->expectException(RoleDoesNotExist::class);
@@ -210,6 +264,7 @@ public function it_creates_a_role_with_findOrCreate_if_the_named_role_does_not_e
}
/** @test */
+ #[Test]
public function it_throws_an_exception_when_a_permission_of_the_wrong_guard_is_passed_in()
{
$this->expectException(GuardDoesNotMatch::class);
@@ -220,6 +275,7 @@ public function it_throws_an_exception_when_a_permission_of_the_wrong_guard_is_p
}
/** @test */
+ #[Test]
public function it_belongs_to_a_guard()
{
$role = app(Role::class)->create(['name' => 'admin', 'guard_name' => 'admin']);
@@ -228,8 +284,38 @@ public function it_belongs_to_a_guard()
}
/** @test */
+ #[Test]
public function it_belongs_to_the_default_guard_by_default()
{
- $this->assertEquals($this->app['config']->get('auth.defaults.guard'), $this->testUserRole->guard_name);
+ $this->assertEquals(
+ $this->app['config']->get('auth.defaults.guard'),
+ $this->testUserRole->guard_name
+ );
+ }
+
+ /** @test */
+ #[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/RoleWithNestingTest.php b/tests/RoleWithNestingTest.php
new file mode 100644
index 000000000..5ddf3cac0
--- /dev/null
+++ b/tests/RoleWithNestingTest.php
@@ -0,0 +1,94 @@
+parent_roles = [
+ 'has_no_children' => Role::create(['name' => 'has_no_children']),
+ 'has_1_child' => Role::create(['name' => 'has_1_child']),
+ 'has_3_children' => Role::create(['name' => 'has_3_children']),
+ ];
+ $this->child_roles = [
+ 'has_no_parents' => Role::create(['name' => 'has_no_parents']),
+ 'has_1_parent' => Role::create(['name' => 'has_1_parent']),
+ 'has_2_parents' => Role::create(['name' => 'has_2_parents']),
+ 'third_child' => Role::create(['name' => 'third_child']),
+ ];
+
+ $this->parent_roles['has_1_child']->children()->attach($this->child_roles['has_2_parents']);
+ $this->parent_roles['has_3_children']->children()->attach([
+ $this->child_roles['has_2_parents']->getKey(),
+ $this->child_roles['has_1_parent']->getKey(),
+ $this->child_roles['third_child']->getKey(),
+ ]);
+ }
+
+ /**
+ * Set up the database.
+ *
+ * @param \Illuminate\Foundation\Application $app
+ */
+ protected function setUpDatabase($app)
+ {
+ parent::setUpDatabase($app);
+
+ $tableRoles = $app['config']->get('permission.table_names.roles');
+
+ $app['db']->connection()->getSchemaBuilder()->create(Role::HIERARCHY_TABLE, function ($table) use ($tableRoles) {
+ $table->id();
+ $table->uuid('parent_id');
+ $table->uuid('child_id');
+ $table->foreign('parent_id')->references('role_test_id')->on($tableRoles);
+ $table->foreign('child_id')->references('role_test_id')->on($tableRoles);
+ });
+ }
+
+ /** @test
+ * @dataProvider roles_list
+ */
+ #[DataProvider('roles_list')]
+ #[Test]
+ public function it_returns_correct_withCount_of_nested_roles($role_group, $index, $relation, $expectedCount)
+ {
+ $role = $this->$role_group[$index];
+ $count_field_name = sprintf('%s_count', $relation);
+
+ $actualCount = (int) Role::withCount($relation)->find($role->getKey())->$count_field_name;
+
+ $this->assertSame(
+ $expectedCount,
+ $actualCount,
+ sprintf('%s expects %d %s, %d found', $role->name, $expectedCount, $relation, $actualCount)
+ );
+ }
+
+ public static function roles_list()
+ {
+ return [
+ ['parent_roles', 'has_no_children', 'children', 0],
+ ['parent_roles', 'has_1_child', 'children', 1],
+ ['parent_roles', 'has_3_children', 'children', 3],
+ ['child_roles', 'has_no_parents', 'parents', 0],
+ ['child_roles', 'has_1_parent', 'parents', 1],
+ ['child_roles', 'has_2_parents', 'parents', 2],
+ ];
+ }
+}
diff --git a/tests/RouteTest.php b/tests/RouteTest.php
index cf76861b8..7b213eee8 100644
--- a/tests/RouteTest.php
+++ b/tests/RouteTest.php
@@ -1,55 +1,49 @@
isVersionAvailable()) {
- $this->markTestSkipped(
- 'This feature available for Laravel 5.5 and higher'
- );
- }
- }
-
/** @test */
+ #[Test]
public function test_role_function()
{
$router = $this->getRouter();
$router->get('role-test', $this->getRouteResponse())
- ->name('role.test')
- ->role('superadmin');
+ ->name('role.test')
+ ->role('superadmin');
$this->assertEquals(['role:superadmin'], $this->getLastRouteMiddlewareFromRouter($router));
}
/** @test */
+ #[Test]
public function test_permission_function()
{
$router = $this->getRouter();
$router->get('permission-test', $this->getRouteResponse())
- ->name('permission.test')
- ->permission(['edit articles', 'save articles']);
+ ->name('permission.test')
+ ->permission(['edit articles', 'save articles']);
$this->assertEquals(['permission:edit articles|save articles'], $this->getLastRouteMiddlewareFromRouter($router));
}
/** @test */
+ #[Test]
public function test_role_and_permission_function_together()
{
$router = $this->getRouter();
$router->get('role-permission-test', $this->getRouteResponse())
- ->name('role-permission.test')
- ->role('superadmin|admin')
- ->permission('create user|edit user');
+ ->name('role-permission.test')
+ ->role('superadmin|admin')
+ ->permission('create user|edit user');
$this->assertEquals(
[
@@ -60,25 +54,65 @@ public function test_role_and_permission_function_together()
);
}
- protected function isVersionAvailable()
+ /**
+ * @test
+ *
+ * @requires PHP 8.1.0
+ */
+ #[RequiresPhp('>= 8.1.0')]
+ #[Test]
+ public function test_role_function_with_backed_enum()
{
- return app()->version() >= '5.5';
- }
+ $router = $this->getRouter();
- protected function getLastRouteMiddlewareFromRouter($router)
- {
- return last($router->getRoutes()->get())->middleware();
+ $router->get('role-test.enum', $this->getRouteResponse())
+ ->name('role.test.enum')
+ ->role(TestRolePermissionsEnum::USERMANAGER);
+
+ $this->assertEquals(['role:'.TestRolePermissionsEnum::USERMANAGER->value], $this->getLastRouteMiddlewareFromRouter($router));
}
- protected function getRouter()
+ /**
+ * @test
+ *
+ * @requires PHP 8.1.0
+ */
+ #[RequiresPhp('>= 8.1.0')]
+ #[Test]
+ public function test_permission_function_with_backed_enum()
{
- return app('router');
+ $router = $this->getRouter();
+
+ $router->get('permission-test.enum', $this->getRouteResponse())
+ ->name('permission.test.enum')
+ ->permission(TestRolePermissionsEnum::WRITER);
+
+ $expected = ['permission:'.TestRolePermissionsEnum::WRITER->value];
+ $this->assertEquals($expected, $this->getLastRouteMiddlewareFromRouter($router));
}
- protected function getRouteResponse()
+ /**
+ * @test
+ *
+ * @requires PHP 8.1.0
+ */
+ #[RequiresPhp('>= 8.1.0')]
+ #[Test]
+ public function test_role_and_permission_function_together_with_backed_enum()
{
- return function () {
- return (new Response())->setContent('');
- };
+ $router = $this->getRouter();
+
+ $router->get('roles-permissions-test.enum', $this->getRouteResponse())
+ ->name('roles-permissions.test.enum')
+ ->role([TestRolePermissionsEnum::USERMANAGER, TestRolePermissionsEnum::ADMIN])
+ ->permission([TestRolePermissionsEnum::WRITER, TestRolePermissionsEnum::EDITOR]);
+
+ $this->assertEquals(
+ [
+ 'role:'.TestRolePermissionsEnum::USERMANAGER->value.'|'.TestRolePermissionsEnum::ADMIN->value,
+ 'permission:'.TestRolePermissionsEnum::WRITER->value.'|'.TestRolePermissionsEnum::EDITOR->value,
+ ],
+ $this->getLastRouteMiddlewareFromRouter($router)
+ );
}
}
diff --git a/tests/TeamHasPermissionsTest.php b/tests/TeamHasPermissionsTest.php
new file mode 100644
index 000000000..1e3ab20af
--- /dev/null
+++ b/tests/TeamHasPermissionsTest.php
@@ -0,0 +1,134 @@
+testUser->givePermissionTo('edit-articles', 'edit-news');
+
+ setPermissionsTeamId(2);
+ $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 */
+ #[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->assignRole('testRole');
+ $this->testUser->givePermissionTo('edit-news');
+
+ setPermissionsTeamId(2);
+ $this->testUser->assignRole('testRole');
+ $this->testUser->givePermissionTo('edit-blog');
+
+ setPermissionsTeamId(1);
+ $this->testUser->load('roles', 'permissions');
+
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-news']),
+ $this->testUser->getAllPermissions()->pluck('name')->sort()->values()
+ );
+
+ setPermissionsTeamId(2);
+ $this->testUser->load('roles', 'permissions');
+
+ $this->assertEquals(
+ collect(['edit-articles', 'edit-blog']),
+ $this->testUser->getAllPermissions()->pluck('name')->sort()->values()
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_sync_or_remove_permission_without_detach_on_different_teams()
+ {
+ setPermissionsTeamId(1);
+ $this->testUser->syncPermissions('edit-articles', 'edit-news');
+
+ setPermissionsTeamId(2);
+ $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 */
+ #[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..113bdc5aa
--- /dev/null
+++ b/tests/TeamHasRolesTest.php
@@ -0,0 +1,162 @@
+ 'user2@test.com']);
+ $user2 = User::create(['email' => 'user2@test.com']);
+
+ setPermissionsTeamId(1);
+ $user1->assignRole('testRole');
+ $user1->givePermissionTo('edit-articles');
+ $user2->assignRole('testRole');
+ $user2->givePermissionTo('edit-articles');
+ setPermissionsTeamId(2);
+ $user1->givePermissionTo('edit-news');
+
+ $this->assertDatabaseHas('model_has_permissions', [config('permission.column_names.model_morph_key') => $user1->id]);
+ $this->assertDatabaseHas('model_has_roles', [config('permission.column_names.model_morph_key') => $user1->id]);
+
+ $user1->delete();
+
+ setPermissionsTeamId(1);
+ $this->assertDatabaseMissing('model_has_permissions', [config('permission.column_names.model_morph_key') => $user1->id]);
+ $this->assertDatabaseMissing('model_has_roles', [config('permission.column_names.model_morph_key') => $user1->id]);
+ $this->assertDatabaseHas('model_has_permissions', [config('permission.column_names.model_morph_key') => $user2->id]);
+ $this->assertDatabaseHas('model_has_roles', [config('permission.column_names.model_morph_key') => $user2->id]);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_assign_same_and_different_roles_on_same_user_different_teams()
+ {
+ app(Role::class)->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->assignRole('testRole', 'testRole2');
+
+ // explicit load of roles to assert no mismatch
+ // when same role assigned in diff teams
+ // while old team's roles are loaded
+ $this->testUser->load('roles');
+
+ setPermissionsTeamId(2);
+ $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 */
+ #[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->syncRoles('testRole', 'testRole2');
+
+ setPermissionsTeamId(2);
+ $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 */
+ #[Test]
+ public function it_can_scope_users_on_different_teams()
+ {
+ User::all()->each(fn ($item) => $item->delete());
+ $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();
+ $scopedUsers3Team1 = User::withoutRole('testRole')->get();
+
+ $this->assertEquals(1, $scopedUsers1Team1->count());
+ $this->assertEquals(2, $scopedUsers2Team1->count());
+ $this->assertEquals(1, $scopedUsers3Team1->count());
+
+ setPermissionsTeamId(1);
+ $scopedUsers1Team2 = User::role($this->testUserRole)->get();
+ $scopedUsers2Team2 = User::role('testRole2')->get();
+ $scopedUsers3Team2 = User::withoutRole('testRole')->get();
+
+ $this->assertEquals(1, $scopedUsers1Team2->count());
+ $this->assertEquals(0, $scopedUsers2Team2->count());
+ $this->assertEquals(1, $scopedUsers3Team2->count());
+ }
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 8414d8b94..cc243faa1 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -1,22 +1,32 @@
prepareMigration();
+ }
+
+ // Note: this also flushes the cache from within the migration
$this->setUpDatabase($this->app);
- $this->testUser = User::first();
- $this->testUserRole = app(Role::class)->find(1);
- $this->testUserPermission = app(Permission::class)->find(1);
+ $this->setUpBaseTestPermissions($this->app);
+
+ if ($this->hasTeams) {
+ setPermissionsTeamId(1);
+ }
+
+ if ($this->usePassport) {
+ $this->setUpPassport($this->app);
+ }
- $this->testAdmin = Admin::first();
- $this->testAdminRole = app(Role::class)->find(3);
- $this->testAdminPermission = app(Permission::class)->find(4);
+ $this->setUpRoutes();
+ }
+
+ protected function tearDown(): void
+ {
+ parent::tearDown();
+
+ if (method_exists(AboutCommand::class, 'flushState')) {
+ AboutCommand::flushState();
+ }
}
/**
- * @param \Illuminate\Foundation\Application $app
- *
- * @return array
+ * @param \Illuminate\Foundation\Application $app
*/
- protected function getPackageProviders($app)
+ protected function getPackageProviders($app): array
{
- return [
+ return $this->getLaravelVersion() < 9 ? [
PermissionServiceProvider::class,
+ ] : [
+ PermissionServiceProvider::class,
+ PassportServiceProvider::class,
];
}
/**
* Set up the environment.
*
- * @param \Illuminate\Foundation\Application $app
+ * @param \Illuminate\Foundation\Application $app
*/
protected function getEnvironmentSetUp($app)
{
+ Model::preventLazyLoading();
+ $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',
+ 'driver' => 'sqlite',
'database' => ':memory:',
- 'prefix' => '',
+ '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\Tests\TestModels\Permission::class);
+ $app['config']->set('permission.models.role', \Spatie\Permission\Tests\TestModels\Role::class);
+ }
// Use test User model for users provider
$app['config']->set('auth.providers.users.model', User::class);
$app['config']->set('cache.prefix', 'spatie_tests---');
+ $app['config']->set('cache.default', getenv('CACHE_DRIVER') ?: 'array');
+
+ // FOR MANUAL TESTING OF ALTERNATE CACHE STORES:
+ // $app['config']->set('cache.default', 'array');
+ // Laravel supports: array, database, file
+ // requires extensions: memcached, redis, dynamodb, octane
}
/**
* Set up the database.
*
- * @param \Illuminate\Foundation\Application $app
+ * @param \Illuminate\Foundation\Application $app
*/
protected function setUpDatabase($app)
{
- $app['config']->set('permission.column_names.model_morph_key', 'model_test_id');
+ $schema = $app['db']->connection()->getSchemaBuilder();
- $app['db']->connection()->getSchemaBuilder()->create('users', function (Blueprint $table) {
+ $schema->create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('email');
$table->softDeletes();
});
- $app['db']->connection()->getSchemaBuilder()->create('admins', function (Blueprint $table) {
+ $schema->create('admins', function (Blueprint $table) {
$table->increments('id');
$table->string('email');
});
+ $schema->create('content', function (Blueprint $table) {
+ $table->increments('id');
+ $table->string('content');
+ $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
+ $table->timestamps();
+ });
+
if (Cache::getStore() instanceof \Illuminate\Cache\DatabaseStore ||
$app[PermissionRegistrar::class]->getCacheStore() instanceof \Illuminate\Cache\DatabaseStore) {
$this->createCacheTable();
}
- include_once __DIR__.'/../database/migrations/create_permission_tables.php.stub';
+ if (! $this->useCustomModels) {
+ self::$migration->up();
+ } else {
+ self::$customMigration->up();
- (new \CreatePermissionTables())->up();
+ $schema->table(config('permission.table_names.roles'), function (Blueprint $table) {
+ $table->softDeletes();
+ });
+ $schema->table(config('permission.table_names.permissions'), function (Blueprint $table) {
+ $table->softDeletes();
+ });
+ }
- 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']);
+ }
+
+ /**
+ * Set up initial roles and permissions used in many tests
+ *
+ * @param \Illuminate\Foundation\Application $app
+ */
+ protected function setUpBaseTestPermissions($app): void
+ {
+ $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']);
+ }
+
+ protected function setUpPassport($app): void
+ {
+ if ($this->getLaravelVersion() < 9) {
+ return;
+ }
+
+ $app['config']->set('permission.use_passport_client_credentials', true);
+ $app['config']->set('auth.guards.api', ['driver' => 'passport', 'provider' => 'users']);
+
+ // mimic passport:install (must load migrations using our own call to loadMigrationsFrom() else rollbacks won't occur, and migrations will be left in skeleton directory
+ // $this->artisan('passport:keys');
+ $this->loadMigrationsFrom(__DIR__.'/../vendor/laravel/passport/database/migrations/');
+ $provider = in_array('users', array_keys(config('auth.providers'))) ? 'users' : null;
+ $this->artisan('passport:client', ['--personal' => true, '--name' => config('app.name').' Personal Access Client']);
+ $this->artisan('passport:client', ['--password' => true, '--name' => config('app.name').' Password Grant Client', '--provider' => $provider]);
+
+ $this->testClient = Client::create(['name' => 'Test', 'redirect' => 'https://example.com/', 'personal_access_client' => 0, 'password_client' => 0, 'revoked' => 0]);
+ $this->testClientRole = $app[Role::class]->create(['name' => 'clientRole', 'guard_name' => 'api']);
+ $this->testClientPermission = $app[Permission::class]->create(['name' => 'edit-posts', 'guard_name' => 'api']);
+ }
+
+ private function prepareMigration()
+ {
+ $migration = str_replace(
+ [
+ '(\'id\'); // permission id',
+ '(\'id\'); // role id',
+ 'references(\'id\') // permission id',
+ 'references(\'id\') // role id',
+ 'bigIncrements',
+ 'unsignedBigInteger($pivotRole)',
+ 'unsignedBigInteger($pivotPermission)',
+ ],
+ [
+ '(\'permission_test_id\');',
+ '(\'role_test_id\');',
+ 'references(\'permission_test_id\')',
+ 'references(\'role_test_id\')',
+ 'uuid',
+ 'uuid($pivotRole)->nullable(false)',
+ 'uuid($pivotPermission)->nullable(false)',
+ ],
+ file_get_contents(__DIR__.'/../database/migrations/create_permission_tables.php.stub')
+ );
+
+ file_put_contents(__DIR__.'/CreatePermissionCustomTables.php', $migration);
+
+ self::$migration = require __DIR__.'/../database/migrations/create_permission_tables.php.stub';
+
+ self::$customMigration = require __DIR__.'/CreatePermissionCustomTables.php';
}
- /**
- * Reload the permissions.
- */
protected function reloadPermissions()
{
app(PermissionRegistrar::class)->forgetCachedPermissions();
@@ -140,4 +285,55 @@ 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'),
+ ];
+ });
+ }
+
+ // //// TEST HELPERS
+ public function runMiddleware($middleware, $permission, $guard = null, bool $client = false)
+ {
+ $request = new Request;
+ if ($client) {
+ $request->headers->set('Authorization', 'Bearer '.str()->random(30));
+ }
+
+ try {
+ return $middleware->handle($request, function () {
+ return (new Response)->setContent('');
+ }, $permission, $guard)->status();
+ } catch (UnauthorizedException $e) {
+ return $e->getStatusCode();
+ }
+ }
+
+ public function getLastRouteMiddlewareFromRouter($router)
+ {
+ return last($router->getRoutes()->get())->middleware();
+ }
+
+ public function getRouter()
+ {
+ return app('router');
+ }
+
+ public function getRouteResponse()
+ {
+ return function () {
+ return (new Response)->setContent('');
+ };
+ }
+
+ protected function getLaravelVersion()
+ {
+ return (float) app()->version();
+ }
}
diff --git a/tests/TestHelper.php b/tests/TestHelper.php
index 2ad3b2a5e..5eddab214 100644
--- a/tests/TestHelper.php
+++ b/tests/TestHelper.php
@@ -1,6 +1,6 @@
handle(new Request(), function () {
- return (new Response())->setContent('');
+ return $middleware->handle(new Request, function () {
+ return (new Response)->setContent('');
}, $parameter)->status();
} catch (HttpException $e) {
return $e->getStatusCode();
diff --git a/tests/TestModels/Admin.php b/tests/TestModels/Admin.php
new file mode 100644
index 000000000..c2aa1c85f
--- /dev/null
+++ b/tests/TestModels/Admin.php
@@ -0,0 +1,10 @@
+{$model->getKeyName()})) {
+ $model->{$model->getKeyName()} = Str::uuid()->toString();
+ }
+ });
+ }
+
+ public function getIncrementing(): bool
+ {
+ return false;
+ }
+
+ public function getKeyType(): string
+ {
+ return 'string';
+ }
+}
diff --git a/tests/TestModels/Role.php b/tests/TestModels/Role.php
new file mode 100644
index 000000000..a5d4a2702
--- /dev/null
+++ b/tests/TestModels/Role.php
@@ -0,0 +1,70 @@
+attributes['name'];
+
+ if (str_contains($name, 'casted_enum')) {
+ return TestRolePermissionsEnum::from($name);
+ }
+
+ return $name;
+ }
+
+ public function parents(): BelongsToMany
+ {
+ return $this->belongsToMany(
+ static::class,
+ static::HIERARCHY_TABLE,
+ 'child_id',
+ 'parent_id');
+ }
+
+ public function children(): BelongsToMany
+ {
+ return $this->belongsToMany(
+ static::class,
+ static::HIERARCHY_TABLE,
+ 'parent_id',
+ 'child_id');
+ }
+
+ protected static function boot()
+ {
+ parent::boot();
+ static::creating(static function ($model) {
+ if (empty($model->{$model->getKeyName()})) {
+ $model->{$model->getKeyName()} = Str::uuid()->toString();
+ }
+ });
+ }
+
+ public function getIncrementing(): bool
+ {
+ return false;
+ }
+
+ public function getKeyType(): string
+ {
+ return 'string';
+ }
+}
diff --git a/tests/TestModels/RuntimeRole.php b/tests/TestModels/RuntimeRole.php
new file mode 100644
index 000000000..a1740434a
--- /dev/null
+++ b/tests/TestModels/RuntimeRole.php
@@ -0,0 +1,11 @@
+value when specifying the role/permission name
+ *
+ * In your application code, when checking for authorization, you can use MyEnum::NAME in most cases.
+ * You can always manually fallback to MyEnum::NAME->value when using features that aren't aware of Enum support.
+ *
+ * TestRolePermissionsEnum::USERMANAGER->name = 'USERMANAGER'
+ * TestRolePermissionsEnum::USERMANAGER->value = 'User Manager' <-- This is the role-name checked by this package
+ * TestRolePermissionsEnum::USERMANAGER->label() = 'Manage Users'
+ */
+enum TestRolePermissionsEnum: string
+{
+ // case NAME = 'value';
+ // case NAMEINAPP = 'name-in-database';
+
+ case WRITER = 'writer';
+ case EDITOR = 'editor';
+ case USERMANAGER = 'user-manager';
+ case ADMIN = 'administrator';
+ case CASTED_ENUM_1 = 'casted_enum-1';
+ case CASTED_ENUM_2 = 'casted_enum-2';
+
+ case VIEWARTICLES = 'view articles';
+ case EDITARTICLES = 'edit articles';
+
+ case WildcardArticlesCreator = 'articles.edit,view,create';
+ case WildcardNewsEverything = 'news.*';
+ case WildcardPostsEverything = 'posts.*';
+
+ case WildcardPostsCreate = 'posts.create';
+ case WildcardArticlesView = 'articles.view';
+ case WildcardProjectsView = 'projects.view';
+
+ // extra helper to allow for greater customization of displayed values, without disclosing the name/value data directly
+ public function label(): string
+ {
+ return match ($this) {
+ self::WRITER => 'Writers',
+ self::EDITOR => 'Editors',
+ self::USERMANAGER => 'User Managers',
+ self::ADMIN => 'Admins',
+
+ self::VIEWARTICLES => 'View Articles',
+ self::EDITARTICLES => 'Edit Articles',
+
+ default => Str::words($this->value),
+ };
+ }
+}
diff --git a/tests/TestModels/User.php b/tests/TestModels/User.php
new file mode 100644
index 000000000..288f5f5f2
--- /dev/null
+++ b/tests/TestModels/User.php
@@ -0,0 +1,10 @@
+set('permission.enable_wildcard_permission', true);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $permission1 = Permission::create(['name' => 'articles.edit,view,create']);
+ $permission2 = Permission::create(['name' => 'news.*']);
+ $permission3 = Permission::create(['name' => 'posts.*']);
+
+ $user1->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo('posts.create'));
+ $this->assertTrue($user1->hasPermissionTo('posts.create.123'));
+ $this->assertTrue($user1->hasPermissionTo('posts.*'));
+ $this->assertTrue($user1->hasPermissionTo('articles.view'));
+ $this->assertFalse($user1->hasPermissionTo('projects.view'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_wildcard_permission_for_a_non_default_guard()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $permission1 = Permission::create(['name' => 'articles.edit,view,create', 'guard_name' => 'api']);
+ $permission2 = Permission::create(['name' => 'news.*', 'guard_name' => 'api']);
+ $permission3 = Permission::create(['name' => 'posts.*', 'guard_name' => 'api']);
+
+ $user1->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo('posts.create', 'api'));
+ $this->assertTrue($user1->hasPermissionTo('posts.create.123', 'api'));
+ $this->assertTrue($user1->hasPermissionTo('posts.*', 'api'));
+ $this->assertTrue($user1->hasPermissionTo('articles.view', 'api'));
+ $this->assertFalse($user1->hasPermissionTo('projects.view', 'api'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_wildcard_permission_from_instance_without_explicit_guard_argument()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $permission2 = Permission::create(['name' => 'articles.view']);
+ $permission1 = Permission::create(['name' => 'articles.edit', 'guard_name' => 'api']);
+ $permission3 = Permission::create(['name' => 'news.*', 'guard_name' => 'api']);
+ $permission4 = Permission::create(['name' => 'posts.*', 'guard_name' => 'api']);
+
+ $user1->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo($permission1));
+ $this->assertTrue($user1->hasPermissionTo($permission2));
+ $this->assertTrue($user1->hasPermissionTo($permission3));
+ $this->assertFalse($user1->hasPermissionTo($permission4));
+ $this->assertFalse($user1->hasPermissionTo('articles.edit'));
+ }
+
+ /**
+ * @test
+ *
+ * @requires PHP >= 8.1
+ */
+ #[RequiresPhp('>= 8.1')]
+ #[Test]
+ public function it_can_assign_wildcard_permissions_using_enums()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $articlesCreator = TestModels\TestRolePermissionsEnum::WildcardArticlesCreator;
+ $newsEverything = TestModels\TestRolePermissionsEnum::WildcardNewsEverything;
+ $postsEverything = TestModels\TestRolePermissionsEnum::WildcardPostsEverything;
+ $postsCreate = TestModels\TestRolePermissionsEnum::WildcardPostsCreate;
+
+ $permission1 = app(Permission::class)->findOrCreate($articlesCreator->value, 'web');
+ $permission2 = app(Permission::class)->findOrCreate($newsEverything->value, 'web');
+ $permission3 = app(Permission::class)->findOrCreate($postsEverything->value, 'web');
+
+ $user1->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo($postsCreate));
+ $this->assertTrue($user1->hasPermissionTo($postsCreate->value.'.123'));
+ $this->assertTrue($user1->hasPermissionTo($postsEverything));
+
+ $this->assertTrue($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardArticlesView));
+ $this->assertTrue($user1->hasAnyPermission(TestModels\TestRolePermissionsEnum::WildcardArticlesView));
+
+ $this->assertFalse($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardProjectsView));
+
+ $user1->revokePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertFalse($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardPostsCreate));
+ $this->assertFalse($user1->hasPermissionTo($postsCreate->value.'.123'));
+ $this->assertFalse($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardPostsEverything));
+
+ $this->assertFalse($user1->hasPermissionTo(TestModels\TestRolePermissionsEnum::WildcardArticlesView));
+ $this->assertFalse($user1->hasAnyPermission(TestModels\TestRolePermissionsEnum::WildcardArticlesView));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_wildcard_permissions_via_roles()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $user1->assignRole('testRole');
+
+ $permission1 = Permission::create(['name' => 'articles,projects.edit,view,create']);
+ $permission2 = Permission::create(['name' => 'news.*.456']);
+ $permission3 = Permission::create(['name' => 'posts']);
+
+ $this->testUserRole->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo('posts.create'));
+ $this->assertTrue($user1->hasPermissionTo('news.create.456'));
+ $this->assertTrue($user1->hasPermissionTo('projects.create'));
+ $this->assertTrue($user1->hasPermissionTo('articles.view'));
+ $this->assertFalse($user1->hasPermissionTo('articles.list'));
+ $this->assertFalse($user1->hasPermissionTo('projects.list'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_custom_wildcard_permission()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+ app('config')->set('permission.wildcard_permission', WildcardPermission::class);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $permission1 = Permission::create(['name' => 'articles:edit;view;create']);
+ $permission2 = Permission::create(['name' => 'news:@']);
+ $permission3 = Permission::create(['name' => 'posts:@']);
+
+ $user1->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo('posts:create'));
+ $this->assertTrue($user1->hasPermissionTo('posts:create:123'));
+ $this->assertTrue($user1->hasPermissionTo('posts:@'));
+ $this->assertTrue($user1->hasPermissionTo('articles:view'));
+ $this->assertFalse($user1->hasPermissionTo('posts.*'));
+ $this->assertFalse($user1->hasPermissionTo('articles.view'));
+ $this->assertFalse($user1->hasPermissionTo('projects:view'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_custom_wildcard_permissions_via_roles()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+ app('config')->set('permission.wildcard_permission', WildcardPermission::class);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $user1->assignRole('testRole');
+
+ $permission1 = Permission::create(['name' => 'articles;projects:edit;view;create']);
+ $permission2 = Permission::create(['name' => 'news:@:456']);
+ $permission3 = Permission::create(['name' => 'posts']);
+
+ $this->testUserRole->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo('posts:create'));
+ $this->assertTrue($user1->hasPermissionTo('news:create:456'));
+ $this->assertTrue($user1->hasPermissionTo('projects:create'));
+ $this->assertTrue($user1->hasPermissionTo('articles:view'));
+ $this->assertFalse($user1->hasPermissionTo('news.create.456'));
+ $this->assertFalse($user1->hasPermissionTo('projects.create'));
+ $this->assertFalse($user1->hasPermissionTo('articles:list'));
+ $this->assertFalse($user1->hasPermissionTo('projects:list'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_check_non_wildcard_permissions()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $permission1 = Permission::create(['name' => 'edit articles']);
+ $permission2 = Permission::create(['name' => 'create news']);
+ $permission3 = Permission::create(['name' => 'update comments']);
+
+ $user1->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo('edit articles'));
+ $this->assertTrue($user1->hasPermissionTo('create news'));
+ $this->assertTrue($user1->hasPermissionTo('update comments'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_verify_complex_wildcard_permissions()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $permission1 = Permission::create(['name' => '*.create,update,delete.*.test,course,finance']);
+ $permission2 = Permission::create(['name' => 'papers,posts,projects,orders.*.test,test1,test2.*']);
+ $permission3 = Permission::create(['name' => 'User::class.create,edit,view']);
+
+ $user1->givePermissionTo([$permission1, $permission2, $permission3]);
+
+ $this->assertTrue($user1->hasPermissionTo('invoices.delete.367463.finance'));
+ $this->assertTrue($user1->hasPermissionTo('projects.update.test2.test3'));
+ $this->assertTrue($user1->hasPermissionTo('User::class.edit'));
+ $this->assertFalse($user1->hasPermissionTo('User::class.delete'));
+ $this->assertFalse($user1->hasPermissionTo('User::class.*'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_throws_exception_when_wildcard_permission_is_not_properly_formatted()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user1 = User::create(['email' => 'user1@test.com']);
+
+ $permission = Permission::create(['name' => '*..']);
+
+ $user1->givePermissionTo([$permission]);
+
+ $this->expectException(WildcardPermissionNotProperlyFormatted::class);
+
+ $user1->hasPermissionTo('invoices.*');
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_verify_permission_instances_not_assigned_to_user()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user = User::create(['email' => 'user@test.com']);
+
+ $userPermission = Permission::create(['name' => 'posts.*']);
+ $permissionToVerify = Permission::create(['name' => 'posts.create']);
+
+ $user->givePermissionTo([$userPermission]);
+
+ $this->assertTrue($user->hasPermissionTo('posts.create'));
+ $this->assertTrue($user->hasPermissionTo('posts.create.123'));
+ $this->assertTrue($user->hasPermissionTo($permissionToVerify->id));
+ $this->assertTrue($user->hasPermissionTo($permissionToVerify));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_verify_permission_instances_assigned_to_user()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user = User::create(['email' => 'user@test.com']);
+
+ $userPermission = Permission::create(['name' => 'posts.*']);
+ $permissionToVerify = Permission::create(['name' => 'posts.create']);
+
+ $user->givePermissionTo([$userPermission, $permissionToVerify]);
+
+ $this->assertTrue($user->hasPermissionTo('posts.create'));
+ $this->assertTrue($user->hasPermissionTo('posts.create.123'));
+ $this->assertTrue($user->hasPermissionTo($permissionToVerify));
+ $this->assertTrue($user->hasPermissionTo($userPermission));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_verify_integers_as_strings()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user = User::create(['email' => 'user@test.com']);
+
+ $userPermission = Permission::create(['name' => '8']);
+
+ $user->givePermissionTo([$userPermission]);
+
+ $this->assertTrue($user->hasPermissionTo('8'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_throws_exception_when_permission_has_invalid_arguments()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user = User::create(['email' => 'user@test.com']);
+
+ $this->expectException(WildcardPermissionInvalidArgument::class);
+
+ $user->hasPermissionTo(['posts.create']);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_throws_exception_when_permission_id_not_exists()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $user = User::create(['email' => 'user@test.com']);
+
+ $this->expectException(PermissionDoesNotExist::class);
+
+ $user->hasPermissionTo(6);
+ }
+}
diff --git a/tests/WildcardMiddlewareTest.php b/tests/WildcardMiddlewareTest.php
new file mode 100644
index 000000000..9bb06873b
--- /dev/null
+++ b/tests/WildcardMiddlewareTest.php
@@ -0,0 +1,166 @@
+roleMiddleware = new RoleMiddleware;
+
+ $this->permissionMiddleware = new PermissionMiddleware;
+
+ $this->roleOrPermissionMiddleware = new RoleOrPermissionMiddleware;
+
+ app('config')->set('permission.enable_wildcard_permission', true);
+ }
+
+ /** @test */
+ #[Test]
+ public function a_guest_cannot_access_a_route_protected_by_the_permission_middleware()
+ {
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'articles.edit')
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function a_user_can_access_a_route_protected_by_permission_middleware_if_have_this_permission()
+ {
+ Auth::login($this->testUser);
+
+ Permission::create(['name' => 'articles']);
+
+ $this->testUser->givePermissionTo('articles');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'articles.edit')
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function a_user_can_access_a_route_protected_by_this_permission_middleware_if_have_one_of_the_permissions()
+ {
+ Auth::login($this->testUser);
+
+ Permission::create(['name' => 'articles.*.test']);
+
+ $this->testUser->givePermissionTo('articles.*.test');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, 'news.edit|articles.create.test')
+ );
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->permissionMiddleware, ['news.edit', 'articles.create.test'])
+ );
+ }
+
+ /** @test */
+ #[Test]
+ public function a_user_cannot_access_a_route_protected_by_the_permission_middleware_if_have_a_different_permission()
+ {
+ Auth::login($this->testUser);
+
+ Permission::create(['name' => 'articles.*']);
+
+ $this->testUser->givePermissionTo('articles.*');
+
+ $this->assertEquals(
+ 403,
+ $this->runMiddleware($this->permissionMiddleware, 'news.edit')
+ );
+ }
+
+ /** @test */
+ #[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, 'articles.edit|news.edit')
+ );
+ }
+
+ /** @test */
+ #[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);
+
+ Permission::create(['name' => 'articles.*']);
+
+ $this->testUser->assignRole('testRole');
+ $this->testUser->givePermissionTo('articles.*');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|news.edit|articles.create')
+ );
+
+ $this->testUser->removeRole('testRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit')
+ );
+
+ $this->testUser->revokePermissionTo('articles.*');
+ $this->testUser->assignRole('testRole');
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, 'testRole|articles.edit')
+ );
+
+ $this->assertEquals(
+ 200,
+ $this->runMiddleware($this->roleOrPermissionMiddleware, ['testRole', 'articles.edit'])
+ );
+ }
+
+ /** @test */
+ #[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('');
+ }, 'permission.some');
+ } catch (UnauthorizedException $e) {
+ $requiredPermissions = $e->getRequiredPermissions();
+ }
+
+ $this->assertEquals(['permission.some'], $requiredPermissions);
+ }
+}
diff --git a/tests/WildcardRoleTest.php b/tests/WildcardRoleTest.php
new file mode 100644
index 000000000..d0e9212f5
--- /dev/null
+++ b/tests/WildcardRoleTest.php
@@ -0,0 +1,112 @@
+set('permission.enable_wildcard_permission', true);
+
+ Permission::create(['name' => 'other-permission']);
+
+ Permission::create(['name' => 'wrong-guard-permission', 'guard_name' => 'admin']);
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_be_given_a_permission()
+ {
+ Permission::create(['name' => 'posts.*']);
+ $this->testUserRole->givePermissionTo('posts.*');
+
+ $this->assertTrue($this->testUserRole->hasPermissionTo('posts.create'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_be_given_multiple_permissions_using_an_array()
+ {
+ Permission::create(['name' => 'posts.*']);
+ Permission::create(['name' => 'news.*']);
+
+ $this->testUserRole->givePermissionTo(['posts.*', 'news.*']);
+
+ $this->assertTrue($this->testUserRole->hasPermissionTo('posts.create'));
+ $this->assertTrue($this->testUserRole->hasPermissionTo('news.create'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_be_given_multiple_permissions_using_multiple_arguments()
+ {
+ Permission::create(['name' => 'posts.*']);
+ Permission::create(['name' => 'news.*']);
+
+ $this->testUserRole->givePermissionTo('posts.*', 'news.*');
+
+ $this->assertTrue($this->testUserRole->hasPermissionTo('posts.edit.123'));
+ $this->assertTrue($this->testUserRole->hasPermissionTo('news.view.1'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_can_be_given_a_permission_using_objects()
+ {
+ $this->testUserRole->givePermissionTo($this->testUserPermission);
+
+ $this->assertTrue($this->testUserRole->hasPermissionTo($this->testUserPermission));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_returns_false_if_it_does_not_have_the_permission()
+ {
+ $this->assertFalse($this->testUserRole->hasPermissionTo('other-permission'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_returns_false_if_permission_does_not_exists()
+ {
+ $this->assertFalse($this->testUserRole->hasPermissionTo('doesnt-exist'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_returns_false_if_it_does_not_have_a_permission_object()
+ {
+ $permission = app(Permission::class)->findByName('other-permission');
+
+ $this->assertFalse($this->testUserRole->hasPermissionTo($permission));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_creates_permission_object_with_findOrCreate_if_it_does_not_have_a_permission_object()
+ {
+ $permission = app(Permission::class)->findOrCreate('another-permission');
+
+ $this->assertFalse($this->testUserRole->hasPermissionTo($permission));
+
+ $this->testUserRole->givePermissionTo($permission);
+
+ $this->testUserRole = $this->testUserRole->fresh();
+
+ $this->assertTrue($this->testUserRole->hasPermissionTo('another-permission'));
+ }
+
+ /** @test */
+ #[Test]
+ public function it_returns_false_when_a_permission_of_the_wrong_guard_is_passed_in()
+ {
+ $permission = app(Permission::class)->findByName('wrong-guard-permission', 'admin');
+
+ $this->assertFalse($this->testUserRole->hasPermissionTo($permission));
+ }
+}
diff --git a/tests/WildcardRouteTest.php b/tests/WildcardRouteTest.php
new file mode 100644
index 000000000..65e3c437c
--- /dev/null
+++ b/tests/WildcardRouteTest.php
@@ -0,0 +1,45 @@
+set('permission.enable_wildcard_permission', true);
+
+ $router = $this->getRouter();
+
+ $router->get('permission-test', $this->getRouteResponse())
+ ->name('permission.test')
+ ->permission(['articles.edit', 'articles.save']);
+
+ $this->assertEquals(['permission:articles.edit|articles.save'], $this->getLastRouteMiddlewareFromRouter($router));
+ }
+
+ /** @test */
+ #[Test]
+ public function test_role_and_permission_function_together()
+ {
+ app('config')->set('permission.enable_wildcard_permission', true);
+
+ $router = $this->getRouter();
+
+ $router->get('role-permission-test', $this->getRouteResponse())
+ ->name('role-permission.test')
+ ->role('superadmin|admin')
+ ->permission('user.create|user.edit');
+
+ $this->assertEquals(
+ [
+ 'role:superadmin|admin',
+ 'permission:user.create|user.edit',
+ ],
+ $this->getLastRouteMiddlewareFromRouter($router)
+ );
+ }
+}
diff --git a/tests/resources/views/can.blade.php b/tests/resources/views/can.blade.php
index 81d526906..f0c6c8214 100644
--- a/tests/resources/views/can.blade.php
+++ b/tests/resources/views/can.blade.php
@@ -1,4 +1,4 @@
-@can($permission)
+@can($permission, $guard ?? null)
has permission
@else
does not have permission
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
diff --git a/tests/resources/views/haspermission.blade.php b/tests/resources/views/haspermission.blade.php
new file mode 100644
index 000000000..7dd4e7903
--- /dev/null
+++ b/tests/resources/views/haspermission.blade.php
@@ -0,0 +1,7 @@
+@haspermission($permission, $guard ?? null)
+has permission
+@elsehaspermission($elsepermission, $guard ?? null)
+has else permission
+@else
+does not have permission
+@endhaspermission