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