Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.79% covered (warning)
78.79%
26 / 33
42.86% covered (danger)
42.86%
3 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
PermissionService
78.79% covered (warning)
78.79%
26 / 33
42.86% covered (danger)
42.86%
3 / 7
30.97
0.00% covered (danger)
0.00%
0 / 1
 groups
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 abilities
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 label
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 enforced
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allows
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 abilityForRoute
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
7.10
 abilityForPath
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
7.05
1<?php
2
3namespace App\Services\Permission;
4
5use Illuminate\Support\Str;
6
7/**
8 * The one place that answers "is this staff member allowed to X".
9 *
10 *   permissions()->allows('manage_products')
11 *   permissions()->abilityForRoute('admin.product.index')   // -> 'manage_products' | null
12 *
13 * Rules (in order):
14 *   1. `rbac` feature OFF                -> allowed (single-owner shop)
15 *   2. user id 1 or has the "Owner" role -> allowed (bypass — see AppServiceProvider Gate::before)
16 *   3. otherwise -> the user's role must grant the ability
17 */
18class PermissionService
19{
20    /** @return array<string, array<string, array>> group => (ability => def) */
21    public function groups(): array
22    {
23        return config('permissions', []);
24    }
25
26    /** Flat list of every ability key. */
27    public function abilities(): array
28    {
29        return collect($this->groups())->flatMap(fn ($g) => array_keys($g))->all();
30    }
31
32    public function label(string $ability): string
33    {
34        foreach ($this->groups() as $abilities) {
35            if (isset($abilities[$ability])) {
36                return $abilities[$ability]['label'];
37            }
38        }
39
40        return Str::headline($ability);
41    }
42
43    public function enforced(): bool
44    {
45        return (bool) feature('rbac');
46    }
47
48    public function allows(string $ability, $user = null): bool
49    {
50        if (! $this->enforced()) {
51            return true;
52        }
53
54        $user ??= auth()->user();
55        if (! $user) {
56            return false;
57        }
58
59        if ((int) $user->id === 1 || $user->hasRole('Owner')) {
60            return true;
61        }
62
63        return $user->hasPermissionTo($ability);
64    }
65
66    /** Which ability (if any) guards a route name? First matching prefix wins. */
67    public function abilityForRoute(?string $routeName): ?string
68    {
69        if (! $routeName) {
70            return null;
71        }
72
73        foreach ($this->groups() as $abilities) {
74            foreach ($abilities as $key => $def) {
75                foreach ($def['routes'] ?? [] as $prefix) {
76                    if (Str::startsWith($routeName, $prefix) || $routeName === rtrim($prefix, '.')) {
77                        return $key;
78                    }
79                }
80            }
81        }
82
83        return null;
84    }
85
86    /**
87     * Which ability guards a URL / path? Route names in this app mirror paths
88     * ("admin.product." -> "/admin/product"), so we translate and prefix-match.
89     * Used to gate menu items that only carry a url.
90     */
91    public function abilityForPath(?string $url): ?string
92    {
93        if (! $url) {
94            return null;
95        }
96
97        $path = '/' . ltrim(parse_url($url, PHP_URL_PATH) ?? '', '/');
98
99        foreach ($this->groups() as $abilities) {
100            foreach ($abilities as $key => $def) {
101                foreach ($def['routes'] ?? [] as $routePrefix) {
102                    $p = '/' . str_replace('.', '/', rtrim($routePrefix, '.'));
103                    if ($path === $p || Str::startsWith($path, $p . '/')) {
104                        return $key;
105                    }
106                }
107            }
108        }
109
110        return null;
111    }
112}