-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathCreateUserCommand.php
More file actions
137 lines (114 loc) · 4.51 KB
/
Copy pathCreateUserCommand.php
File metadata and controls
137 lines (114 loc) · 4.51 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
<?php
/*
* Copyright (c) 2026 Netresearch DTT GmbH
* SPDX-License-Identifier: AGPL-3.0-only
*/
declare(strict_types=1);
namespace App\Command;
use App\Entity\User;
use App\Enum\UserType;
use Doctrine\ORM\EntityManagerInterface;
use SensitiveParameter;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use function is_string;
use function sprintf;
/**
* Creates a local (password) user or (re)sets an existing user's password —
* the bootstrap for a local-only install and the LDAP-outage escape hatch
* (ADR-018 D1). The password is hashed via the configured `auto` hasher and
* never logged.
*
* Invokable, attribute-driven input (Symfony 8): no configure()/getOption(),
* which also keeps the phpstan-symfony console analyser from booting the app.
*/
#[AsCommand(name: 'app:user:create', description: 'Create a local password user or reset a user password')]
final readonly class CreateUserCommand
{
public function __construct(
private EntityManagerInterface $entityManager,
private UserPasswordHasherInterface $passwordHasher,
) {
}
public function __invoke(
SymfonyStyle $io,
#[Argument(description: 'Login username')]
?string $username = null,
#[Option(description: 'User type: USER, DEV, PL or ADMIN (default ADMIN for a new user; preserved for an existing one)')]
?string $type = null,
#[Option(description: 'Password (omit to be prompted; prompting keeps it out of shell history)')]
#[SensitiveParameter]
?string $password = null,
): int {
if (null === $username) {
$answer = $io->ask('Username');
$username = is_string($answer) ? $answer : '';
}
if ('' === $username) {
$io->error('A username is required.');
return Command::FAILURE;
}
if (null === $password) {
$answer = $io->askHidden('Password');
$password = is_string($answer) ? $answer : '';
}
if ('' === $password) {
$io->error('A non-empty password is required.');
return Command::FAILURE;
}
$user = $this->entityManager->getRepository(User::class)->findOneBy(['username' => $username]);
$isNew = !$user instanceof User;
$userType = $this->resolveType($type, $user);
if (!$userType instanceof UserType) {
$io->error('Invalid --type. Use one of: USER, DEV, PL, ADMIN.');
return Command::FAILURE;
}
if ($isNew) {
// Locale is intentionally left at the entity default ('de') to match
// LDAP auto-provisioning; no per-account override is offered here.
$user = new User()
->setUsername($username)
->setType($userType);
} else {
$user->setType($userType);
}
$this->setHashedPassword($user, $password);
$this->entityManager->persist($user);
$this->entityManager->flush();
$io->success(sprintf(
'%s local user "%s" (%s).',
$isNew ? 'Created' : 'Updated password for',
$username,
$userType->value,
));
return Command::SUCCESS;
}
/**
* Resolve the effective user type without silently re-typing an existing user.
*
* An explicitly given --type is validated (a `null` return signals an invalid
* value). When --type is omitted, a brand-new user defaults to ADMIN (the
* bootstrap case) and an existing user KEEPS its current type — this command
* doubles as a password reset, so re-running it for `jane` must never promote
* her just because ADMIN is the new-user default.
*/
private function resolveType(?string $type, ?User $existing): ?UserType
{
if (null === $type) {
return $existing instanceof User ? $existing->getType() : UserType::ADMIN;
}
$userType = UserType::tryFrom($type);
if (!$userType instanceof UserType || UserType::UNKNOWN === $userType) {
return null;
}
return $userType;
}
private function setHashedPassword(User $user, #[SensitiveParameter] string $plainPassword): void
{
$user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword));
}
}