• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / shield / 26644118751

29 May 2026 02:48PM UTC coverage: 92.884% (+0.09%) from 92.798%
26644118751

Pull #1327

github

web-flow
Merge b0150be7e into 34be62ba8
Pull Request #1327: feat: support hierarchical permission wildcards

34 of 34 new or added lines in 3 files covered. (100.0%)

2 existing lines in 1 file now uncovered.

2924 of 3148 relevant lines covered (92.88%)

26.42 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.23
/src/Authorization/Traits/Authorizable.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter Shield.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Shield\Authorization\Traits;
15

16
use CodeIgniter\I18n\Time;
17
use CodeIgniter\Shield\Authorization\AuthorizationException;
18
use CodeIgniter\Shield\Authorization\PermissionMatcher;
19
use CodeIgniter\Shield\Exceptions\LogicException;
20
use CodeIgniter\Shield\Models\GroupModel;
21
use CodeIgniter\Shield\Models\PermissionModel;
22

23
trait Authorizable
24
{
25
    protected ?array $groupCache       = null;
26
    protected ?array $permissionsCache = null;
27

28
    /**
29
     * Adds one or more groups to the current User.
30
     *
31
     * @return $this
32
     */
33
    public function addGroup(string ...$groups): self
34
    {
35
        $this->populateGroups();
24✔
36

37
        $groupCount = count($this->groupCache);
24✔
38

39
        foreach ($groups as $group) {
24✔
40
            $group = strtolower($group);
24✔
41

42
            // don't allow dupes
43
            if (in_array($group, $this->groupCache, true)) {
24✔
44
                continue;
2✔
45
            }
46

47
            /** @var GroupModel $groupModel */
48
            $groupModel = model(GroupModel::class);
24✔
49

50
            // make sure it's a valid group
51
            if (! $groupModel->isValidGroup($group)) {
24✔
52
                throw AuthorizationException::forUnknownGroup($group);
1✔
53
            }
54

55
            $this->groupCache[] = $group;
24✔
56
        }
57

58
        // Only save the results if there's anything new.
59
        if (count($this->groupCache) > $groupCount) {
23✔
60
            $this->saveGroups();
23✔
61
        }
62

63
        return $this;
23✔
64
    }
65

66
    /**
67
     * Removes one or more groups from the user.
68
     *
69
     * @return $this
70
     */
71
    public function removeGroup(string ...$groups): self
72
    {
73
        $this->populateGroups();
3✔
74

75
        foreach ($groups as &$group) {
3✔
76
            $group = strtolower($group);
3✔
77
        }
78

79
        // Remove from local cache
80
        $this->groupCache = array_diff($this->groupCache, $groups);
3✔
81

82
        // Update the database.
83
        $this->saveGroups();
3✔
84

85
        return $this;
3✔
86
    }
87

88
    /**
89
     * Given an array of groups, will update the database
90
     * so only those groups are valid for this user, removing
91
     * all groups not in this list.
92
     *
93
     * @return $this
94
     *
95
     * @throws AuthorizationException
96
     */
97
    public function syncGroups(string ...$groups): self
98
    {
99
        $this->populateGroups();
1✔
100

101
        /** @var GroupModel $groupModel */
102
        $groupModel = model(GroupModel::class);
1✔
103

104
        foreach ($groups as $group) {
1✔
105
            if (! $groupModel->isValidGroup($group)) {
1✔
UNCOV
106
                throw AuthorizationException::forUnknownGroup($group);
×
107
            }
108
        }
109

110
        $this->groupCache = $groups;
1✔
111
        $this->saveGroups();
1✔
112

113
        return $this;
1✔
114
    }
115

116
    /**
117
     * Set groups cache manually
118
     */
119
    public function setGroupsCache(array $groups): void
120
    {
121
        $this->groupCache = $groups;
7✔
122
    }
123

124
    /**
125
     * Set permissions cache manually
126
     */
127
    public function setPermissionsCache(array $permissions): void
128
    {
129
        $this->permissionsCache = $permissions;
6✔
130
    }
131

132
    /**
133
     * Returns all groups this user is a part of.
134
     */
135
    public function getGroups(): ?array
136
    {
137
        $this->populateGroups();
5✔
138

139
        return $this->groupCache;
5✔
140
    }
141

142
    /**
143
     * Returns all permissions this user has
144
     * assigned directly to them.
145
     */
146
    public function getPermissions(): ?array
147
    {
148
        $this->populatePermissions();
4✔
149

150
        return $this->permissionsCache;
4✔
151
    }
152

153
    /**
154
     * Adds one or more permissions to the current user.
155
     *
156
     * @return $this
157
     *
158
     * @throws AuthorizationException
159
     */
160
    public function addPermission(string ...$permissions): self
161
    {
162
        $this->populatePermissions();
11✔
163

164
        $configPermissions = $this->getConfigPermissions();
11✔
165

166
        $permissionCount = count($this->permissionsCache);
11✔
167

168
        foreach ($permissions as $permission) {
11✔
169
            $permission = strtolower($permission);
11✔
170

171
            // don't allow dupes
172
            if (in_array($permission, $this->permissionsCache, true)) {
11✔
173
                continue;
2✔
174
            }
175

176
            // make sure it's a valid group
177
            if (! in_array($permission, $configPermissions, true)) {
11✔
178
                throw AuthorizationException::forUnknownPermission($permission);
2✔
179
            }
180

181
            $this->permissionsCache[] = $permission;
10✔
182
        }
183

184
        // Only save the results if there's anything new.
185
        if (count($this->permissionsCache) > $permissionCount) {
9✔
186
            $this->savePermissions();
9✔
187
        }
188

189
        return $this;
9✔
190
    }
191

192
    /**
193
     * Removes one or more permissions from the current user.
194
     *
195
     * @return $this
196
     */
197
    public function removePermission(string ...$permissions): self
198
    {
199
        $this->populatePermissions();
3✔
200

201
        foreach ($permissions as &$permission) {
3✔
202
            $permission = strtolower($permission);
3✔
203
        }
204

205
        // Remove from local cache
206
        $this->permissionsCache = array_diff($this->permissionsCache, $permissions);
3✔
207

208
        // Update the database.
209
        $this->savePermissions();
3✔
210

211
        return $this;
3✔
212
    }
213

214
    /**
215
     * Given an array of permissions, will update the database
216
     * so only those permissions are valid for this user, removing
217
     * all permissions not in this list.
218
     *
219
     * @return $this
220
     *
221
     * @throws AuthorizationException
222
     */
223
    public function syncPermissions(string ...$permissions): self
224
    {
225
        $this->populatePermissions();
1✔
226

227
        $configPermissions = $this->getConfigPermissions();
1✔
228

229
        foreach ($permissions as $permission) {
1✔
230
            if (! in_array($permission, $configPermissions, true)) {
1✔
UNCOV
231
                throw AuthorizationException::forUnknownPermission($permission);
×
232
            }
233
        }
234

235
        $this->permissionsCache = $permissions;
1✔
236
        $this->savePermissions();
1✔
237

238
        return $this;
1✔
239
    }
240

241
    /**
242
     * Checks to see if the user has the permission set
243
     * directly on themselves. This disregards any groups
244
     * they are part of.
245
     */
246
    public function hasPermission(string $permission): bool
247
    {
248
        $this->populatePermissions();
5✔
249

250
        $permission = strtolower($permission);
5✔
251

252
        return in_array($permission, $this->permissionsCache, true);
5✔
253
    }
254

255
    /**
256
     * Checks user permissions and their group permissions
257
     * to see if the user has one or more permissions.
258
     *
259
     * @param string $permissions Dot-separated permission string(s), like `users.create`
260
     */
261
    public function can(string ...$permissions): bool
262
    {
263
        // Get user's permissions and store in cache
264
        $this->populatePermissions();
14✔
265

266
        // Check the groups the user belongs to
267
        $this->populateGroups();
14✔
268

269
        // Get the group matrix
270
        $matrix = setting('AuthGroups.matrix');
14✔
271

272
        foreach ($permissions as $permission) {
14✔
273
            // Permission must contain at least two dot-separated segments.
274
            if (! str_contains($permission, '.')) {
14✔
275
                throw new LogicException(
1✔
276
                    'A permission must be a dot-separated string, like `users.create`.'
1✔
277
                    . ' Invalid permission: ' . $permission,
1✔
278
                );
1✔
279
            }
280

281
            $permission = strtolower($permission);
13✔
282

283
            // Check user's permissions
284
            if (PermissionMatcher::matches($permission, $this->permissionsCache)) {
13✔
285
                return true;
8✔
286
            }
287

288
            foreach ($this->groupCache as $group) {
11✔
289
                if (isset($matrix[$group]) && PermissionMatcher::matches($permission, $matrix[$group])) {
6✔
290
                    return true;
6✔
291
                }
292
            }
293
        }
294

295
        return false;
6✔
296
    }
297

298
    /**
299
     * Checks to see if the user is a member of one
300
     * of the groups passed in.
301
     */
302
    public function inGroup(string ...$groups): bool
303
    {
304
        $this->populateGroups();
15✔
305

306
        foreach ($groups as $group) {
15✔
307
            if (in_array(strtolower($group), $this->groupCache, true)) {
15✔
308
                return true;
12✔
309
            }
310
        }
311

312
        return false;
8✔
313
    }
314

315
    /**
316
     * Used internally to populate the User groups
317
     * so we hit the database as little as possible.
318
     */
319
    private function populateGroups(): void
320
    {
321
        if (is_array($this->groupCache)) {
41✔
322
            return;
27✔
323
        }
324

325
        /** @var GroupModel $groupModel */
326
        $groupModel = model(GroupModel::class);
34✔
327

328
        $this->groupCache = $groupModel->getForUser($this);
34✔
329
    }
330

331
    /**
332
     * Used internally to populate the User permissions
333
     * so we hit the database as little as possible.
334
     */
335
    private function populatePermissions(): void
336
    {
337
        if (is_array($this->permissionsCache)) {
24✔
338
            return;
18✔
339
        }
340

341
        /** @var PermissionModel $permissionModel */
342
        $permissionModel = model(PermissionModel::class);
18✔
343

344
        $this->permissionsCache = $permissionModel->getForUser($this);
18✔
345
    }
346

347
    /**
348
     * Inserts or Updates the current groups.
349
     */
350
    private function saveGroups(): void
351
    {
352
        /** @var GroupModel $model */
353
        $model = model(GroupModel::class);
26✔
354

355
        $cache = $this->groupCache;
26✔
356

357
        $this->saveGroupsOrPermissions('group', $model, $cache);
26✔
358
    }
359

360
    /**
361
     * Inserts or Updates either the current permissions.
362
     */
363
    private function savePermissions(): void
364
    {
365
        /** @var PermissionModel $model */
366
        $model = model(PermissionModel::class);
12✔
367

368
        $cache = $this->permissionsCache;
12✔
369

370
        $this->saveGroupsOrPermissions('permission', $model, $cache);
12✔
371
    }
372

373
    /**
374
     * @param 'group'|'permission'       $type
375
     * @param GroupModel|PermissionModel $model
376
     */
377
    private function saveGroupsOrPermissions(string $type, $model, array $cache): void
378
    {
379
        $existing = $model->getForUser($this);
36✔
380

381
        $new = array_diff($cache, $existing);
36✔
382

383
        // Delete any not in the cache
384
        if ($cache !== []) {
36✔
385
            $model->deleteNotIn($this->id, $cache);
32✔
386
        }
387
        // Nothing in the cache? Then make sure
388
        // we delete all from this user
389
        else {
390
            $model->deleteAll($this->id);
6✔
391
        }
392

393
        // Insert new ones
394
        if ($new !== []) {
36✔
395
            $inserts = [];
32✔
396

397
            foreach ($new as $item) {
32✔
398
                $inserts[] = [
32✔
399
                    'user_id'    => $this->id,
32✔
400
                    $type        => $item,
32✔
401
                    'created_at' => Time::now(),
32✔
402
                ];
32✔
403
            }
404

405
            $model->insertBatch($inserts);
32✔
406
        }
407
    }
408

409
    /**
410
     * @return list<string>
411
     */
412
    private function getConfigPermissions(): array
413
    {
414
        return array_keys(setting('AuthGroups.permissions'));
12✔
415
    }
416
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc