-
-
Notifications
You must be signed in to change notification settings - Fork 737
Expand file tree
/
Copy pathusers.py
More file actions
224 lines (190 loc) · 5.2 KB
/
users.py
File metadata and controls
224 lines (190 loc) · 5.2 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
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
from dataclasses import dataclass, field
from enum import Enum
from typing import NotRequired, Self, TypedDict, override
from archinstall.lib.crypt import crypt_yescrypt
from archinstall.lib.translationhandler import tr
class PasswordStrength(Enum):
VERY_WEAK = 'very weak'
WEAK = 'weak'
MODERATE = 'moderate'
STRONG = 'strong'
@property
@override
def value(self) -> str: # pylint: disable=invalid-overridden-method
match self:
case PasswordStrength.VERY_WEAK:
return tr('very weak')
case PasswordStrength.WEAK:
return tr('weak')
case PasswordStrength.MODERATE:
return tr('moderate')
case PasswordStrength.STRONG:
return tr('strong')
def color(self) -> str:
match self:
case PasswordStrength.VERY_WEAK:
return 'red'
case PasswordStrength.WEAK:
return 'red'
case PasswordStrength.MODERATE:
return 'yellow'
case PasswordStrength.STRONG:
return 'green'
@classmethod
def strength(cls, password: str) -> Self:
digit = any(character.isdigit() for character in password)
upper = any(character.isupper() for character in password)
lower = any(character.islower() for character in password)
symbol = any(not character.isalnum() for character in password)
return cls._check_password_strength(digit, upper, lower, symbol, len(password))
@classmethod
def _check_password_strength(
cls,
digit: bool,
upper: bool,
lower: bool,
symbol: bool,
length: int,
) -> Self:
# suggested evaluation
# https://github.com/archlinux/archinstall/issues/1304#issuecomment-1146768163
if digit and upper and lower and symbol:
match length:
case num if 13 <= num:
return cls.STRONG
case num if 11 <= num <= 12:
return cls.MODERATE
case num if 7 <= num <= 10:
return cls.WEAK
case num if num <= 6:
return cls.VERY_WEAK
elif digit and upper and lower:
match length:
case num if 14 <= num:
return cls.STRONG
case num if 11 <= num <= 13:
return cls.MODERATE
case num if 7 <= num <= 10:
return cls.WEAK
case num if num <= 6:
return cls.VERY_WEAK
elif upper and lower:
match length:
case num if 15 <= num:
return cls.STRONG
case num if 12 <= num <= 14:
return cls.MODERATE
case num if 7 <= num <= 11:
return cls.WEAK
case num if num <= 6:
return cls.VERY_WEAK
elif lower or upper:
match length:
case num if 18 <= num:
return cls.STRONG
case num if 14 <= num <= 17:
return cls.MODERATE
case num if 9 <= num <= 13:
return cls.WEAK
case num if num <= 8:
return cls.VERY_WEAK
return cls.VERY_WEAK
UserSerialization = TypedDict(
'UserSerialization',
{
'username': str,
'!password': NotRequired[str],
'sudo': bool,
'groups': list[str],
'enc_password': str | None,
'birth_date': NotRequired[str],
},
)
class Password:
def __init__(
self,
plaintext: str = '',
enc_password: str | None = None,
):
if plaintext:
enc_password = crypt_yescrypt(plaintext)
if not plaintext and not enc_password:
raise ValueError('Either plaintext or enc_password must be provided')
self._plaintext = plaintext
self.enc_password = enc_password
@property
def plaintext(self) -> str:
return self._plaintext
@plaintext.setter
def plaintext(self, value: str) -> None:
self._plaintext = value
self.enc_password = crypt_yescrypt(value)
@override
def __eq__(self, other: object) -> bool:
if not isinstance(other, Password):
return NotImplemented
if self._plaintext and other._plaintext:
return self._plaintext == other._plaintext
return self.enc_password == other.enc_password
def hidden(self) -> str:
if self._plaintext:
return '*' * len(self._plaintext)
else:
return '*' * 8
@dataclass
class User:
username: str
password: Password
sudo: bool
groups: list[str] = field(default_factory=list)
birth_date: str = ''
@override
def __str__(self) -> str:
# safety overwrite to make sure password is not leaked
return f'User({self.username=}, {self.sudo=}, {self.groups=})'
def table_data(self) -> dict[str, str | bool | list[str]]:
return {
'username': self.username,
'password': self.password.hidden(),
'sudo': self.sudo,
'groups': self.groups,
'birth_date': self.birth_date,
}
def json(self) -> UserSerialization:
data: UserSerialization = {
'username': self.username,
'enc_password': self.password.enc_password,
'sudo': self.sudo,
'groups': self.groups,
}
if self.birth_date:
data['birth_date'] = self.birth_date
return data
@classmethod
def parse_arguments(
cls,
args: list[UserSerialization],
) -> list[Self]:
users = []
for entry in args:
username = entry.get('username')
password: Password | None = None
groups = entry.get('groups', [])
plaintext = entry.get('!password')
enc_password = entry.get('enc_password')
# DEPRECATED: backwards compatibility
if plaintext:
password = Password(plaintext=plaintext)
elif enc_password:
password = Password(enc_password=enc_password)
if not username or password is None:
continue
user = cls(
username=username,
password=password,
sudo=entry.get('sudo', False) is True,
groups=groups,
birth_date=entry.get('birth_date', ''),
)
users.append(user)
return users