]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/services/access_control.py
import ceph nautilus 14.2.2
[ceph.git] / ceph / src / pybind / mgr / dashboard / services / access_control.py
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
6 import errno
7 import json
8 import threading
9 import time
10
11 import bcrypt
12
13 from mgr_module import CLIReadCommand, CLIWriteCommand
14
15 from .. import mgr, logger
16 from ..security import Scope, Permission
17 from ..exceptions import RoleAlreadyExists, RoleDoesNotExist, ScopeNotValid, \
18 PermissionNotValid, RoleIsAssociatedWithUser, \
19 UserAlreadyExists, UserDoesNotExist, ScopeNotInRole, \
20 RoleNotInUser
21
22
23 # password hashing algorithm
24 def password_hash(password, salt_password=None):
25 if not password:
26 return None
27 if not salt_password:
28 salt_password = bcrypt.gensalt()
29 else:
30 salt_password = salt_password.encode('utf8')
31 return bcrypt.hashpw(password.encode('utf8'), salt_password).decode('utf8')
32
33
34 _P = Permission # short alias
35
36
37 class Role(object):
38 def __init__(self, name, description=None, scope_permissions=None):
39 self.name = name
40 self.description = description
41 if scope_permissions is None:
42 self.scopes_permissions = {}
43 else:
44 self.scopes_permissions = scope_permissions
45
46 def __hash__(self):
47 return hash(self.name)
48
49 def __eq__(self, other):
50 return self.name == other.name
51
52 def set_scope_permissions(self, scope, permissions):
53 if not Scope.valid_scope(scope):
54 raise ScopeNotValid(scope)
55 for perm in permissions:
56 if not Permission.valid_permission(perm):
57 raise PermissionNotValid(perm)
58
59 permissions.sort()
60 self.scopes_permissions[scope] = permissions
61
62 def del_scope_permissions(self, scope):
63 if scope not in self.scopes_permissions:
64 raise ScopeNotInRole(scope, self.name)
65 del self.scopes_permissions[scope]
66
67 def reset_scope_permissions(self):
68 self.scopes_permissions = {}
69
70 def authorize(self, scope, permissions):
71 if scope in self.scopes_permissions:
72 role_perms = self.scopes_permissions[scope]
73 for perm in permissions:
74 if perm not in role_perms:
75 return False
76 return True
77 return False
78
79 def to_dict(self):
80 return {
81 'name': self.name,
82 'description': self.description,
83 'scopes_permissions': self.scopes_permissions
84 }
85
86 @classmethod
87 def from_dict(cls, r_dict):
88 return Role(r_dict['name'], r_dict['description'],
89 r_dict['scopes_permissions'])
90
91
92 # static pre-defined system roles
93 # this roles cannot be deleted nor updated
94
95 # admin role provides all permissions for all scopes
96 ADMIN_ROLE = Role('administrator', 'Administrator', {
97 scope_name: Permission.all_permissions()
98 for scope_name in Scope.all_scopes()
99 })
100
101
102 # read-only role provides read-only permission for all scopes
103 READ_ONLY_ROLE = Role('read-only', 'Read-Only', {
104 scope_name: [_P.READ] for scope_name in Scope.all_scopes()
105 if scope_name != Scope.DASHBOARD_SETTINGS
106 })
107
108
109 # block manager role provides all permission for block related scopes
110 BLOCK_MGR_ROLE = Role('block-manager', 'Block Manager', {
111 Scope.RBD_IMAGE: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
112 Scope.POOL: [_P.READ],
113 Scope.ISCSI: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
114 Scope.RBD_MIRRORING: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
115 })
116
117
118 # RadosGW manager role provides all permissions for block related scopes
119 RGW_MGR_ROLE = Role('rgw-manager', 'RGW Manager', {
120 Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
121 Scope.CONFIG_OPT: [_P.READ],
122 })
123
124
125 # Cluster manager role provides all permission for OSDs, Monitors, and
126 # Config options
127 CLUSTER_MGR_ROLE = Role('cluster-manager', 'Cluster Manager', {
128 Scope.HOSTS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
129 Scope.OSD: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
130 Scope.MONITOR: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
131 Scope.MANAGER: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
132 Scope.CONFIG_OPT: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
133 Scope.LOG: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
134 })
135
136
137 # Pool manager role provides all permissions for pool related scopes
138 POOL_MGR_ROLE = Role('pool-manager', 'Pool Manager', {
139 Scope.POOL: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
140 Scope.CONFIG_OPT: [_P.READ],
141 })
142
143 # Pool manager role provides all permissions for CephFS related scopes
144 CEPHFS_MGR_ROLE = Role('cephfs-manager', 'CephFS Manager', {
145 Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
146 Scope.CONFIG_OPT: [_P.READ],
147 })
148
149 GANESHA_MGR_ROLE = Role('ganesha-manager', 'NFS Ganesha Manager', {
150 Scope.NFS_GANESHA: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
151 Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
152 Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE],
153 Scope.CONFIG_OPT: [_P.READ],
154 })
155
156
157 SYSTEM_ROLES = {
158 ADMIN_ROLE.name: ADMIN_ROLE,
159 READ_ONLY_ROLE.name: READ_ONLY_ROLE,
160 BLOCK_MGR_ROLE.name: BLOCK_MGR_ROLE,
161 RGW_MGR_ROLE.name: RGW_MGR_ROLE,
162 CLUSTER_MGR_ROLE.name: CLUSTER_MGR_ROLE,
163 POOL_MGR_ROLE.name: POOL_MGR_ROLE,
164 CEPHFS_MGR_ROLE.name: CEPHFS_MGR_ROLE,
165 GANESHA_MGR_ROLE.name: GANESHA_MGR_ROLE,
166 }
167
168
169 class User(object):
170 def __init__(self, username, password, name=None, email=None, roles=None,
171 lastUpdate=None):
172 self.username = username
173 self.password = password
174 self.name = name
175 self.email = email
176 if roles is None:
177 self.roles = set()
178 else:
179 self.roles = roles
180 if lastUpdate is None:
181 self.refreshLastUpdate()
182 else:
183 self.lastUpdate = lastUpdate
184
185 def refreshLastUpdate(self):
186 self.lastUpdate = int(time.time())
187
188 def set_password(self, password):
189 self.password = password_hash(password)
190 self.refreshLastUpdate()
191
192 def set_roles(self, roles):
193 self.roles = set(roles)
194 self.refreshLastUpdate()
195
196 def add_roles(self, roles):
197 self.roles = self.roles.union(set(roles))
198 self.refreshLastUpdate()
199
200 def del_roles(self, roles):
201 for role in roles:
202 if role not in self.roles:
203 raise RoleNotInUser(role.name, self.username)
204 self.roles.difference_update(set(roles))
205 self.refreshLastUpdate()
206
207 def authorize(self, scope, permissions):
208 for role in self.roles:
209 if role.authorize(scope, permissions):
210 return True
211 return False
212
213 def permissions_dict(self):
214 perms = {}
215 for role in self.roles:
216 for scope, perms_list in role.scopes_permissions.items():
217 if scope in perms:
218 perms_tmp = set(perms[scope]).union(set(perms_list))
219 perms[scope] = list(perms_tmp)
220 else:
221 perms[scope] = perms_list
222
223 return perms
224
225 def to_dict(self):
226 return {
227 'username': self.username,
228 'password': self.password,
229 'roles': sorted([r.name for r in self.roles]),
230 'name': self.name,
231 'email': self.email,
232 'lastUpdate': self.lastUpdate
233 }
234
235 @classmethod
236 def from_dict(cls, u_dict, roles):
237 return User(u_dict['username'], u_dict['password'], u_dict['name'],
238 u_dict['email'], {roles[r] for r in u_dict['roles']},
239 u_dict['lastUpdate'])
240
241
242 class AccessControlDB(object):
243 VERSION = 1
244 ACDB_CONFIG_KEY = "accessdb_v"
245
246 def __init__(self, version, users, roles):
247 self.users = users
248 self.version = version
249 self.roles = roles
250 self.lock = threading.RLock()
251
252 def create_role(self, name, description=None):
253 with self.lock:
254 if name in SYSTEM_ROLES or name in self.roles:
255 raise RoleAlreadyExists(name)
256 role = Role(name, description)
257 self.roles[name] = role
258 return role
259
260 def get_role(self, name):
261 with self.lock:
262 if name not in self.roles:
263 raise RoleDoesNotExist(name)
264 return self.roles[name]
265
266 def delete_role(self, name):
267 with self.lock:
268 if name not in self.roles:
269 raise RoleDoesNotExist(name)
270 role = self.roles[name]
271
272 # check if role is not associated with a user
273 for username, user in self.users.items():
274 if role in user.roles:
275 raise RoleIsAssociatedWithUser(name, username)
276
277 del self.roles[name]
278
279 def create_user(self, username, password, name, email):
280 logger.debug("AC: creating user: username=%s", username)
281 with self.lock:
282 if username in self.users:
283 raise UserAlreadyExists(username)
284 user = User(username, password_hash(password), name, email)
285 self.users[username] = user
286 return user
287
288 def get_user(self, username):
289 with self.lock:
290 if username not in self.users:
291 raise UserDoesNotExist(username)
292 return self.users[username]
293
294 def delete_user(self, username):
295 with self.lock:
296 if username not in self.users:
297 raise UserDoesNotExist(username)
298 del self.users[username]
299
300 def update_users_with_roles(self, role):
301 with self.lock:
302 if not role:
303 return
304 for _, user in self.users.items():
305 if role in user.roles:
306 user.refreshLastUpdate()
307
308 def save(self):
309 with self.lock:
310 db = {
311 'users': {un: u.to_dict() for un, u in self.users.items()},
312 'roles': {rn: r.to_dict() for rn, r in self.roles.items()},
313 'version': self.version
314 }
315 mgr.set_store(self.accessdb_config_key(), json.dumps(db))
316
317 @classmethod
318 def accessdb_config_key(cls, version=None):
319 if version is None:
320 version = cls.VERSION
321 return "{}{}".format(cls.ACDB_CONFIG_KEY, version)
322
323 def check_and_update_db(self):
324 logger.debug("AC: Checking for previews DB versions")
325 if self.VERSION == 1: # current version
326 # check if there is username/password from previous version
327 username = mgr.get_module_option('username', None)
328 password = mgr.get_module_option('password', None)
329 if username and password:
330 logger.debug("AC: Found single user credentials: user=%s",
331 username)
332 # found user credentials
333 user = self.create_user(username, "", None, None)
334 # password is already hashed, so setting manually
335 user.password = password
336 user.add_roles([ADMIN_ROLE])
337 self.save()
338 else:
339 raise NotImplementedError()
340
341 @classmethod
342 def load(cls):
343 logger.info("AC: Loading user roles DB version=%s", cls.VERSION)
344
345 json_db = mgr.get_store(cls.accessdb_config_key())
346 if json_db is None:
347 logger.debug("AC: No DB v%s found, creating new...", cls.VERSION)
348 db = cls(cls.VERSION, {}, {})
349 # check if we can update from a previous version database
350 db.check_and_update_db()
351 return db
352
353 db = json.loads(json_db)
354 roles = {rn: Role.from_dict(r)
355 for rn, r in db.get('roles', {}).items()}
356 users = {un: User.from_dict(u, dict(roles, **SYSTEM_ROLES))
357 for un, u in db.get('users', {}).items()}
358 return cls(db['version'], users, roles)
359
360
361 def load_access_control_db():
362 mgr.ACCESS_CTRL_DB = AccessControlDB.load()
363
364
365 # CLI dashboard access control scope commands
366
367 @CLIWriteCommand('dashboard set-login-credentials',
368 'name=username,type=CephString '
369 'name=password,type=CephString',
370 'Set the login credentials')
371 def set_login_credentials_cmd(_, username, password):
372 try:
373 user = mgr.ACCESS_CTRL_DB.get_user(username)
374 user.set_password(password)
375 except UserDoesNotExist:
376 user = mgr.ACCESS_CTRL_DB.create_user(username, password, None, None)
377 user.set_roles([ADMIN_ROLE])
378
379 mgr.ACCESS_CTRL_DB.save()
380
381 return 0, '''\
382 ******************************************************************
383 *** WARNING: this command is deprecated. ***
384 *** Please use the ac-user-* related commands to manage users. ***
385 ******************************************************************
386 Username and password updated''', ''
387
388
389 @CLIReadCommand('dashboard ac-role-show',
390 'name=rolename,type=CephString,req=false',
391 'Show role info')
392 def ac_role_show_cmd(_, rolename=None):
393 if not rolename:
394 roles = dict(mgr.ACCESS_CTRL_DB.roles)
395 roles.update(SYSTEM_ROLES)
396 roles_list = [name for name, _ in roles.items()]
397 return 0, json.dumps(roles_list), ''
398 try:
399 role = mgr.ACCESS_CTRL_DB.get_role(rolename)
400 except RoleDoesNotExist as ex:
401 if rolename not in SYSTEM_ROLES:
402 return -errno.ENOENT, '', str(ex)
403 role = SYSTEM_ROLES[rolename]
404 return 0, json.dumps(role.to_dict()), ''
405
406
407 @CLIWriteCommand('dashboard ac-role-create',
408 'name=rolename,type=CephString '
409 'name=description,type=CephString,req=false',
410 'Create a new access control role')
411 def ac_role_create_cmd(_, rolename, description=None):
412 try:
413 role = mgr.ACCESS_CTRL_DB.create_role(rolename, description)
414 mgr.ACCESS_CTRL_DB.save()
415 return 0, json.dumps(role.to_dict()), ''
416 except RoleAlreadyExists as ex:
417 return -errno.EEXIST, '', str(ex)
418
419
420 @CLIWriteCommand('dashboard ac-role-delete',
421 'name=rolename,type=CephString',
422 'Delete an access control role')
423 def ac_role_delete_cmd(_, rolename):
424 try:
425 mgr.ACCESS_CTRL_DB.delete_role(rolename)
426 mgr.ACCESS_CTRL_DB.save()
427 return 0, "Role '{}' deleted".format(rolename), ""
428 except RoleDoesNotExist as ex:
429 if rolename in SYSTEM_ROLES:
430 return -errno.EPERM, '', "Cannot delete system role '{}'" \
431 .format(rolename)
432 return -errno.ENOENT, '', str(ex)
433 except RoleIsAssociatedWithUser as ex:
434 return -errno.EPERM, '', str(ex)
435
436
437 @CLIWriteCommand('dashboard ac-role-add-scope-perms',
438 'name=rolename,type=CephString '
439 'name=scopename,type=CephString '
440 'name=permissions,type=CephString,n=N',
441 'Add the scope permissions for a role')
442 def ac_role_add_scope_perms_cmd(_, rolename, scopename, permissions):
443 try:
444 role = mgr.ACCESS_CTRL_DB.get_role(rolename)
445 perms_array = [perm.strip() for perm in permissions]
446 role.set_scope_permissions(scopename, perms_array)
447 mgr.ACCESS_CTRL_DB.update_users_with_roles(role)
448 mgr.ACCESS_CTRL_DB.save()
449 return 0, json.dumps(role.to_dict()), ''
450 except RoleDoesNotExist as ex:
451 if rolename in SYSTEM_ROLES:
452 return -errno.EPERM, '', "Cannot update system role '{}'" \
453 .format(rolename)
454 return -errno.ENOENT, '', str(ex)
455 except ScopeNotValid as ex:
456 return -errno.EINVAL, '', str(ex) + "\n Possible values: {}" \
457 .format(Scope.all_scopes())
458 except PermissionNotValid as ex:
459 return -errno.EINVAL, '', str(ex) + \
460 "\n Possible values: {}" \
461 .format(Permission.all_permissions())
462
463
464 @CLIWriteCommand('dashboard ac-role-del-scope-perms',
465 'name=rolename,type=CephString '
466 'name=scopename,type=CephString',
467 'Delete the scope permissions for a role')
468 def ac_role_del_scope_perms_cmd(_, rolename, scopename):
469 try:
470 role = mgr.ACCESS_CTRL_DB.get_role(rolename)
471 role.del_scope_permissions(scopename)
472 mgr.ACCESS_CTRL_DB.update_users_with_roles(role)
473 mgr.ACCESS_CTRL_DB.save()
474 return 0, json.dumps(role.to_dict()), ''
475 except RoleDoesNotExist as ex:
476 if rolename in SYSTEM_ROLES:
477 return -errno.EPERM, '', "Cannot update system role '{}'" \
478 .format(rolename)
479 return -errno.ENOENT, '', str(ex)
480 except ScopeNotInRole as ex:
481 return -errno.ENOENT, '', str(ex)
482
483
484 @CLIReadCommand('dashboard ac-user-show',
485 'name=username,type=CephString,req=false',
486 'Show user info')
487 def ac_user_show_cmd(_, username=None):
488 if not username:
489 users = mgr.ACCESS_CTRL_DB.users
490 users_list = [name for name, _ in users.items()]
491 return 0, json.dumps(users_list), ''
492 try:
493 user = mgr.ACCESS_CTRL_DB.get_user(username)
494 return 0, json.dumps(user.to_dict()), ''
495 except UserDoesNotExist as ex:
496 return -errno.ENOENT, '', str(ex)
497
498
499 @CLIWriteCommand('dashboard ac-user-create',
500 'name=username,type=CephString '
501 'name=password,type=CephString,req=false '
502 'name=rolename,type=CephString,req=false '
503 'name=name,type=CephString,req=false '
504 'name=email,type=CephString,req=false',
505 'Create a user')
506 def ac_user_create_cmd(_, username, password=None, rolename=None, name=None,
507 email=None):
508 try:
509 role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None
510 except RoleDoesNotExist as ex:
511 if rolename not in SYSTEM_ROLES:
512 return -errno.ENOENT, '', str(ex)
513 role = SYSTEM_ROLES[rolename]
514
515 try:
516 user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email)
517 except UserAlreadyExists as ex:
518 return -errno.EEXIST, '', str(ex)
519
520 if role:
521 user.set_roles([role])
522 mgr.ACCESS_CTRL_DB.save()
523 return 0, json.dumps(user.to_dict()), ''
524
525
526 @CLIWriteCommand('dashboard ac-user-delete',
527 'name=username,type=CephString',
528 'Delete user')
529 def ac_user_delete_cmd(_, username):
530 try:
531 mgr.ACCESS_CTRL_DB.delete_user(username)
532 mgr.ACCESS_CTRL_DB.save()
533 return 0, "User '{}' deleted".format(username), ""
534 except UserDoesNotExist as ex:
535 return -errno.ENOENT, '', str(ex)
536
537
538 @CLIWriteCommand('dashboard ac-user-set-roles',
539 'name=username,type=CephString '
540 'name=roles,type=CephString,n=N',
541 'Set user roles')
542 def ac_user_set_roles_cmd(_, username, roles):
543 rolesname = roles
544 roles = []
545 for rolename in rolesname:
546 try:
547 roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
548 except RoleDoesNotExist as ex:
549 if rolename not in SYSTEM_ROLES:
550 return -errno.ENOENT, '', str(ex)
551 roles.append(SYSTEM_ROLES[rolename])
552 try:
553 user = mgr.ACCESS_CTRL_DB.get_user(username)
554 user.set_roles(roles)
555 mgr.ACCESS_CTRL_DB.save()
556 return 0, json.dumps(user.to_dict()), ''
557 except UserDoesNotExist as ex:
558 return -errno.ENOENT, '', str(ex)
559
560
561 @CLIWriteCommand('dashboard ac-user-add-roles',
562 'name=username,type=CephString '
563 'name=roles,type=CephString,n=N',
564 'Add roles to user')
565 def ac_user_add_roles_cmd(_, username, roles):
566 rolesname = roles
567 roles = []
568 for rolename in rolesname:
569 try:
570 roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
571 except RoleDoesNotExist as ex:
572 if rolename not in SYSTEM_ROLES:
573 return -errno.ENOENT, '', str(ex)
574 roles.append(SYSTEM_ROLES[rolename])
575 try:
576 user = mgr.ACCESS_CTRL_DB.get_user(username)
577 user.add_roles(roles)
578 mgr.ACCESS_CTRL_DB.save()
579 return 0, json.dumps(user.to_dict()), ''
580 except UserDoesNotExist as ex:
581 return -errno.ENOENT, '', str(ex)
582
583
584 @CLIWriteCommand('dashboard ac-user-del-roles',
585 'name=username,type=CephString '
586 'name=roles,type=CephString,n=N',
587 'Delete roles from user')
588 def ac_user_del_roles_cmd(_, username, roles):
589 rolesname = roles
590 roles = []
591 for rolename in rolesname:
592 try:
593 roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename))
594 except RoleDoesNotExist as ex:
595 if rolename not in SYSTEM_ROLES:
596 return -errno.ENOENT, '', str(ex)
597 roles.append(SYSTEM_ROLES[rolename])
598 try:
599 user = mgr.ACCESS_CTRL_DB.get_user(username)
600 user.del_roles(roles)
601 mgr.ACCESS_CTRL_DB.save()
602 return 0, json.dumps(user.to_dict()), ''
603 except UserDoesNotExist as ex:
604 return -errno.ENOENT, '', str(ex)
605 except RoleNotInUser as ex:
606 return -errno.ENOENT, '', str(ex)
607
608
609 @CLIWriteCommand('dashboard ac-user-set-password',
610 'name=username,type=CephString '
611 'name=password,type=CephString',
612 'Set user password')
613 def ac_user_set_password(_, username, password):
614 try:
615 user = mgr.ACCESS_CTRL_DB.get_user(username)
616 user.set_password(password)
617
618 mgr.ACCESS_CTRL_DB.save()
619 return 0, json.dumps(user.to_dict()), ''
620 except UserDoesNotExist as ex:
621 return -errno.ENOENT, '', str(ex)
622
623
624 @CLIWriteCommand('dashboard ac-user-set-info',
625 'name=username,type=CephString '
626 'name=name,type=CephString '
627 'name=email,type=CephString',
628 'Set user info')
629 def ac_user_set_info(_, username, name, email):
630 try:
631 user = mgr.ACCESS_CTRL_DB.get_user(username)
632 if name:
633 user.name = name
634 if email:
635 user.email = email
636 mgr.ACCESS_CTRL_DB.save()
637 return 0, json.dumps(user.to_dict()), ''
638 except UserDoesNotExist as ex:
639 return -errno.ENOENT, '', str(ex)
640
641
642 class LocalAuthenticator(object):
643 def __init__(self):
644 load_access_control_db()
645
646 def get_user(self, username):
647 return mgr.ACCESS_CTRL_DB.get_user(username)
648
649 def authenticate(self, username, password):
650 try:
651 user = mgr.ACCESS_CTRL_DB.get_user(username)
652 if user.password:
653 pass_hash = password_hash(password, user.password)
654 if pass_hash == user.password:
655 return user.permissions_dict()
656 except UserDoesNotExist:
657 logger.debug("User '%s' does not exist", username)
658 return None
659
660 def authorize(self, username, scope, permissions):
661 user = mgr.ACCESS_CTRL_DB.get_user(username)
662 return user.authorize(scope, permissions)