diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 5b2c7aa1b..b6f2501b0 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -159,6 +159,12 @@ 'count' => 1, 'path' => __DIR__ . '/src/Collectors/Auth.php', ]; +$ignoreErrors[] = [ + // identifier: codeigniter.factoriesClassConstFetch + 'message' => '#^Call to function model with CodeIgniter\\\\Shield\\\\Models\\\\GroupModel\\:\\:class is discouraged\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Commands/User.php', +]; $ignoreErrors[] = [ // identifier: codeigniter.factoriesClassConstFetch 'message' => '#^Call to function model with CodeIgniter\\\\Shield\\\\Models\\\\UserModel\\:\\:class is discouraged\\.$#', @@ -259,7 +265,7 @@ $ignoreErrors[] = [ // identifier: codeigniter.factoriesClassConstFetch 'message' => '#^Call to function model with CodeIgniter\\\\Shield\\\\Models\\\\GroupModel\\:\\:class is discouraged\\.$#', - 'count' => 2, + 'count' => 4, 'path' => __DIR__ . '/src/Entities/User.php', ]; $ignoreErrors[] = [ @@ -330,6 +336,12 @@ 'count' => 1, 'path' => __DIR__ . '/src/Models/UserIdentityModel.php', ]; +$ignoreErrors[] = [ + // identifier: codeigniter.factoriesClassConstFetch + 'message' => '#^Call to function model with CodeIgniter\\\\Shield\\\\Models\\\\GroupModel\\:\\:class is discouraged\\.$#', + 'count' => 1, + 'path' => __DIR__ . '/src/Models/UserModel.php', +]; $ignoreErrors[] = [ // identifier: codeigniter.factoriesClassConstFetch 'message' => '#^Call to function model with CodeIgniter\\\\Shield\\\\Models\\\\UserIdentityModel\\:\\:class is discouraged\\.$#', diff --git a/src/Authorization/Traits/Authorizable.php b/src/Authorization/Traits/Authorizable.php index 79e313951..3972c0918 100644 --- a/src/Authorization/Traits/Authorizable.php +++ b/src/Authorization/Traits/Authorizable.php @@ -33,8 +33,6 @@ public function addGroup(string ...$groups): self { $this->populateGroups(); - $configGroups = $this->getConfigGroups(); - $groupCount = count($this->groupCache); foreach ($groups as $group) { @@ -45,8 +43,11 @@ public function addGroup(string ...$groups): self continue; } + /** @var GroupModel $groupModel */ + $groupModel = model(GroupModel::class); + // make sure it's a valid group - if (! in_array($group, $configGroups, true)) { + if (! $groupModel->isValidGroup($group)) { throw AuthorizationException::forUnknownGroup($group); } @@ -96,10 +97,11 @@ public function syncGroups(string ...$groups): self { $this->populateGroups(); - $configGroups = $this->getConfigGroups(); + /** @var GroupModel $groupModel */ + $groupModel = model(GroupModel::class); foreach ($groups as $group) { - if (! in_array($group, $configGroups, true)) { + if (! $groupModel->isValidGroup($group)) { throw AuthorizationException::forUnknownGroup($group); } } @@ -398,14 +400,6 @@ private function saveGroupsOrPermissions(string $type, $model, array $cache): vo } } - /** - * @return list - */ - private function getConfigGroups(): array - { - return array_keys(setting('AuthGroups.groups')); - } - /** * @return list */ diff --git a/src/Commands/User.php b/src/Commands/User.php index 939438138..c0977cdf5 100644 --- a/src/Commands/User.php +++ b/src/Commands/User.php @@ -19,6 +19,7 @@ use CodeIgniter\Shield\Config\Auth; use CodeIgniter\Shield\Entities\User as UserEntity; use CodeIgniter\Shield\Exceptions\UserNotFoundException; +use CodeIgniter\Shield\Models\GroupModel; use CodeIgniter\Shield\Models\UserModel; use CodeIgniter\Shield\Validation\ValidationRules; use Config\Services; @@ -53,6 +54,7 @@ class User extends BaseCommand shield:user options shield:user create -n newusername -e newuser@example.com + shield:user create -n newusername -e newuser@example.com -g mygroup shield:user activate -n username shield:user activate -e user@example.com @@ -159,7 +161,7 @@ public function run(array $params): int try { switch ($action) { case 'create': - $this->create($username, $email); + $this->create($username, $email, $group); break; case 'activate': @@ -252,8 +254,9 @@ private function setValidationRules(): void * * @param string|null $username User name to create (optional) * @param string|null $email User email to create (optional) + * @param string|null $group Group to add user to (optional) */ - private function create(?string $username = null, ?string $email = null): void + private function create(?string $username = null, ?string $email = null, ?string $group = null): void { $data = []; @@ -303,6 +306,11 @@ private function create(?string $username = null, ?string $email = null): void $user = new UserEntity($data); + // Validate the group + if ($group !== null && ! $this->validateGroup($group)) { + throw new CancelException('Invalid group: "' . $group . '"'); + } + if ($username === null) { $userModel->allowEmptyInserts()->save($user); $this->write('New User created', 'green'); @@ -311,11 +319,26 @@ private function create(?string $username = null, ?string $email = null): void $this->write('User "' . $username . '" created', 'green'); } - // Add to default group $user = $userModel->findById($userModel->getInsertID()); - $userModel->addToDefaultGroup($user); - $this->write('The user is added to the default group.', 'green'); + if ($group === null) { + // Add to default group + $userModel->addToDefaultGroup($user); + + $this->write('The user is added to the default group.', 'green'); + } else { + $user->addGroup($group); + + $this->write('The user is added to group "' . $group . '".', 'green'); + } + } + + private function validateGroup(string $group): bool + { + /** @var GroupModel $groupModel */ + $groupModel = model(GroupModel::class); + + return $groupModel->isValidGroup($group); } /** diff --git a/src/Models/GroupModel.php b/src/Models/GroupModel.php index 899931400..d08c8774d 100644 --- a/src/Models/GroupModel.php +++ b/src/Models/GroupModel.php @@ -73,4 +73,14 @@ public function deleteNotIn($userId, $cache): void $this->checkQueryReturn($return); } + + /** + * @param non-empty-string $group Group name + */ + public function isValidGroup(string $group): bool + { + $allowedGroups = array_keys(setting('AuthGroups.groups')); + + return in_array($group, $allowedGroups, true); + } } diff --git a/src/Models/UserModel.php b/src/Models/UserModel.php index 27020b0b8..0fede61ec 100644 --- a/src/Models/UserModel.php +++ b/src/Models/UserModel.php @@ -153,10 +153,12 @@ private function assignIdentities(array $data, array $identities): array */ public function addToDefaultGroup(User $user): void { - $defaultGroup = setting('AuthGroups.defaultGroup'); - $allowedGroups = array_keys(setting('AuthGroups.groups')); + $defaultGroup = setting('AuthGroups.defaultGroup'); - if (empty($defaultGroup) || ! in_array($defaultGroup, $allowedGroups, true)) { + /** @var GroupModel $groupModel */ + $groupModel = model(GroupModel::class); + + if (empty($defaultGroup) || ! $groupModel->isValidGroup($defaultGroup)) { throw new InvalidArgumentException(lang('Auth.unknownGroup', [$defaultGroup ?? '--not found--'])); } diff --git a/tests/Commands/UserTest.php b/tests/Commands/UserTest.php index a85e713d6..b28cd0ae6 100644 --- a/tests/Commands/UserTest.php +++ b/tests/Commands/UserTest.php @@ -100,6 +100,59 @@ public function testCreate(): void ]); } + public function testCreateWithGroupBeta(): void + { + $this->setMockIo([ + 'Secret Passw0rd!', + 'Secret Passw0rd!', + ]); + + command('shield:user create -n user1 -e user1@example.com -g beta'); + + $this->assertStringContainsString( + 'User "user1" created', + $this->io->getFirstOutput() + ); + $this->assertStringContainsString( + 'The user is added to group "beta"', + $this->io->getFirstOutput() + ); + + $users = model(UserModel::class); + $user = $users->findByCredentials(['email' => 'user1@example.com']); + $this->seeInDatabase($this->tables['identities'], [ + 'user_id' => $user->id, + 'secret' => 'user1@example.com', + ]); + $this->seeInDatabase($this->tables['users'], [ + 'id' => $user->id, + 'active' => 0, + ]); + $this->seeInDatabase($this->tables['groups_users'], [ + 'user_id' => $user->id, + 'group' => 'beta', + ]); + } + + public function testCreateWithInvalidGroup(): void + { + $this->setMockIo([ + 'Secret Passw0rd!', + 'Secret Passw0rd!', + ]); + + command('shield:user create -n user1 -e user1@example.com -g invalid'); + + $this->assertStringContainsString( + 'Invalid group: "invalid"', + $this->io->getFirstOutput() + ); + + $users = model(UserModel::class); + $user = $users->findByCredentials(['email' => 'user1@example.com']); + $this->assertNull($user); + } + public function testCreateNotUniqueName(): void { $user = $this->createUser([