]>
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() | |
222 | if scope_name != Scope.DASHBOARD_SETTINGS | |
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 | ||
520 | def check_and_update_db(self): | |
9f95a23c TL |
521 | logger.debug("Checking for previews DB versions") |
522 | ||
523 | def check_migrate_v0_to_current(): | |
11fdf7f2 TL |
524 | # check if there is username/password from previous version |
525 | username = mgr.get_module_option('username', None) | |
526 | password = mgr.get_module_option('password', None) | |
527 | if username and password: | |
9f95a23c | 528 | logger.debug("Found single user credentials: user=%s", username) |
11fdf7f2 TL |
529 | # found user credentials |
530 | user = self.create_user(username, "", None, None) | |
531 | # password is already hashed, so setting manually | |
532 | user.password = password | |
533 | user.add_roles([ADMIN_ROLE]) | |
534 | self.save() | |
9f95a23c TL |
535 | |
536 | def check_migrate_v1_to_current(): | |
537 | # Check if version 1 exists in the DB and migrate it to current version | |
538 | v1_db = mgr.get_store(self.accessdb_config_key(1)) | |
539 | if v1_db: | |
540 | logger.debug("Found database v1 credentials") | |
541 | v1_db = json.loads(v1_db) | |
542 | ||
543 | for user, _ in v1_db['users'].items(): | |
544 | v1_db['users'][user]['enabled'] = True | |
545 | v1_db['users'][user]['pwdExpirationDate'] = None | |
546 | v1_db['users'][user]['pwdUpdateRequired'] = False | |
547 | ||
548 | self.roles = {rn: Role.from_dict(r) for rn, r in v1_db.get('roles', {}).items()} | |
549 | self.users = {un: User.from_dict(u, dict(self.roles, **SYSTEM_ROLES)) | |
550 | for un, u in v1_db.get('users', {}).items()} | |
551 | ||
552 | self.save() | |
553 | else: | |
554 | # If version 1 does not exist, check if migration of VERSION "0" needs to be done | |
555 | check_migrate_v0_to_current() | |
556 | ||
557 | check_migrate_v1_to_current() | |
11fdf7f2 TL |
558 | |
559 | @classmethod | |
560 | def load(cls): | |
9f95a23c | 561 | logger.info("Loading user roles DB version=%s", cls.VERSION) |
11fdf7f2 TL |
562 | |
563 | json_db = mgr.get_store(cls.accessdb_config_key()) | |
564 | if json_db is None: | |
9f95a23c | 565 | logger.debug("No DB v%s found, creating new...", cls.VERSION) |
11fdf7f2 TL |
566 | db = cls(cls.VERSION, {}, {}) |
567 | # check if we can update from a previous version database | |
568 | db.check_and_update_db() | |
569 | return db | |
570 | ||
9f95a23c | 571 | dict_db = json.loads(json_db) |
11fdf7f2 | 572 | roles = {rn: Role.from_dict(r) |
9f95a23c | 573 | for rn, r in dict_db.get('roles', {}).items()} |
11fdf7f2 | 574 | users = {un: User.from_dict(u, dict(roles, **SYSTEM_ROLES)) |
9f95a23c TL |
575 | for un, u in dict_db.get('users', {}).items()} |
576 | return cls(dict_db['version'], users, roles) | |
11fdf7f2 TL |
577 | |
578 | ||
579 | def load_access_control_db(): | |
580 | mgr.ACCESS_CTRL_DB = AccessControlDB.load() | |
581 | ||
582 | ||
583 | # CLI dashboard access control scope commands | |
584 | ||
585 | @CLIWriteCommand('dashboard set-login-credentials', | |
cd265ab1 TL |
586 | 'name=username,type=CephString', |
587 | 'Set the login credentials. Password read from -i <file>') | |
588 | @CLICheckNonemptyFileInput | |
589 | def set_login_credentials_cmd(_, username, inbuf): | |
590 | password = inbuf | |
11fdf7f2 TL |
591 | try: |
592 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
593 | user.set_password(password) | |
594 | except UserDoesNotExist: | |
595 | user = mgr.ACCESS_CTRL_DB.create_user(username, password, None, None) | |
596 | user.set_roles([ADMIN_ROLE]) | |
597 | ||
598 | mgr.ACCESS_CTRL_DB.save() | |
599 | ||
600 | return 0, '''\ | |
601 | ****************************************************************** | |
602 | *** WARNING: this command is deprecated. *** | |
603 | *** Please use the ac-user-* related commands to manage users. *** | |
604 | ****************************************************************** | |
605 | Username and password updated''', '' | |
606 | ||
607 | ||
608 | @CLIReadCommand('dashboard ac-role-show', | |
609 | 'name=rolename,type=CephString,req=false', | |
610 | 'Show role info') | |
611 | def ac_role_show_cmd(_, rolename=None): | |
612 | if not rolename: | |
613 | roles = dict(mgr.ACCESS_CTRL_DB.roles) | |
614 | roles.update(SYSTEM_ROLES) | |
615 | roles_list = [name for name, _ in roles.items()] | |
616 | return 0, json.dumps(roles_list), '' | |
617 | try: | |
618 | role = mgr.ACCESS_CTRL_DB.get_role(rolename) | |
619 | except RoleDoesNotExist as ex: | |
620 | if rolename not in SYSTEM_ROLES: | |
621 | return -errno.ENOENT, '', str(ex) | |
622 | role = SYSTEM_ROLES[rolename] | |
623 | return 0, json.dumps(role.to_dict()), '' | |
624 | ||
625 | ||
626 | @CLIWriteCommand('dashboard ac-role-create', | |
627 | 'name=rolename,type=CephString ' | |
628 | 'name=description,type=CephString,req=false', | |
629 | 'Create a new access control role') | |
630 | def ac_role_create_cmd(_, rolename, description=None): | |
631 | try: | |
632 | role = mgr.ACCESS_CTRL_DB.create_role(rolename, description) | |
633 | mgr.ACCESS_CTRL_DB.save() | |
634 | return 0, json.dumps(role.to_dict()), '' | |
635 | except RoleAlreadyExists as ex: | |
636 | return -errno.EEXIST, '', str(ex) | |
637 | ||
638 | ||
639 | @CLIWriteCommand('dashboard ac-role-delete', | |
640 | 'name=rolename,type=CephString', | |
641 | 'Delete an access control role') | |
642 | def ac_role_delete_cmd(_, rolename): | |
643 | try: | |
644 | mgr.ACCESS_CTRL_DB.delete_role(rolename) | |
645 | mgr.ACCESS_CTRL_DB.save() | |
646 | return 0, "Role '{}' deleted".format(rolename), "" | |
647 | except RoleDoesNotExist as ex: | |
648 | if rolename in SYSTEM_ROLES: | |
649 | return -errno.EPERM, '', "Cannot delete system role '{}'" \ | |
650 | .format(rolename) | |
651 | return -errno.ENOENT, '', str(ex) | |
652 | except RoleIsAssociatedWithUser as ex: | |
653 | return -errno.EPERM, '', str(ex) | |
654 | ||
655 | ||
656 | @CLIWriteCommand('dashboard ac-role-add-scope-perms', | |
657 | 'name=rolename,type=CephString ' | |
658 | 'name=scopename,type=CephString ' | |
659 | 'name=permissions,type=CephString,n=N', | |
660 | 'Add the scope permissions for a role') | |
661 | def ac_role_add_scope_perms_cmd(_, rolename, scopename, permissions): | |
662 | try: | |
663 | role = mgr.ACCESS_CTRL_DB.get_role(rolename) | |
664 | perms_array = [perm.strip() for perm in permissions] | |
665 | role.set_scope_permissions(scopename, perms_array) | |
666 | mgr.ACCESS_CTRL_DB.update_users_with_roles(role) | |
667 | mgr.ACCESS_CTRL_DB.save() | |
668 | return 0, json.dumps(role.to_dict()), '' | |
669 | except RoleDoesNotExist as ex: | |
670 | if rolename in SYSTEM_ROLES: | |
671 | return -errno.EPERM, '', "Cannot update system role '{}'" \ | |
672 | .format(rolename) | |
673 | return -errno.ENOENT, '', str(ex) | |
674 | except ScopeNotValid as ex: | |
675 | return -errno.EINVAL, '', str(ex) + "\n Possible values: {}" \ | |
676 | .format(Scope.all_scopes()) | |
677 | except PermissionNotValid as ex: | |
678 | return -errno.EINVAL, '', str(ex) + \ | |
679 | "\n Possible values: {}" \ | |
680 | .format(Permission.all_permissions()) | |
681 | ||
682 | ||
683 | @CLIWriteCommand('dashboard ac-role-del-scope-perms', | |
684 | 'name=rolename,type=CephString ' | |
685 | 'name=scopename,type=CephString', | |
686 | 'Delete the scope permissions for a role') | |
687 | def ac_role_del_scope_perms_cmd(_, rolename, scopename): | |
688 | try: | |
689 | role = mgr.ACCESS_CTRL_DB.get_role(rolename) | |
690 | role.del_scope_permissions(scopename) | |
691 | mgr.ACCESS_CTRL_DB.update_users_with_roles(role) | |
692 | mgr.ACCESS_CTRL_DB.save() | |
693 | return 0, json.dumps(role.to_dict()), '' | |
694 | except RoleDoesNotExist as ex: | |
695 | if rolename in SYSTEM_ROLES: | |
696 | return -errno.EPERM, '', "Cannot update system role '{}'" \ | |
697 | .format(rolename) | |
698 | return -errno.ENOENT, '', str(ex) | |
699 | except ScopeNotInRole as ex: | |
700 | return -errno.ENOENT, '', str(ex) | |
701 | ||
702 | ||
703 | @CLIReadCommand('dashboard ac-user-show', | |
704 | 'name=username,type=CephString,req=false', | |
705 | 'Show user info') | |
706 | def ac_user_show_cmd(_, username=None): | |
707 | if not username: | |
708 | users = mgr.ACCESS_CTRL_DB.users | |
709 | users_list = [name for name, _ in users.items()] | |
710 | return 0, json.dumps(users_list), '' | |
711 | try: | |
712 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
713 | return 0, json.dumps(user.to_dict()), '' | |
714 | except UserDoesNotExist as ex: | |
715 | return -errno.ENOENT, '', str(ex) | |
716 | ||
717 | ||
718 | @CLIWriteCommand('dashboard ac-user-create', | |
719 | 'name=username,type=CephString ' | |
11fdf7f2 TL |
720 | 'name=rolename,type=CephString,req=false ' |
721 | 'name=name,type=CephString,req=false ' | |
9f95a23c TL |
722 | 'name=email,type=CephString,req=false ' |
723 | 'name=enabled,type=CephBool,req=false ' | |
724 | 'name=force_password,type=CephBool,req=false ' | |
725 | 'name=pwd_expiration_date,type=CephInt,req=false ' | |
726 | 'name=pwd_update_required,type=CephBool,req=false', | |
cd265ab1 TL |
727 | 'Create a user. Password read from -i <file>') |
728 | @CLICheckNonemptyFileInput | |
729 | def ac_user_create_cmd(_, username, inbuf, rolename=None, name=None, | |
9f95a23c TL |
730 | email=None, enabled=True, force_password=False, |
731 | pwd_expiration_date=None, pwd_update_required=False): | |
cd265ab1 | 732 | password = inbuf |
11fdf7f2 TL |
733 | try: |
734 | role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None | |
735 | except RoleDoesNotExist as ex: | |
736 | if rolename not in SYSTEM_ROLES: | |
737 | return -errno.ENOENT, '', str(ex) | |
738 | role = SYSTEM_ROLES[rolename] | |
739 | ||
740 | try: | |
9f95a23c TL |
741 | if not force_password: |
742 | pw_check = PasswordPolicy(password, username) | |
743 | pw_check.check_all() | |
744 | user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email, | |
745 | enabled, pwd_expiration_date, | |
746 | pwd_update_required) | |
747 | except PasswordPolicyException as ex: | |
748 | return -errno.EINVAL, '', str(ex) | |
11fdf7f2 | 749 | except UserAlreadyExists as ex: |
801d1391 | 750 | return 0, str(ex), '' |
11fdf7f2 TL |
751 | |
752 | if role: | |
753 | user.set_roles([role]) | |
754 | mgr.ACCESS_CTRL_DB.save() | |
755 | return 0, json.dumps(user.to_dict()), '' | |
756 | ||
757 | ||
9f95a23c TL |
758 | @CLIWriteCommand('dashboard ac-user-enable', |
759 | 'name=username,type=CephString', | |
760 | 'Enable a user') | |
761 | def ac_user_enable(_, username): | |
762 | try: | |
763 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
764 | user.enabled = True | |
adb31ebb | 765 | mgr.ACCESS_CTRL_DB.reset_attempt(username) |
9f95a23c TL |
766 | |
767 | mgr.ACCESS_CTRL_DB.save() | |
768 | return 0, json.dumps(user.to_dict()), '' | |
769 | except UserDoesNotExist as ex: | |
770 | return -errno.ENOENT, '', str(ex) | |
771 | ||
772 | ||
773 | @CLIWriteCommand('dashboard ac-user-disable', | |
774 | 'name=username,type=CephString', | |
775 | 'Disable a user') | |
776 | def ac_user_disable(_, username): | |
777 | try: | |
778 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
779 | user.enabled = False | |
780 | ||
781 | mgr.ACCESS_CTRL_DB.save() | |
782 | return 0, json.dumps(user.to_dict()), '' | |
783 | except UserDoesNotExist as ex: | |
784 | return -errno.ENOENT, '', str(ex) | |
785 | ||
786 | ||
11fdf7f2 TL |
787 | @CLIWriteCommand('dashboard ac-user-delete', |
788 | 'name=username,type=CephString', | |
789 | 'Delete user') | |
790 | def ac_user_delete_cmd(_, username): | |
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 | ||
799 | @CLIWriteCommand('dashboard ac-user-set-roles', | |
800 | 'name=username,type=CephString ' | |
801 | 'name=roles,type=CephString,n=N', | |
802 | 'Set user roles') | |
803 | def ac_user_set_roles_cmd(_, username, roles): | |
804 | rolesname = roles | |
805 | roles = [] | |
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 | ||
822 | @CLIWriteCommand('dashboard ac-user-add-roles', | |
823 | 'name=username,type=CephString ' | |
824 | 'name=roles,type=CephString,n=N', | |
825 | 'Add roles to user') | |
826 | def ac_user_add_roles_cmd(_, username, roles): | |
827 | rolesname = roles | |
828 | roles = [] | |
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 | ||
845 | @CLIWriteCommand('dashboard ac-user-del-roles', | |
846 | 'name=username,type=CephString ' | |
847 | 'name=roles,type=CephString,n=N', | |
848 | 'Delete roles from user') | |
849 | def ac_user_del_roles_cmd(_, username, roles): | |
850 | rolesname = roles | |
851 | roles = [] | |
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 | ||
870 | @CLIWriteCommand('dashboard ac-user-set-password', | |
871 | 'name=username,type=CephString ' | |
9f95a23c | 872 | 'name=force_password,type=CephBool,req=false', |
cd265ab1 TL |
873 | 'Set user password from -i <file>') |
874 | @CLICheckNonemptyFileInput | |
875 | def ac_user_set_password(_, username, inbuf, force_password=False): | |
876 | password = inbuf | |
11fdf7f2 TL |
877 | try: |
878 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
9f95a23c TL |
879 | if not force_password: |
880 | pw_check = PasswordPolicy(password, user.name) | |
881 | pw_check.check_all() | |
11fdf7f2 | 882 | user.set_password(password) |
9f95a23c TL |
883 | mgr.ACCESS_CTRL_DB.save() |
884 | return 0, json.dumps(user.to_dict()), '' | |
885 | except PasswordPolicyException as ex: | |
886 | return -errno.EINVAL, '', str(ex) | |
887 | except UserDoesNotExist as ex: | |
888 | return -errno.ENOENT, '', str(ex) | |
889 | ||
890 | ||
891 | @CLIWriteCommand('dashboard ac-user-set-password-hash', | |
cd265ab1 TL |
892 | 'name=username,type=CephString', |
893 | 'Set user password bcrypt hash from -i <file>') | |
894 | @CLICheckNonemptyFileInput | |
895 | def ac_user_set_password_hash(_, username, inbuf): | |
896 | hashed_password = inbuf | |
9f95a23c TL |
897 | try: |
898 | # make sure the hashed_password is actually a bcrypt hash | |
899 | bcrypt.checkpw(b'', hashed_password.encode('utf-8')) | |
900 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
901 | user.set_password_hash(hashed_password) | |
11fdf7f2 TL |
902 | |
903 | mgr.ACCESS_CTRL_DB.save() | |
904 | return 0, json.dumps(user.to_dict()), '' | |
9f95a23c TL |
905 | except ValueError: |
906 | return -errno.EINVAL, '', 'Invalid password hash' | |
11fdf7f2 TL |
907 | except UserDoesNotExist as ex: |
908 | return -errno.ENOENT, '', str(ex) | |
909 | ||
910 | ||
911 | @CLIWriteCommand('dashboard ac-user-set-info', | |
912 | 'name=username,type=CephString ' | |
913 | 'name=name,type=CephString ' | |
914 | 'name=email,type=CephString', | |
915 | 'Set user info') | |
916 | def ac_user_set_info(_, username, name, email): | |
917 | try: | |
918 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
919 | if name: | |
920 | user.name = name | |
921 | if email: | |
922 | user.email = email | |
923 | mgr.ACCESS_CTRL_DB.save() | |
924 | return 0, json.dumps(user.to_dict()), '' | |
925 | except UserDoesNotExist as ex: | |
926 | return -errno.ENOENT, '', str(ex) | |
927 | ||
928 | ||
929 | class LocalAuthenticator(object): | |
930 | def __init__(self): | |
931 | load_access_control_db() | |
932 | ||
933 | def get_user(self, username): | |
934 | return mgr.ACCESS_CTRL_DB.get_user(username) | |
935 | ||
936 | def authenticate(self, username, password): | |
937 | try: | |
938 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
939 | if user.password: | |
9f95a23c TL |
940 | if user.enabled and user.compare_password(password) \ |
941 | and not user.is_pwd_expired(): | |
942 | return {'permissions': user.permissions_dict(), | |
943 | 'pwdExpirationDate': user.pwd_expiration_date, | |
944 | 'pwdUpdateRequired': user.pwd_update_required} | |
11fdf7f2 TL |
945 | except UserDoesNotExist: |
946 | logger.debug("User '%s' does not exist", username) | |
947 | return None | |
948 | ||
949 | def authorize(self, username, scope, permissions): | |
950 | user = mgr.ACCESS_CTRL_DB.get_user(username) | |
951 | return user.authorize(scope, permissions) |