]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
1 | # -*- coding: utf-8 -*- |
2 | # pylint: disable=too-many-arguments,too-many-return-statements | |
3 | # pylint: disable=too-many-branches, too-many-locals, too-many-statements | |
11fdf7f2 TL |
4 | |
5 | import errno | |
6 | import json | |
9f95a23c | 7 | import logging |
f67539c2 | 8 | import re |
11fdf7f2 TL |
9 | import threading |
10 | import time | |
9f95a23c | 11 | from datetime import datetime, timedelta |
f67539c2 TL |
12 | from string import ascii_lowercase, ascii_uppercase, digits, punctuation |
13 | from typing import List, Optional, Sequence | |
11fdf7f2 TL |
14 | |
15 | import bcrypt | |
cd265ab1 | 16 | from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand |
11fdf7f2 | 17 | |
9f95a23c | 18 | from .. import mgr |
f67539c2 TL |
19 | from ..exceptions import PasswordPolicyException, PermissionNotValid, \ |
20 | PwdExpirationDateNotValid, RoleAlreadyExists, RoleDoesNotExist, \ | |
21 | RoleIsAssociatedWithUser, RoleNotInUser, ScopeNotInRole, ScopeNotValid, \ | |
22 | UserAlreadyExists, UserDoesNotExist | |
23 | from ..security import Permission, Scope | |
9f95a23c | 24 | from ..settings import Settings |
9f95a23c TL |
25 | |
26 | logger = logging.getLogger('access_control') | |
522d829b | 27 | DEFAULT_FILE_DESC = 'password/secret' |
11fdf7f2 TL |
28 | |
29 | ||
30 | # password hashing algorithm | |
31 | def password_hash(password, salt_password=None): | |
32 | if not password: | |
33 | return None | |
34 | if not salt_password: | |
35 | salt_password = bcrypt.gensalt() | |
36 | else: | |
37 | salt_password = salt_password.encode('utf8') | |
38 | return bcrypt.hashpw(password.encode('utf8'), salt_password).decode('utf8') | |
39 | ||
40 | ||
41 | _P = Permission # short alias | |
42 | ||
43 | ||
9f95a23c TL |
44 | class PasswordPolicy(object): |
45 | def __init__(self, password, username=None, old_password=None): | |
46 | """ | |
47 | :param password: The new plain password. | |
48 | :type password: str | |
49 | :param username: The name of the user. | |
50 | :type username: str | None | |
51 | :param old_password: The old plain password. | |
52 | :type old_password: str | None | |
53 | """ | |
54 | self.password = password | |
55 | self.username = username | |
56 | self.old_password = old_password | |
57 | self.forbidden_words = Settings.PWD_POLICY_EXCLUSION_LIST.split(',') | |
58 | self.complexity_credits = 0 | |
59 | ||
60 | @staticmethod | |
61 | def _check_if_contains_word(password, word): | |
62 | return re.compile('(?:{0})'.format(word), | |
63 | flags=re.IGNORECASE).search(password) | |
64 | ||
65 | def check_password_complexity(self): | |
66 | if not Settings.PWD_POLICY_CHECK_COMPLEXITY_ENABLED: | |
67 | return Settings.PWD_POLICY_MIN_COMPLEXITY | |
68 | digit_credit = 1 | |
69 | small_letter_credit = 1 | |
70 | big_letter_credit = 2 | |
71 | special_character_credit = 3 | |
72 | other_character_credit = 5 | |
73 | self.complexity_credits = 0 | |
74 | for ch in self.password: | |
75 | if ch in ascii_uppercase: | |
76 | self.complexity_credits += big_letter_credit | |
77 | elif ch in ascii_lowercase: | |
78 | self.complexity_credits += small_letter_credit | |
79 | elif ch in digits: | |
80 | self.complexity_credits += digit_credit | |
81 | elif ch in punctuation: | |
82 | self.complexity_credits += special_character_credit | |
83 | else: | |
84 | self.complexity_credits += other_character_credit | |
85 | return self.complexity_credits | |
86 | ||
87 | def check_is_old_password(self): | |
88 | if not Settings.PWD_POLICY_CHECK_OLDPWD_ENABLED: | |
89 | return False | |
90 | return self.old_password and self.password == self.old_password | |
91 | ||
92 | def check_if_contains_username(self): | |
93 | if not Settings.PWD_POLICY_CHECK_USERNAME_ENABLED: | |
94 | return False | |
95 | if not self.username: | |
96 | return False | |
97 | return self._check_if_contains_word(self.password, self.username) | |
98 | ||
99 | def check_if_contains_forbidden_words(self): | |
100 | if not Settings.PWD_POLICY_CHECK_EXCLUSION_LIST_ENABLED: | |
101 | return False | |
102 | return self._check_if_contains_word(self.password, | |
103 | '|'.join(self.forbidden_words)) | |
104 | ||
105 | def check_if_sequential_characters(self): | |
106 | if not Settings.PWD_POLICY_CHECK_SEQUENTIAL_CHARS_ENABLED: | |
107 | return False | |
108 | for i in range(1, len(self.password) - 1): | |
109 | if ord(self.password[i - 1]) + 1 == ord(self.password[i])\ | |
110 | == ord(self.password[i + 1]) - 1: | |
111 | return True | |
112 | return False | |
113 | ||
114 | def check_if_repetitive_characters(self): | |
115 | if not Settings.PWD_POLICY_CHECK_REPETITIVE_CHARS_ENABLED: | |
116 | return False | |
117 | for i in range(1, len(self.password) - 1): | |
118 | if self.password[i - 1] == self.password[i] == self.password[i + 1]: | |
119 | return True | |
120 | return False | |
121 | ||
122 | def check_password_length(self): | |
123 | if not Settings.PWD_POLICY_CHECK_LENGTH_ENABLED: | |
124 | return True | |
125 | return len(self.password) >= Settings.PWD_POLICY_MIN_LENGTH | |
126 | ||
127 | def check_all(self): | |
128 | """ | |
129 | Perform all password policy checks. | |
130 | :raise PasswordPolicyException: If a password policy check fails. | |
131 | """ | |
132 | if not Settings.PWD_POLICY_ENABLED: | |
133 | return | |
134 | if self.check_password_complexity() < Settings.PWD_POLICY_MIN_COMPLEXITY: | |
135 | raise PasswordPolicyException('Password is too weak.') | |
136 | if not self.check_password_length(): | |
137 | raise PasswordPolicyException('Password is too weak.') | |
138 | if self.check_is_old_password(): | |
139 | raise PasswordPolicyException('Password must not be the same as the previous one.') | |
140 | if self.check_if_contains_username(): | |
141 | raise PasswordPolicyException('Password must not contain username.') | |
142 | result = self.check_if_contains_forbidden_words() | |
143 | if result: | |
144 | raise PasswordPolicyException('Password must not contain the keyword "{}".'.format( | |
145 | result.group(0))) | |
146 | if self.check_if_repetitive_characters(): | |
147 | raise PasswordPolicyException('Password must not contain repetitive characters.') | |
148 | if self.check_if_sequential_characters(): | |
149 | raise PasswordPolicyException('Password must not contain sequential characters.') | |
150 | ||
151 | ||
11fdf7f2 TL |
152 | class Role(object): |
153 | def __init__(self, name, description=None, scope_permissions=None): | |
154 | self.name = name | |
155 | self.description = description | |
156 | if scope_permissions is None: | |
157 | self.scopes_permissions = {} | |
158 | else: | |
159 | self.scopes_permissions = scope_permissions | |
160 | ||
161 | def __hash__(self): | |
162 | return hash(self.name) | |
163 | ||
164 | def __eq__(self, other): | |
165 | return self.name == other.name | |
166 | ||
167 | def set_scope_permissions(self, scope, permissions): | |
168 | if not Scope.valid_scope(scope): | |
169 | raise ScopeNotValid(scope) | |
170 | for perm in permissions: | |
171 | if not Permission.valid_permission(perm): | |
172 | raise PermissionNotValid(perm) | |
173 | ||
174 | permissions.sort() | |
175 | self.scopes_permissions[scope] = permissions | |
176 | ||
177 | def del_scope_permissions(self, scope): | |
178 | if scope not in self.scopes_permissions: | |
179 | raise ScopeNotInRole(scope, self.name) | |
180 | del self.scopes_permissions[scope] | |
181 | ||
182 | def reset_scope_permissions(self): | |
183 | self.scopes_permissions = {} | |
184 | ||
185 | def authorize(self, scope, permissions): | |
186 | if scope in self.scopes_permissions: | |
187 | role_perms = self.scopes_permissions[scope] | |
188 | for perm in permissions: | |
189 | if perm not in role_perms: | |
190 | return False | |
191 | return True | |
192 | return False | |
193 | ||
194 | def to_dict(self): | |
195 | return { | |
196 | 'name': self.name, | |
197 | 'description': self.description, | |
198 | 'scopes_permissions': self.scopes_permissions | |
199 | } | |
200 | ||
201 | @classmethod | |
202 | def from_dict(cls, r_dict): | |
203 | return Role(r_dict['name'], r_dict['description'], | |
204 | r_dict['scopes_permissions']) | |
205 | ||
206 | ||
207 | # static pre-defined system roles | |
208 | # this roles cannot be deleted nor updated | |
209 | ||
210 | # admin role provides all permissions for all scopes | |
f67539c2 TL |
211 | ADMIN_ROLE = Role( |
212 | 'administrator', 'allows full permissions for all security scopes', { | |
213 | scope_name: Permission.all_permissions() | |
214 | for scope_name in Scope.all_scopes() | |
215 | }) | |
11fdf7f2 TL |
216 | |
217 | ||
218 | # read-only role provides read-only permission for all scopes | |
f67539c2 TL |
219 | READ_ONLY_ROLE = Role( |
220 | 'read-only', | |
221 | 'allows read permission for all security scope except dashboard settings and config-opt', { | |
222 | scope_name: [_P.READ] for scope_name in Scope.all_scopes() | |
223 | if scope_name not in (Scope.DASHBOARD_SETTINGS, Scope.CONFIG_OPT) | |
224 | }) | |
11fdf7f2 TL |
225 | |
226 | ||
227 | # block manager role provides all permission for block related scopes | |
f67539c2 TL |
228 | BLOCK_MGR_ROLE = Role( |
229 | 'block-manager', 'allows full permissions for rbd-image, rbd-mirroring, and iscsi scopes', { | |
230 | Scope.RBD_IMAGE: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
231 | Scope.POOL: [_P.READ], | |
232 | Scope.ISCSI: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
233 | Scope.RBD_MIRRORING: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
234 | Scope.GRAFANA: [_P.READ], | |
235 | }) | |
11fdf7f2 TL |
236 | |
237 | ||
238 | # RadosGW manager role provides all permissions for block related scopes | |
f67539c2 TL |
239 | RGW_MGR_ROLE = Role( |
240 | 'rgw-manager', 'allows full permissions for the rgw scope', { | |
241 | Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
242 | Scope.GRAFANA: [_P.READ], | |
243 | }) | |
11fdf7f2 TL |
244 | |
245 | ||
246 | # Cluster manager role provides all permission for OSDs, Monitors, and | |
247 | # Config options | |
f67539c2 TL |
248 | CLUSTER_MGR_ROLE = Role( |
249 | 'cluster-manager', """allows full permissions for the hosts, osd, mon, mgr, | |
250 | and config-opt scopes""", { | |
251 | Scope.HOSTS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
252 | Scope.OSD: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
253 | Scope.MONITOR: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
254 | Scope.MANAGER: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
255 | Scope.CONFIG_OPT: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
256 | Scope.LOG: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
257 | Scope.GRAFANA: [_P.READ], | |
258 | }) | |
11fdf7f2 TL |
259 | |
260 | ||
261 | # Pool manager role provides all permissions for pool related scopes | |
f67539c2 TL |
262 | POOL_MGR_ROLE = Role( |
263 | 'pool-manager', 'allows full permissions for the pool scope', { | |
264 | Scope.POOL: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
265 | Scope.GRAFANA: [_P.READ], | |
266 | }) | |
11fdf7f2 | 267 | |
9f95a23c | 268 | # CephFS manager role provides all permissions for CephFS related scopes |
f67539c2 TL |
269 | CEPHFS_MGR_ROLE = Role( |
270 | 'cephfs-manager', 'allows full permissions for the cephfs scope', { | |
271 | Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
272 | Scope.GRAFANA: [_P.READ], | |
273 | }) | |
11fdf7f2 | 274 | |
f67539c2 TL |
275 | GANESHA_MGR_ROLE = Role( |
276 | 'ganesha-manager', 'allows full permissions for the nfs-ganesha scope', { | |
277 | Scope.NFS_GANESHA: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
278 | Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
279 | Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], | |
280 | Scope.GRAFANA: [_P.READ], | |
281 | }) | |
11fdf7f2 TL |
282 | |
283 | ||
284 | SYSTEM_ROLES = { | |
285 | ADMIN_ROLE.name: ADMIN_ROLE, | |
286 | READ_ONLY_ROLE.name: READ_ONLY_ROLE, | |
287 | BLOCK_MGR_ROLE.name: BLOCK_MGR_ROLE, | |
288 | RGW_MGR_ROLE.name: RGW_MGR_ROLE, | |
289 | CLUSTER_MGR_ROLE.name: CLUSTER_MGR_ROLE, | |
290 | POOL_MGR_ROLE.name: POOL_MGR_ROLE, | |
291 | CEPHFS_MGR_ROLE.name: CEPHFS_MGR_ROLE, | |
292 | GANESHA_MGR_ROLE.name: GANESHA_MGR_ROLE, | |
293 | } | |
294 | ||
295 | ||
296 | class User(object): | |
297 | def __init__(self, username, password, name=None, email=None, roles=None, | |
9f95a23c TL |
298 | last_update=None, enabled=True, pwd_expiration_date=None, |
299 | pwd_update_required=False): | |
11fdf7f2 TL |
300 | self.username = username |
301 | self.password = password | |
302 | self.name = name | |
303 | self.email = email | |
adb31ebb | 304 | self.invalid_auth_attempt = 0 |
11fdf7f2 TL |
305 | if roles is None: |
306 | self.roles = set() | |
307 | else: | |
308 | self.roles = roles | |
9f95a23c TL |
309 | if last_update is None: |
310 | self.refresh_last_update() | |
11fdf7f2 | 311 | else: |
9f95a23c TL |
312 | self.last_update = last_update |
313 | self._enabled = enabled | |
314 | self.pwd_expiration_date = pwd_expiration_date | |
315 | if self.pwd_expiration_date is None: | |
316 | self.refresh_pwd_expiration_date() | |
317 | self.pwd_update_required = pwd_update_required | |
318 | ||
319 | def refresh_last_update(self): | |
320 | self.last_update = int(time.time()) | |
321 | ||
322 | def refresh_pwd_expiration_date(self): | |
323 | if Settings.USER_PWD_EXPIRATION_SPAN > 0: | |
324 | expiration_date = datetime.utcnow() + timedelta( | |
325 | days=Settings.USER_PWD_EXPIRATION_SPAN) | |
326 | self.pwd_expiration_date = int(time.mktime(expiration_date.timetuple())) | |
327 | else: | |
328 | self.pwd_expiration_date = None | |
329 | ||
330 | @property | |
331 | def enabled(self): | |
332 | return self._enabled | |
11fdf7f2 | 333 | |
9f95a23c TL |
334 | @enabled.setter |
335 | def enabled(self, value): | |
336 | self._enabled = value | |
337 | self.refresh_last_update() | |
11fdf7f2 TL |
338 | |
339 | def set_password(self, password): | |
9f95a23c TL |
340 | self.set_password_hash(password_hash(password)) |
341 | ||
342 | def set_password_hash(self, hashed_password): | |
adb31ebb | 343 | self.invalid_auth_attempt = 0 |
9f95a23c TL |
344 | self.password = hashed_password |
345 | self.refresh_last_update() | |
346 | self.refresh_pwd_expiration_date() | |
347 | self.pwd_update_required = False | |
348 | ||
349 | def compare_password(self, password): | |
350 | """ | |
351 | Compare the specified password with the user password. | |
352 | :param password: The plain password to check. | |
353 | :type password: str | |
354 | :return: `True` if the passwords are equal, otherwise `False`. | |
355 | :rtype: bool | |
356 | """ | |
357 | pass_hash = password_hash(password, salt_password=self.password) | |
358 | return pass_hash == self.password | |
359 | ||
360 | def is_pwd_expired(self): | |
361 | if self.pwd_expiration_date: | |
362 | current_time = int(time.mktime(datetime.utcnow().timetuple())) | |
363 | return self.pwd_expiration_date < current_time | |
364 | return False | |
11fdf7f2 TL |
365 | |
366 | def set_roles(self, roles): | |
367 | self.roles = set(roles) | |
9f95a23c | 368 | self.refresh_last_update() |
11fdf7f2 TL |
369 | |
370 | def add_roles(self, roles): | |
371 | self.roles = self.roles.union(set(roles)) | |
9f95a23c | 372 | self.refresh_last_update() |
11fdf7f2 TL |
373 | |
374 | def del_roles(self, roles): | |
375 | for role in roles: | |
376 | if role not in self.roles: | |
377 | raise RoleNotInUser(role.name, self.username) | |
378 | self.roles.difference_update(set(roles)) | |
9f95a23c | 379 | self.refresh_last_update() |
11fdf7f2 TL |
380 | |
381 | def authorize(self, scope, permissions): | |
9f95a23c TL |
382 | if self.pwd_update_required: |
383 | return False | |
384 | ||
11fdf7f2 TL |
385 | for role in self.roles: |
386 | if role.authorize(scope, permissions): | |
387 | return True | |
388 | return False | |
389 | ||
390 | def permissions_dict(self): | |
9f95a23c TL |
391 | # type: () -> dict |
392 | perms = {} # type: dict | |
11fdf7f2 TL |
393 | for role in self.roles: |
394 | for scope, perms_list in role.scopes_permissions.items(): | |
395 | if scope in perms: | |
396 | perms_tmp = set(perms[scope]).union(set(perms_list)) | |
397 | perms[scope] = list(perms_tmp) | |
398 | else: | |
399 | perms[scope] = perms_list | |
400 | ||
401 | return perms | |
402 | ||
403 | def to_dict(self): | |
404 | return { | |
405 | 'username': self.username, | |
406 | 'password': self.password, | |
407 | 'roles': sorted([r.name for r in self.roles]), | |
408 | 'name': self.name, | |
409 | 'email': self.email, | |
9f95a23c TL |
410 | 'lastUpdate': self.last_update, |
411 | 'enabled': self.enabled, | |
412 | 'pwdExpirationDate': self.pwd_expiration_date, | |
413 | 'pwdUpdateRequired': self.pwd_update_required | |
11fdf7f2 TL |
414 | } |
415 | ||
416 | @classmethod | |
417 | def from_dict(cls, u_dict, roles): | |
418 | return User(u_dict['username'], u_dict['password'], u_dict['name'], | |
419 | u_dict['email'], {roles[r] for r in u_dict['roles']}, | |
9f95a23c TL |
420 | u_dict['lastUpdate'], u_dict['enabled'], |
421 | u_dict['pwdExpirationDate'], u_dict['pwdUpdateRequired']) | |
11fdf7f2 TL |
422 | |
423 | ||
424 | class AccessControlDB(object): | |
9f95a23c | 425 | VERSION = 2 |
11fdf7f2 TL |
426 | ACDB_CONFIG_KEY = "accessdb_v" |
427 | ||
428 | def __init__(self, version, users, roles): | |
429 | self.users = users | |
430 | self.version = version | |
431 | self.roles = roles | |
432 | self.lock = threading.RLock() | |
433 | ||
434 | def create_role(self, name, description=None): | |
435 | with self.lock: | |
436 | if name in SYSTEM_ROLES or name in self.roles: | |
437 | raise RoleAlreadyExists(name) | |
438 | role = Role(name, description) | |
439 | self.roles[name] = role | |
440 | return role | |
441 | ||
442 | def get_role(self, name): | |
443 | with self.lock: | |
444 | if name not in self.roles: | |
445 | raise RoleDoesNotExist(name) | |
446 | return self.roles[name] | |
447 | ||
adb31ebb TL |
448 | def increment_attempt(self, username): |
449 | with self.lock: | |
450 | if username in self.users: | |
451 | self.users[username].invalid_auth_attempt += 1 | |
452 | ||
453 | def reset_attempt(self, username): | |
454 | with self.lock: | |
455 | if username in self.users: | |
456 | self.users[username].invalid_auth_attempt = 0 | |
457 | ||
458 | def get_attempt(self, username): | |
459 | with self.lock: | |
460 | try: | |
461 | return self.users[username].invalid_auth_attempt | |
462 | except KeyError: | |
463 | return 0 | |
464 | ||
11fdf7f2 TL |
465 | def delete_role(self, name): |
466 | with self.lock: | |
467 | if name not in self.roles: | |
468 | raise RoleDoesNotExist(name) | |
469 | role = self.roles[name] | |
470 | ||
471 | # check if role is not associated with a user | |
472 | for username, user in self.users.items(): | |
473 | if role in user.roles: | |
474 | raise RoleIsAssociatedWithUser(name, username) | |
475 | ||
476 | del self.roles[name] | |
477 | ||
9f95a23c TL |
478 | def create_user(self, username, password, name, email, enabled=True, |
479 | pwd_expiration_date=None, pwd_update_required=False): | |
480 | logger.debug("creating user: username=%s", username) | |
11fdf7f2 TL |
481 | with self.lock: |
482 | if username in self.users: | |
483 | raise UserAlreadyExists(username) | |
9f95a23c TL |
484 | if pwd_expiration_date and \ |
485 | (pwd_expiration_date < int(time.mktime(datetime.utcnow().timetuple()))): | |
486 | raise PwdExpirationDateNotValid() | |
487 | user = User(username, password_hash(password), name, email, enabled=enabled, | |
488 | pwd_expiration_date=pwd_expiration_date, | |
489 | pwd_update_required=pwd_update_required) | |
11fdf7f2 TL |
490 | self.users[username] = user |
491 | return user | |
492 | ||
493 | def get_user(self, username): | |
494 | with self.lock: | |
495 | if username not in self.users: | |
496 | raise UserDoesNotExist(username) | |
497 | return self.users[username] | |
498 | ||
499 | def delete_user(self, username): | |
500 | with self.lock: | |
501 | if username not in self.users: | |
502 | raise UserDoesNotExist(username) | |
503 | del self.users[username] | |
504 | ||
505 | def update_users_with_roles(self, role): | |
506 | with self.lock: | |
507 | if not role: | |
508 | return | |
509 | for _, user in self.users.items(): | |
510 | if role in user.roles: | |
9f95a23c | 511 | user.refresh_last_update() |
11fdf7f2 TL |
512 | |
513 | def save(self): | |
514 | with self.lock: | |
515 | db = { | |
516 | 'users': {un: u.to_dict() for un, u in self.users.items()}, | |
517 | 'roles': {rn: r.to_dict() for rn, r in self.roles.items()}, | |
518 | 'version': self.version | |
519 | } | |
520 | mgr.set_store(self.accessdb_config_key(), json.dumps(db)) | |
521 | ||
522 | @classmethod | |
523 | def accessdb_config_key(cls, version=None): | |
524 | if version is None: | |
525 | version = cls.VERSION | |
526 | return "{}{}".format(cls.ACDB_CONFIG_KEY, version) | |
527 | ||
b3b6e05e TL |
528 | def check_and_update_db(self): |
529 | logger.debug("Checking for previous DB versions") | |
530 | ||
531 | def check_migrate_v1_to_current(): | |
532 | # Check if version 1 exists in the DB and migrate it to current version | |
533 | v1_db = mgr.get_store(self.accessdb_config_key(1)) | |
534 | if v1_db: | |
535 | logger.debug("Found database v1 credentials") | |
536 | v1_db = json.loads(v1_db) | |
537 | ||
538 | for user, _ in v1_db['users'].items(): | |
539 | v1_db['users'][user]['enabled'] = True | |
540 | v1_db['users'][user]['pwdExpirationDate'] = None | |
541 | v1_db['users'][user]['pwdUpdateRequired'] = False | |
542 | ||
543 | self.roles = {rn: Role.from_dict(r) for rn, r in v1_db.get('roles', {}).items()} | |
544 | self.users = {un: User.from_dict(u, dict(self.roles, **SYSTEM_ROLES)) | |
545 | for un, u in v1_db.get('users', {}).items()} | |
546 | ||
547 | self.save() | |
548 | ||
549 | check_migrate_v1_to_current() | |
550 | ||
11fdf7f2 TL |
551 | @classmethod |
552 | def load(cls): | |
9f95a23c | 553 | logger.info("Loading user roles DB version=%s", cls.VERSION) |
11fdf7f2 TL |
554 | |
555 | json_db = mgr.get_store(cls.accessdb_config_key()) | |
556 | if json_db is None: | |
9f95a23c | 557 | logger.debug("No DB v%s found, creating new...", cls.VERSION) |
11fdf7f2 | 558 | db = cls(cls.VERSION, {}, {}) |
b3b6e05e TL |
559 | # check if we can update from a previous version database |
560 | db.check_and_update_db() | |
11fdf7f2 TL |
561 | return db |
562 | ||
9f95a23c | 563 | dict_db = json.loads(json_db) |
11fdf7f2 | 564 | roles = {rn: Role.from_dict(r) |
9f95a23c | 565 | for rn, r in dict_db.get('roles', {}).items()} |
11fdf7f2 | 566 | users = {un: User.from_dict(u, dict(roles, **SYSTEM_ROLES)) |
9f95a23c TL |
567 | for un, u in dict_db.get('users', {}).items()} |
568 | return cls(dict_db['version'], users, roles) | |
11fdf7f2 TL |
569 | |
570 | ||
571 | def load_access_control_db(): | |
572 | mgr.ACCESS_CTRL_DB = AccessControlDB.load() | |
573 | ||
574 | ||
575 | # CLI dashboard access control scope commands | |
576 | ||
f67539c2 | 577 | @CLIWriteCommand('dashboard set-login-credentials') |
522d829b | 578 | @CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC) |
f67539c2 TL |
579 | def set_login_credentials_cmd(_, username: str, inbuf: str): |
580 | ''' | |
581 | Set the login credentials. Password read from -i <file> | |
582 | ''' | |
cd265ab1 | 583 | password = inbuf |
11fdf7f2 TL |
584 | try: |
585 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
586 | user.set_password(password) | |
587 | except UserDoesNotExist: | |
588 | user = mgr.ACCESS_CTRL_DB.create_user(username, password, None, None) | |
589 | user.set_roles([ADMIN_ROLE]) | |
590 | ||
591 | mgr.ACCESS_CTRL_DB.save() | |
592 | ||
593 | return 0, '''\ | |
594 | ****************************************************************** | |
595 | *** WARNING: this command is deprecated. *** | |
596 | *** Please use the ac-user-* related commands to manage users. *** | |
597 | ****************************************************************** | |
598 | Username and password updated''', '' | |
599 | ||
600 | ||
f67539c2 TL |
601 | @CLIReadCommand('dashboard ac-role-show') |
602 | def ac_role_show_cmd(_, rolename: Optional[str] = None): | |
603 | ''' | |
604 | Show role info | |
605 | ''' | |
11fdf7f2 TL |
606 | if not rolename: |
607 | roles = dict(mgr.ACCESS_CTRL_DB.roles) | |
608 | roles.update(SYSTEM_ROLES) | |
609 | roles_list = [name for name, _ in roles.items()] | |
610 | return 0, json.dumps(roles_list), '' | |
611 | try: | |
612 | role = mgr.ACCESS_CTRL_DB.get_role(rolename) | |
613 | except RoleDoesNotExist as ex: | |
614 | if rolename not in SYSTEM_ROLES: | |
615 | return -errno.ENOENT, '', str(ex) | |
616 | role = SYSTEM_ROLES[rolename] | |
617 | return 0, json.dumps(role.to_dict()), '' | |
618 | ||
619 | ||
f67539c2 TL |
620 | @CLIWriteCommand('dashboard ac-role-create') |
621 | def ac_role_create_cmd(_, rolename: str, description: Optional[str] = None): | |
622 | ''' | |
623 | Create a new access control role | |
624 | ''' | |
11fdf7f2 TL |
625 | try: |
626 | role = mgr.ACCESS_CTRL_DB.create_role(rolename, description) | |
627 | mgr.ACCESS_CTRL_DB.save() | |
628 | return 0, json.dumps(role.to_dict()), '' | |
629 | except RoleAlreadyExists as ex: | |
630 | return -errno.EEXIST, '', str(ex) | |
631 | ||
632 | ||
f67539c2 TL |
633 | @CLIWriteCommand('dashboard ac-role-delete') |
634 | def ac_role_delete_cmd(_, rolename: str): | |
635 | ''' | |
636 | Delete an access control role | |
637 | ''' | |
11fdf7f2 TL |
638 | try: |
639 | mgr.ACCESS_CTRL_DB.delete_role(rolename) | |
640 | mgr.ACCESS_CTRL_DB.save() | |
641 | return 0, "Role '{}' deleted".format(rolename), "" | |
642 | except RoleDoesNotExist as ex: | |
643 | if rolename in SYSTEM_ROLES: | |
644 | return -errno.EPERM, '', "Cannot delete system role '{}'" \ | |
f67539c2 | 645 | .format(rolename) |
11fdf7f2 TL |
646 | return -errno.ENOENT, '', str(ex) |
647 | except RoleIsAssociatedWithUser as ex: | |
648 | return -errno.EPERM, '', str(ex) | |
649 | ||
650 | ||
f67539c2 TL |
651 | @CLIWriteCommand('dashboard ac-role-add-scope-perms') |
652 | def ac_role_add_scope_perms_cmd(_, | |
653 | rolename: str, | |
654 | scopename: str, | |
655 | permissions: Sequence[str]): | |
656 | ''' | |
657 | Add the scope permissions for a role | |
658 | ''' | |
11fdf7f2 TL |
659 | try: |
660 | role = mgr.ACCESS_CTRL_DB.get_role(rolename) | |
661 | perms_array = [perm.strip() for perm in permissions] | |
662 | role.set_scope_permissions(scopename, perms_array) | |
663 | mgr.ACCESS_CTRL_DB.update_users_with_roles(role) | |
664 | mgr.ACCESS_CTRL_DB.save() | |
665 | return 0, json.dumps(role.to_dict()), '' | |
666 | except RoleDoesNotExist as ex: | |
667 | if rolename in SYSTEM_ROLES: | |
668 | return -errno.EPERM, '', "Cannot update system role '{}'" \ | |
f67539c2 | 669 | .format(rolename) |
11fdf7f2 TL |
670 | return -errno.ENOENT, '', str(ex) |
671 | except ScopeNotValid as ex: | |
672 | return -errno.EINVAL, '', str(ex) + "\n Possible values: {}" \ | |
673 | .format(Scope.all_scopes()) | |
674 | except PermissionNotValid as ex: | |
675 | return -errno.EINVAL, '', str(ex) + \ | |
f67539c2 TL |
676 | "\n Possible values: {}" \ |
677 | .format(Permission.all_permissions()) | |
11fdf7f2 TL |
678 | |
679 | ||
f67539c2 TL |
680 | @CLIWriteCommand('dashboard ac-role-del-scope-perms') |
681 | def ac_role_del_scope_perms_cmd(_, rolename: str, scopename: str): | |
682 | ''' | |
683 | Delete the scope permissions for a role | |
684 | ''' | |
11fdf7f2 TL |
685 | try: |
686 | role = mgr.ACCESS_CTRL_DB.get_role(rolename) | |
687 | role.del_scope_permissions(scopename) | |
688 | mgr.ACCESS_CTRL_DB.update_users_with_roles(role) | |
689 | mgr.ACCESS_CTRL_DB.save() | |
690 | return 0, json.dumps(role.to_dict()), '' | |
691 | except RoleDoesNotExist as ex: | |
692 | if rolename in SYSTEM_ROLES: | |
693 | return -errno.EPERM, '', "Cannot update system role '{}'" \ | |
f67539c2 | 694 | .format(rolename) |
11fdf7f2 TL |
695 | return -errno.ENOENT, '', str(ex) |
696 | except ScopeNotInRole as ex: | |
697 | return -errno.ENOENT, '', str(ex) | |
698 | ||
699 | ||
f67539c2 TL |
700 | @CLIReadCommand('dashboard ac-user-show') |
701 | def ac_user_show_cmd(_, username: Optional[str] = None): | |
702 | ''' | |
703 | Show user info | |
704 | ''' | |
11fdf7f2 TL |
705 | if not username: |
706 | users = mgr.ACCESS_CTRL_DB.users | |
707 | users_list = [name for name, _ in users.items()] | |
708 | return 0, json.dumps(users_list), '' | |
709 | try: | |
710 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
711 | return 0, json.dumps(user.to_dict()), '' | |
712 | except UserDoesNotExist as ex: | |
713 | return -errno.ENOENT, '', str(ex) | |
714 | ||
715 | ||
f67539c2 | 716 | @CLIWriteCommand('dashboard ac-user-create') |
522d829b | 717 | @CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC) |
f67539c2 TL |
718 | def ac_user_create_cmd(_, username: str, inbuf: str, |
719 | rolename: Optional[str] = None, | |
720 | name: Optional[str] = None, | |
721 | email: Optional[str] = None, | |
722 | enabled: bool = True, | |
723 | force_password: bool = False, | |
724 | pwd_expiration_date: Optional[int] = None, | |
725 | pwd_update_required: bool = False): | |
726 | ''' | |
727 | Create a user. Password read from -i <file> | |
728 | ''' | |
cd265ab1 | 729 | password = inbuf |
11fdf7f2 TL |
730 | try: |
731 | role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None | |
732 | except RoleDoesNotExist as ex: | |
733 | if rolename not in SYSTEM_ROLES: | |
734 | return -errno.ENOENT, '', str(ex) | |
735 | role = SYSTEM_ROLES[rolename] | |
736 | ||
737 | try: | |
9f95a23c TL |
738 | if not force_password: |
739 | pw_check = PasswordPolicy(password, username) | |
740 | pw_check.check_all() | |
741 | user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email, | |
742 | enabled, pwd_expiration_date, | |
743 | pwd_update_required) | |
744 | except PasswordPolicyException as ex: | |
745 | return -errno.EINVAL, '', str(ex) | |
11fdf7f2 | 746 | except UserAlreadyExists as ex: |
801d1391 | 747 | return 0, str(ex), '' |
11fdf7f2 TL |
748 | |
749 | if role: | |
750 | user.set_roles([role]) | |
751 | mgr.ACCESS_CTRL_DB.save() | |
752 | return 0, json.dumps(user.to_dict()), '' | |
753 | ||
754 | ||
f67539c2 TL |
755 | @CLIWriteCommand('dashboard ac-user-enable') |
756 | def ac_user_enable(_, username: str): | |
757 | ''' | |
758 | Enable a user | |
759 | ''' | |
9f95a23c TL |
760 | try: |
761 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
762 | user.enabled = True | |
adb31ebb | 763 | mgr.ACCESS_CTRL_DB.reset_attempt(username) |
9f95a23c TL |
764 | |
765 | mgr.ACCESS_CTRL_DB.save() | |
766 | return 0, json.dumps(user.to_dict()), '' | |
767 | except UserDoesNotExist as ex: | |
768 | return -errno.ENOENT, '', str(ex) | |
769 | ||
770 | ||
f67539c2 TL |
771 | @CLIWriteCommand('dashboard ac-user-disable') |
772 | def ac_user_disable(_, username: str): | |
773 | ''' | |
774 | Disable a user | |
775 | ''' | |
9f95a23c TL |
776 | try: |
777 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
778 | user.enabled = False | |
779 | ||
780 | mgr.ACCESS_CTRL_DB.save() | |
781 | return 0, json.dumps(user.to_dict()), '' | |
782 | except UserDoesNotExist as ex: | |
783 | return -errno.ENOENT, '', str(ex) | |
784 | ||
785 | ||
f67539c2 TL |
786 | @CLIWriteCommand('dashboard ac-user-delete') |
787 | def ac_user_delete_cmd(_, username: str): | |
788 | ''' | |
789 | Delete user | |
790 | ''' | |
11fdf7f2 TL |
791 | try: |
792 | mgr.ACCESS_CTRL_DB.delete_user(username) | |
793 | mgr.ACCESS_CTRL_DB.save() | |
794 | return 0, "User '{}' deleted".format(username), "" | |
795 | except UserDoesNotExist as ex: | |
796 | return -errno.ENOENT, '', str(ex) | |
797 | ||
798 | ||
f67539c2 TL |
799 | @CLIWriteCommand('dashboard ac-user-set-roles') |
800 | def ac_user_set_roles_cmd(_, username: str, roles: Sequence[str]): | |
801 | ''' | |
802 | Set user roles | |
803 | ''' | |
11fdf7f2 | 804 | rolesname = roles |
f67539c2 | 805 | roles: List[Role] = [] |
11fdf7f2 TL |
806 | for rolename in rolesname: |
807 | try: | |
808 | roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename)) | |
809 | except RoleDoesNotExist as ex: | |
810 | if rolename not in SYSTEM_ROLES: | |
811 | return -errno.ENOENT, '', str(ex) | |
812 | roles.append(SYSTEM_ROLES[rolename]) | |
813 | try: | |
814 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
815 | user.set_roles(roles) | |
816 | mgr.ACCESS_CTRL_DB.save() | |
817 | return 0, json.dumps(user.to_dict()), '' | |
818 | except UserDoesNotExist as ex: | |
819 | return -errno.ENOENT, '', str(ex) | |
820 | ||
821 | ||
f67539c2 TL |
822 | @CLIWriteCommand('dashboard ac-user-add-roles') |
823 | def ac_user_add_roles_cmd(_, username: str, roles: Sequence[str]): | |
824 | ''' | |
825 | Add roles to user | |
826 | ''' | |
11fdf7f2 | 827 | rolesname = roles |
f67539c2 | 828 | roles: List[Role] = [] |
11fdf7f2 TL |
829 | for rolename in rolesname: |
830 | try: | |
831 | roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename)) | |
832 | except RoleDoesNotExist as ex: | |
833 | if rolename not in SYSTEM_ROLES: | |
834 | return -errno.ENOENT, '', str(ex) | |
835 | roles.append(SYSTEM_ROLES[rolename]) | |
836 | try: | |
837 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
838 | user.add_roles(roles) | |
839 | mgr.ACCESS_CTRL_DB.save() | |
840 | return 0, json.dumps(user.to_dict()), '' | |
841 | except UserDoesNotExist as ex: | |
842 | return -errno.ENOENT, '', str(ex) | |
843 | ||
844 | ||
f67539c2 TL |
845 | @CLIWriteCommand('dashboard ac-user-del-roles') |
846 | def ac_user_del_roles_cmd(_, username: str, roles: Sequence[str]): | |
847 | ''' | |
848 | Delete roles from user | |
849 | ''' | |
11fdf7f2 | 850 | rolesname = roles |
f67539c2 | 851 | roles: List[Role] = [] |
11fdf7f2 TL |
852 | for rolename in rolesname: |
853 | try: | |
854 | roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename)) | |
855 | except RoleDoesNotExist as ex: | |
856 | if rolename not in SYSTEM_ROLES: | |
857 | return -errno.ENOENT, '', str(ex) | |
858 | roles.append(SYSTEM_ROLES[rolename]) | |
859 | try: | |
860 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
861 | user.del_roles(roles) | |
862 | mgr.ACCESS_CTRL_DB.save() | |
863 | return 0, json.dumps(user.to_dict()), '' | |
864 | except UserDoesNotExist as ex: | |
865 | return -errno.ENOENT, '', str(ex) | |
866 | except RoleNotInUser as ex: | |
867 | return -errno.ENOENT, '', str(ex) | |
868 | ||
869 | ||
f67539c2 | 870 | @CLIWriteCommand('dashboard ac-user-set-password') |
522d829b | 871 | @CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC) |
f67539c2 TL |
872 | def ac_user_set_password(_, username: str, inbuf: str, |
873 | force_password: bool = False): | |
874 | ''' | |
875 | Set user password from -i <file> | |
876 | ''' | |
cd265ab1 | 877 | password = inbuf |
11fdf7f2 TL |
878 | try: |
879 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
9f95a23c TL |
880 | if not force_password: |
881 | pw_check = PasswordPolicy(password, user.name) | |
882 | pw_check.check_all() | |
11fdf7f2 | 883 | user.set_password(password) |
9f95a23c TL |
884 | mgr.ACCESS_CTRL_DB.save() |
885 | return 0, json.dumps(user.to_dict()), '' | |
886 | except PasswordPolicyException as ex: | |
887 | return -errno.EINVAL, '', str(ex) | |
888 | except UserDoesNotExist as ex: | |
889 | return -errno.ENOENT, '', str(ex) | |
890 | ||
891 | ||
f67539c2 | 892 | @CLIWriteCommand('dashboard ac-user-set-password-hash') |
522d829b | 893 | @CLICheckNonemptyFileInput(desc=DEFAULT_FILE_DESC) |
f67539c2 TL |
894 | def ac_user_set_password_hash(_, username: str, inbuf: str): |
895 | ''' | |
896 | Set user password bcrypt hash from -i <file> | |
897 | ''' | |
cd265ab1 | 898 | hashed_password = inbuf |
9f95a23c TL |
899 | try: |
900 | # make sure the hashed_password is actually a bcrypt hash | |
901 | bcrypt.checkpw(b'', hashed_password.encode('utf-8')) | |
902 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
903 | user.set_password_hash(hashed_password) | |
11fdf7f2 TL |
904 | |
905 | mgr.ACCESS_CTRL_DB.save() | |
906 | return 0, json.dumps(user.to_dict()), '' | |
9f95a23c TL |
907 | except ValueError: |
908 | return -errno.EINVAL, '', 'Invalid password hash' | |
11fdf7f2 TL |
909 | except UserDoesNotExist as ex: |
910 | return -errno.ENOENT, '', str(ex) | |
911 | ||
912 | ||
f67539c2 TL |
913 | @CLIWriteCommand('dashboard ac-user-set-info') |
914 | def ac_user_set_info(_, username: str, name: str, email: str): | |
915 | ''' | |
916 | Set user info | |
917 | ''' | |
11fdf7f2 TL |
918 | try: |
919 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
920 | if name: | |
921 | user.name = name | |
922 | if email: | |
923 | user.email = email | |
924 | mgr.ACCESS_CTRL_DB.save() | |
925 | return 0, json.dumps(user.to_dict()), '' | |
926 | except UserDoesNotExist as ex: | |
927 | return -errno.ENOENT, '', str(ex) | |
928 | ||
929 | ||
930 | class LocalAuthenticator(object): | |
931 | def __init__(self): | |
932 | load_access_control_db() | |
933 | ||
934 | def get_user(self, username): | |
935 | return mgr.ACCESS_CTRL_DB.get_user(username) | |
936 | ||
937 | def authenticate(self, username, password): | |
938 | try: | |
939 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
940 | if user.password: | |
9f95a23c TL |
941 | if user.enabled and user.compare_password(password) \ |
942 | and not user.is_pwd_expired(): | |
943 | return {'permissions': user.permissions_dict(), | |
944 | 'pwdExpirationDate': user.pwd_expiration_date, | |
945 | 'pwdUpdateRequired': user.pwd_update_required} | |
11fdf7f2 TL |
946 | except UserDoesNotExist: |
947 | logger.debug("User '%s' does not exist", username) | |
948 | return None | |
949 | ||
950 | def authorize(self, username, scope, permissions): | |
951 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
952 | return user.authorize(scope, permissions) |