Repository navigation
enhancement: Site Editor RBAC Integration with CMS Framework #220
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Check Gate If Defined Middleware. | ||
| * | ||
| * Only performs an authorization check when the gate has been | ||
| * explicitly registered. When the gate is not defined (e.g. the | ||
| * cms-framework is not installed), the request passes through | ||
| * without any authorization check (graceful degradation). | ||
| * | ||
| * @package ArtisanPack_UI | ||
| * @subpackage VisualEditor\Http\Middleware | ||
| * | ||
| * @author Jacob Martella <me@jacobmartella.com> | ||
| * | ||
| * @since 1.0.0 | ||
| */ | ||
|
|
||
| declare( strict_types=1 ); | ||
|
|
||
| namespace ArtisanPackUI\VisualEditor\Http\Middleware; | ||
|
|
||
| use Closure; | ||
| use Illuminate\Http\Request; | ||
| use Illuminate\Support\Facades\Gate; | ||
| use Symfony\Component\HttpFoundation\Response; | ||
|
|
||
| /** | ||
| * Middleware that checks an authorization gate only when it is defined. | ||
| * | ||
| * @package ArtisanPack_UI | ||
| * @subpackage VisualEditor\Http\Middleware | ||
| * | ||
| * @since 1.0.0 | ||
| */ | ||
| class CheckGateIfDefined | ||
| { | ||
| /** | ||
| * Handle an incoming request. | ||
| * | ||
| * @since 1.0.0 | ||
| * | ||
| * @param Request $request The incoming request. | ||
| * @param Closure $next The next middleware. | ||
| * @param string $ability The gate ability to check. | ||
| * | ||
| * @return Response | ||
| */ | ||
| public function handle( Request $request, Closure $next, string $ability ): Response | ||
| { | ||
| if ( Gate::has( $ability ) ) { | ||
| if ( ! $request->user() || ! $request->user()->can( $ability ) ) { | ||
| abort( 403 ); | ||
| } | ||
| } | ||
|
|
||
| return $next( $request ); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,9 @@ | |
|
|
||
| namespace ArtisanPackUI\VisualEditor\Livewire\SiteEditor; | ||
|
|
||
| use Illuminate\Contracts\Auth\Authenticatable; | ||
| use Illuminate\Contracts\View\View; | ||
| use Illuminate\Support\Facades\Auth; | ||
| use Illuminate\Support\Facades\Gate; | ||
| use Livewire\Attributes\Layout; | ||
| use Livewire\Component; | ||
|
|
@@ -67,6 +69,8 @@ public function getCards(): array | |
| { | ||
| $prefix = (string) config( 'artisanpack.visual-editor.site_editor.route_prefix', 'site-editor' ); | ||
|
|
||
| $gates = (array) config( 'artisanpack.visual-editor.site_editor.gates', [] ); | ||
|
|
||
| $cards = [ | ||
| [ | ||
| 'slug' => 'global-styles', | ||
|
|
@@ -75,6 +79,7 @@ public function getCards(): array | |
| 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" /></svg>', | ||
| 'url' => url( $prefix . '/global-styles' ), | ||
| 'count' => null, | ||
| 'permission' => $gates['styles'] ?? null, | ||
| ], | ||
| [ | ||
| 'slug' => 'templates', | ||
|
|
@@ -83,6 +88,7 @@ public function getCards(): array | |
| 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /></svg>', | ||
| 'url' => url( $prefix . '/templates' ), | ||
| 'count' => $this->getTemplateCount(), | ||
| 'permission' => $gates['templates'] ?? null, | ||
| ], | ||
| [ | ||
| 'slug' => 'template-parts', | ||
|
|
@@ -91,6 +97,7 @@ public function getCards(): array | |
| 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" d="M14.25 6.087c0-.355.186-.676.401-.959.221-.29.349-.634.349-1.003 0-1.036-1.007-1.875-2.25-1.875s-2.25.84-2.25 1.875c0 .369.128.713.349 1.003.215.283.401.604.401.959v0a.64.64 0 0 1-.657.643 48.491 48.491 0 0 1-4.163-.3c.186 1.613.293 3.25.315 4.907a.656.656 0 0 1-.658.663v0c-.355 0-.676-.186-.959-.401a1.647 1.647 0 0 0-1.003-.349c-1.036 0-1.875 1.007-1.875 2.25s.84 2.25 1.875 2.25c.369 0 .713-.128 1.003-.349.283-.215.604-.401.959-.401v0c.31 0 .555.26.532.57a48.039 48.039 0 0 1-.642 5.056c1.518.19 3.058.309 4.616.354a.64.64 0 0 0 .657-.643v0c0-.355-.186-.676-.401-.959a1.647 1.647 0 0 1-.349-1.003c0-1.035 1.008-1.875 2.25-1.875 1.243 0 2.25.84 2.25 1.875 0 .369-.128.713-.349 1.003-.215.283-.4.604-.4.959v0c0 .333.277.599.61.58a48.1 48.1 0 0 0 5.427-.63 48.05 48.05 0 0 0 .582-4.717.532.532 0 0 0-.533-.57v0c-.355 0-.676.186-.959.401-.29.221-.634.349-1.003.349-1.035 0-1.875-1.007-1.875-2.25s.84-2.25 1.875-2.25c.37 0 .713.128 1.003.349.283.215.604.401.96.401v0a.656.656 0 0 0 .657-.663 48.422 48.422 0 0 0-.37-5.36c-1.886.342-3.81.574-5.766.689a.578.578 0 0 1-.61-.58v0Z" /></svg>', | ||
| 'url' => url( $prefix . '/parts' ), | ||
| 'count' => $this->getTemplatePartCount(), | ||
| 'permission' => $gates['parts'] ?? null, | ||
| ], | ||
| [ | ||
| 'slug' => 'patterns', | ||
|
|
@@ -99,10 +106,13 @@ public function getCards(): array | |
| 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25a2.25 2.25 0 0 1-2.25-2.25v-2.25Z" /></svg>', | ||
| 'url' => url( $prefix . '/patterns' ), | ||
| 'count' => null, | ||
| 'permission' => $gates['patterns'] ?? null, | ||
| ], | ||
| ]; | ||
|
|
||
| return veApplyFilters( 've.hub.cards', $cards ); | ||
| $cards = veApplyFilters( 've.hub.cards', $cards ); | ||
|
|
||
| return $this->filterCardsByPermission( $cards ); | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -119,6 +129,43 @@ public function render(): View | |
| ] ); | ||
| } | ||
|
|
||
| /** | ||
| * Filter hub cards by the current user's permissions. | ||
| * | ||
| * Cards without a permission key or with a null permission are always shown. | ||
| * Cards with a permission are only shown if the gate is not defined | ||
| * (graceful degradation) or if the user passes the gate check. | ||
| * | ||
| * @since 1.0.0 | ||
| * | ||
| * @param array<int, array<string, mixed>> $cards The hub cards to filter. | ||
| * | ||
| * @return array<int, array<string, mixed>> | ||
| */ | ||
| protected function filterCardsByPermission( array $cards ): array | ||
| { | ||
| /** @var Authenticatable|null $user */ | ||
| $user = Auth::user(); | ||
|
|
||
| return array_values( array_filter( $cards, function ( array $card ) use ( $user ): bool { | ||
| $permission = $card['permission'] ?? null; | ||
|
|
||
| if ( null === $permission || '' === $permission ) { | ||
| return true; | ||
| } | ||
|
|
||
| if ( ! Gate::has( $permission ) ) { | ||
| return true; | ||
| } | ||
|
|
||
| if ( null === $user ) { | ||
| return false; | ||
| } | ||
|
|
||
| return $user->can( $permission ); | ||
| } ) ); | ||
|
Comment on lines
+154
to
+175
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result:
So if an ability is handled only by a Spatie’s docs, for example, show doing permission checks via Sources Citations:
🏁 Script executed: # Verify the file exists and check the specific lines
if [ -f "src/Livewire/SiteEditor/HubPage.php" ]; then
echo "=== File exists. Checking lines 145-166 ==="
sed -n '145,166p' "src/Livewire/SiteEditor/HubPage.php"
else
echo "File not found at src/Livewire/SiteEditor/HubPage.php"
# Try to locate it
find . -name "HubPage.php" -type f 2>/dev/null | head -5
fiRepository: ArtisanPack-UI/visual-editor Length of output: 627 🏁 Script executed: # Check the referenced middleware file
if [ -f "src/Http/Middleware/CheckGateIfDefined.php" ]; then
echo "=== CheckGateIfDefined.php exists. Checking lines 35-58 ==="
sed -n '35,58p' "src/Http/Middleware/CheckGateIfDefined.php"
else
echo "File not found at src/Http/Middleware/CheckGateIfDefined.php"
# Try to locate it
find . -name "CheckGateIfDefined.php" -type f 2>/dev/null | head -5
fiRepository: ArtisanPack-UI/visual-editor Length of output: 659 🏁 Script executed: # Search for tests related to filterCardsByPermission
find . -name "*.php" -path "*/test*" -o -name "*Test.php" | xargs grep -l "filterCardsByPermission" 2>/dev/null || echo "No tests found for filterCardsByPermission"Repository: ArtisanPack-UI/visual-editor Length of output: 114 🏁 Script executed: # Search for Spatie Permission setup in tests
find . -name "*.php" -path "*/test*" | xargs grep -l "spatie.*permission\|Spatie.*Permission" 2>/dev/null | head -10 || echo "No Spatie Permission tests found"Repository: ArtisanPack-UI/visual-editor Length of output: 54 🏁 Script executed: # Check what permission systems are actually tested
grep -r "Gate::define\|Gate::before" --include="*.php" src/ | head -20Repository: ArtisanPack-UI/visual-editor Length of output: 54 🏁 Script executed: # Search for tests related to HubPage or CheckGateIfDefined
find . -name "*.php" -type f | xargs grep -l "HubPage\|CheckGateIfDefined" 2>/dev/null | grep -i testRepository: ArtisanPack-UI/visual-editor Length of output: 170 🏁 Script executed: # Check if there are gate definitions in test setup/bootstrap
find . -path "*/tests/*" -name "*.php" -type f | xargs grep -l "Gate::" 2>/dev/null | head -10Repository: ArtisanPack-UI/visual-editor Length of output: 621 🏁 Script executed: # Check HubPageTest.php - see what tests exist for filterCardsByPermission
cat -n ./tests/Feature/Livewire/HubPageTest.phpRepository: ArtisanPack-UI/visual-editor Length of output: 6136 🏁 Script executed: # Check GracefulDegradationTest.php
cat -n ./tests/Feature/Authorization/GracefulDegradationTest.phpRepository: ArtisanPack-UI/visual-editor Length of output: 5661 🏁 Script executed: # Check SiteEditorAuthorizationTest.php
wc -l ./tests/Feature/Authorization/SiteEditorAuthorizationTest.php && head -100 ./tests/Feature/Authorization/SiteEditorAuthorizationTest.phpRepository: ArtisanPack-UI/visual-editor Length of output: 2815
Lines 157 and 51 (middleware) both treat The test suite confirms this: 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| /** | ||
| * Get the count of registered templates. | ||
| * | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| <?php | ||
|
|
||
| /** | ||
| * Global Style Policy. | ||
| * | ||
| * Provides authorization checks for global style management | ||
| * within the site editor. Uses applyFilters for extensibility. | ||
| * | ||
| * @package ArtisanPack_UI | ||
| * @subpackage VisualEditor\Policies | ||
| * | ||
| * @author Jacob Martella <me@jacobmartella.com> | ||
| * | ||
| * @since 1.0.0 | ||
| */ | ||
|
|
||
| declare( strict_types=1 ); | ||
|
|
||
| namespace ArtisanPackUI\VisualEditor\Policies; | ||
|
|
||
| use Illuminate\Contracts\Auth\Authenticatable; | ||
|
|
||
| /** | ||
| * Policy for global style operations in the site editor. | ||
| * | ||
| * @package ArtisanPack_UI | ||
| * @subpackage VisualEditor\Policies | ||
| * | ||
| * @since 1.0.0 | ||
| */ | ||
| class GlobalStylePolicy | ||
| { | ||
| /** | ||
| * Determine whether the user can update global styles. | ||
| * | ||
| * @since 1.0.0 | ||
| * | ||
| * @param Authenticatable $user The authenticated user. | ||
| * | ||
| * @return bool | ||
| */ | ||
| public function update( Authenticatable $user ): bool | ||
| { | ||
| $permission = veApplyFilters( 've.global-style.update', 'visual-editor.manage-styles' ); | ||
|
|
||
| return $user->can( $permission ); | ||
| } | ||
|
|
||
| /** | ||
| * Determine whether the user can reset global styles to defaults. | ||
| * | ||
| * @since 1.0.0 | ||
| * | ||
| * @param Authenticatable $user The authenticated user. | ||
| * | ||
| * @return bool | ||
| */ | ||
| public function reset( Authenticatable $user ): bool | ||
| { | ||
| $permission = veApplyFilters( 've.global-style.reset', 'visual-editor.manage-styles' ); | ||
|
|
||
| return $user->can( $permission ); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.