# -*- coding: utf-8 -*- # pylint: disable=too-many-arguments,too-many-return-statements # pylint: disable=too-many-branches, too-many-locals, too-many-statements from __future__ import absolute_import import errno import json import threading import time import six import bcrypt from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand from .. import mgr, logger from ..security import Scope, Permission from ..exceptions import RoleAlreadyExists, RoleDoesNotExist, ScopeNotValid, \ PermissionNotValid, RoleIsAssociatedWithUser, \ UserAlreadyExists, UserDoesNotExist, ScopeNotInRole, \ RoleNotInUser # password hashing algorithm def password_hash(password, salt_password=None): if not password: return None if six.PY2: password = unicode(password, 'utf-8') if isinstance(password, str) else password if not salt_password: salt_password = bcrypt.gensalt() else: salt_password = salt_password.encode('utf8') return bcrypt.hashpw(password.encode('utf8'), salt_password).decode('utf8') _P = Permission # short alias class Role(object): def __init__(self, name, description=None, scope_permissions=None): self.name = name self.description = description if scope_permissions is None: self.scopes_permissions = {} else: self.scopes_permissions = scope_permissions def __hash__(self): return hash(self.name) def __eq__(self, other): return self.name == other.name def set_scope_permissions(self, scope, permissions): if not Scope.valid_scope(scope): raise ScopeNotValid(scope) for perm in permissions: if not Permission.valid_permission(perm): raise PermissionNotValid(perm) permissions.sort() self.scopes_permissions[scope] = permissions def del_scope_permissions(self, scope): if scope not in self.scopes_permissions: raise ScopeNotInRole(scope, self.name) del self.scopes_permissions[scope] def reset_scope_permissions(self): self.scopes_permissions = {} def authorize(self, scope, permissions): if scope in self.scopes_permissions: role_perms = self.scopes_permissions[scope] for perm in permissions: if perm not in role_perms: return False return True return False def to_dict(self): return { 'name': self.name, 'description': self.description, 'scopes_permissions': self.scopes_permissions } @classmethod def from_dict(cls, r_dict): return Role(r_dict['name'], r_dict['description'], r_dict['scopes_permissions']) # static pre-defined system roles # this roles cannot be deleted nor updated # admin role provides all permissions for all scopes ADMIN_ROLE = Role('administrator', 'Administrator', { scope_name: Permission.all_permissions() for scope_name in Scope.all_scopes() }) # read-only role provides read-only permission for all scopes READ_ONLY_ROLE = Role('read-only', 'Read-Only', { scope_name: [_P.READ] for scope_name in Scope.all_scopes() if scope_name != Scope.DASHBOARD_SETTINGS }) # block manager role provides all permission for block related scopes BLOCK_MGR_ROLE = Role('block-manager', 'Block Manager', { Scope.RBD_IMAGE: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.POOL: [_P.READ], Scope.ISCSI: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.RBD_MIRRORING: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.GRAFANA: [_P.READ], }) # RadosGW manager role provides all permissions for block related scopes RGW_MGR_ROLE = Role('rgw-manager', 'RGW Manager', { Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.GRAFANA: [_P.READ], }) # Cluster manager role provides all permission for OSDs, Monitors, and # Config options CLUSTER_MGR_ROLE = Role('cluster-manager', 'Cluster Manager', { Scope.HOSTS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.OSD: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.MONITOR: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.MANAGER: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.CONFIG_OPT: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.LOG: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.GRAFANA: [_P.READ], }) # Pool manager role provides all permissions for pool related scopes POOL_MGR_ROLE = Role('pool-manager', 'Pool Manager', { Scope.POOL: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.GRAFANA: [_P.READ], }) # CephFS manager role provides all permissions for CephFS related scopes CEPHFS_MGR_ROLE = Role('cephfs-manager', 'CephFS Manager', { Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.GRAFANA: [_P.READ], }) GANESHA_MGR_ROLE = Role('ganesha-manager', 'NFS Ganesha Manager', { Scope.NFS_GANESHA: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.CEPHFS: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.RGW: [_P.READ, _P.CREATE, _P.UPDATE, _P.DELETE], Scope.GRAFANA: [_P.READ], }) SYSTEM_ROLES = { ADMIN_ROLE.name: ADMIN_ROLE, READ_ONLY_ROLE.name: READ_ONLY_ROLE, BLOCK_MGR_ROLE.name: BLOCK_MGR_ROLE, RGW_MGR_ROLE.name: RGW_MGR_ROLE, CLUSTER_MGR_ROLE.name: CLUSTER_MGR_ROLE, POOL_MGR_ROLE.name: POOL_MGR_ROLE, CEPHFS_MGR_ROLE.name: CEPHFS_MGR_ROLE, GANESHA_MGR_ROLE.name: GANESHA_MGR_ROLE, } class User(object): def __init__(self, username, password, name=None, email=None, roles=None, lastUpdate=None): self.username = username self.password = password self.name = name self.email = email if roles is None: self.roles = set() else: self.roles = roles if lastUpdate is None: self.refreshLastUpdate() else: self.lastUpdate = lastUpdate def refreshLastUpdate(self): self.lastUpdate = int(time.time()) def set_password(self, password): self.password = password_hash(password) self.refreshLastUpdate() def set_roles(self, roles): self.roles = set(roles) self.refreshLastUpdate() def add_roles(self, roles): self.roles = self.roles.union(set(roles)) self.refreshLastUpdate() def del_roles(self, roles): for role in roles: if role not in self.roles: raise RoleNotInUser(role.name, self.username) self.roles.difference_update(set(roles)) self.refreshLastUpdate() def authorize(self, scope, permissions): for role in self.roles: if role.authorize(scope, permissions): return True return False def permissions_dict(self): perms = {} for role in self.roles: for scope, perms_list in role.scopes_permissions.items(): if scope in perms: perms_tmp = set(perms[scope]).union(set(perms_list)) perms[scope] = list(perms_tmp) else: perms[scope] = perms_list return perms def to_dict(self): return { 'username': self.username, 'password': self.password, 'roles': sorted([r.name for r in self.roles]), 'name': self.name, 'email': self.email, 'lastUpdate': self.lastUpdate } @classmethod def from_dict(cls, u_dict, roles): return User(u_dict['username'], u_dict['password'], u_dict['name'], u_dict['email'], {roles[r] for r in u_dict['roles']}, u_dict['lastUpdate']) class AccessControlDB(object): VERSION = 1 ACDB_CONFIG_KEY = "accessdb_v" def __init__(self, version, users, roles): self.users = users self.version = version self.roles = roles self.lock = threading.RLock() def create_role(self, name, description=None): with self.lock: if name in SYSTEM_ROLES or name in self.roles: raise RoleAlreadyExists(name) role = Role(name, description) self.roles[name] = role return role def get_role(self, name): with self.lock: if name not in self.roles: raise RoleDoesNotExist(name) return self.roles[name] def delete_role(self, name): with self.lock: if name not in self.roles: raise RoleDoesNotExist(name) role = self.roles[name] # check if role is not associated with a user for username, user in self.users.items(): if role in user.roles: raise RoleIsAssociatedWithUser(name, username) del self.roles[name] def create_user(self, username, password, name, email): logger.debug("AC: creating user: username=%s", username) with self.lock: if username in self.users: raise UserAlreadyExists(username) user = User(username, password_hash(password), name, email) self.users[username] = user return user def get_user(self, username): with self.lock: if username not in self.users: raise UserDoesNotExist(username) return self.users[username] def delete_user(self, username): with self.lock: if username not in self.users: raise UserDoesNotExist(username) del self.users[username] def update_users_with_roles(self, role): with self.lock: if not role: return for _, user in self.users.items(): if role in user.roles: user.refreshLastUpdate() def save(self): with self.lock: db = { 'users': {un: u.to_dict() for un, u in self.users.items()}, 'roles': {rn: r.to_dict() for rn, r in self.roles.items()}, 'version': self.version } mgr.set_store(self.accessdb_config_key(), json.dumps(db)) @classmethod def accessdb_config_key(cls, version=None): if version is None: version = cls.VERSION return "{}{}".format(cls.ACDB_CONFIG_KEY, version) def check_and_update_db(self): logger.debug("AC: Checking for previews DB versions") if self.VERSION == 1: # current version # check if there is username/password from previous version username = mgr.get_module_option('username', None) password = mgr.get_module_option('password', None) if username and password: logger.debug("AC: Found single user credentials: user=%s", username) # found user credentials user = self.create_user(username, "", None, None) # password is already hashed, so setting manually user.password = password user.add_roles([ADMIN_ROLE]) self.save() else: raise NotImplementedError() @classmethod def load(cls): logger.info("AC: Loading user roles DB version=%s", cls.VERSION) json_db = mgr.get_store(cls.accessdb_config_key()) if json_db is None: logger.debug("AC: No DB v%s found, creating new...", cls.VERSION) db = cls(cls.VERSION, {}, {}) # check if we can update from a previous version database db.check_and_update_db() return db db = json.loads(json_db) roles = {rn: Role.from_dict(r) for rn, r in db.get('roles', {}).items()} users = {un: User.from_dict(u, dict(roles, **SYSTEM_ROLES)) for un, u in db.get('users', {}).items()} return cls(db['version'], users, roles) def load_access_control_db(): mgr.ACCESS_CTRL_DB = AccessControlDB.load() # CLI dashboard access control scope commands @CLIWriteCommand('dashboard set-login-credentials', 'name=username,type=CephString', 'Set the login credentials. Password read from -i ') @CLICheckNonemptyFileInput def set_login_credentials_cmd(_, username, inbuf): password = inbuf try: user = mgr.ACCESS_CTRL_DB.get_user(username) user.set_password(password) except UserDoesNotExist: user = mgr.ACCESS_CTRL_DB.create_user(username, password, None, None) user.set_roles([ADMIN_ROLE]) mgr.ACCESS_CTRL_DB.save() return 0, '''\ ****************************************************************** *** WARNING: this command is deprecated. *** *** Please use the ac-user-* related commands to manage users. *** ****************************************************************** Username and password updated''', '' @CLIReadCommand('dashboard ac-role-show', 'name=rolename,type=CephString,req=false', 'Show role info') def ac_role_show_cmd(_, rolename=None): if not rolename: roles = dict(mgr.ACCESS_CTRL_DB.roles) roles.update(SYSTEM_ROLES) roles_list = [name for name, _ in roles.items()] return 0, json.dumps(roles_list), '' try: role = mgr.ACCESS_CTRL_DB.get_role(rolename) except RoleDoesNotExist as ex: if rolename not in SYSTEM_ROLES: return -errno.ENOENT, '', str(ex) role = SYSTEM_ROLES[rolename] return 0, json.dumps(role.to_dict()), '' @CLIWriteCommand('dashboard ac-role-create', 'name=rolename,type=CephString ' 'name=description,type=CephString,req=false', 'Create a new access control role') def ac_role_create_cmd(_, rolename, description=None): try: role = mgr.ACCESS_CTRL_DB.create_role(rolename, description) mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(role.to_dict()), '' except RoleAlreadyExists as ex: return -errno.EEXIST, '', str(ex) @CLIWriteCommand('dashboard ac-role-delete', 'name=rolename,type=CephString', 'Delete an access control role') def ac_role_delete_cmd(_, rolename): try: mgr.ACCESS_CTRL_DB.delete_role(rolename) mgr.ACCESS_CTRL_DB.save() return 0, "Role '{}' deleted".format(rolename), "" except RoleDoesNotExist as ex: if rolename in SYSTEM_ROLES: return -errno.EPERM, '', "Cannot delete system role '{}'" \ .format(rolename) return -errno.ENOENT, '', str(ex) except RoleIsAssociatedWithUser as ex: return -errno.EPERM, '', str(ex) @CLIWriteCommand('dashboard ac-role-add-scope-perms', 'name=rolename,type=CephString ' 'name=scopename,type=CephString ' 'name=permissions,type=CephString,n=N', 'Add the scope permissions for a role') def ac_role_add_scope_perms_cmd(_, rolename, scopename, permissions): try: role = mgr.ACCESS_CTRL_DB.get_role(rolename) perms_array = [perm.strip() for perm in permissions] role.set_scope_permissions(scopename, perms_array) mgr.ACCESS_CTRL_DB.update_users_with_roles(role) mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(role.to_dict()), '' except RoleDoesNotExist as ex: if rolename in SYSTEM_ROLES: return -errno.EPERM, '', "Cannot update system role '{}'" \ .format(rolename) return -errno.ENOENT, '', str(ex) except ScopeNotValid as ex: return -errno.EINVAL, '', str(ex) + "\n Possible values: {}" \ .format(Scope.all_scopes()) except PermissionNotValid as ex: return -errno.EINVAL, '', str(ex) + \ "\n Possible values: {}" \ .format(Permission.all_permissions()) @CLIWriteCommand('dashboard ac-role-del-scope-perms', 'name=rolename,type=CephString ' 'name=scopename,type=CephString', 'Delete the scope permissions for a role') def ac_role_del_scope_perms_cmd(_, rolename, scopename): try: role = mgr.ACCESS_CTRL_DB.get_role(rolename) role.del_scope_permissions(scopename) mgr.ACCESS_CTRL_DB.update_users_with_roles(role) mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(role.to_dict()), '' except RoleDoesNotExist as ex: if rolename in SYSTEM_ROLES: return -errno.EPERM, '', "Cannot update system role '{}'" \ .format(rolename) return -errno.ENOENT, '', str(ex) except ScopeNotInRole as ex: return -errno.ENOENT, '', str(ex) @CLIReadCommand('dashboard ac-user-show', 'name=username,type=CephString,req=false', 'Show user info') def ac_user_show_cmd(_, username=None): if not username: users = mgr.ACCESS_CTRL_DB.users users_list = [name for name, _ in users.items()] return 0, json.dumps(users_list), '' try: user = mgr.ACCESS_CTRL_DB.get_user(username) return 0, json.dumps(user.to_dict()), '' except UserDoesNotExist as ex: return -errno.ENOENT, '', str(ex) @CLIWriteCommand('dashboard ac-user-create', 'name=username,type=CephString ' 'name=rolename,type=CephString,req=false ' 'name=name,type=CephString,req=false ' 'name=email,type=CephString,req=false', 'Create a user. Password read from -i ') @CLICheckNonemptyFileInput def ac_user_create_cmd(_, username, inbuf, rolename=None, name=None, email=None): password = inbuf try: role = mgr.ACCESS_CTRL_DB.get_role(rolename) if rolename else None except RoleDoesNotExist as ex: if rolename not in SYSTEM_ROLES: return -errno.ENOENT, '', str(ex) role = SYSTEM_ROLES[rolename] try: user = mgr.ACCESS_CTRL_DB.create_user(username, password, name, email) except UserAlreadyExists as ex: return -errno.EEXIST, '', str(ex) if role: user.set_roles([role]) mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(user.to_dict()), '' @CLIWriteCommand('dashboard ac-user-delete', 'name=username,type=CephString', 'Delete user') def ac_user_delete_cmd(_, username): try: mgr.ACCESS_CTRL_DB.delete_user(username) mgr.ACCESS_CTRL_DB.save() return 0, "User '{}' deleted".format(username), "" except UserDoesNotExist as ex: return -errno.ENOENT, '', str(ex) @CLIWriteCommand('dashboard ac-user-set-roles', 'name=username,type=CephString ' 'name=roles,type=CephString,n=N', 'Set user roles') def ac_user_set_roles_cmd(_, username, roles): rolesname = roles roles = [] for rolename in rolesname: try: roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename)) except RoleDoesNotExist as ex: if rolename not in SYSTEM_ROLES: return -errno.ENOENT, '', str(ex) roles.append(SYSTEM_ROLES[rolename]) try: user = mgr.ACCESS_CTRL_DB.get_user(username) user.set_roles(roles) mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(user.to_dict()), '' except UserDoesNotExist as ex: return -errno.ENOENT, '', str(ex) @CLIWriteCommand('dashboard ac-user-add-roles', 'name=username,type=CephString ' 'name=roles,type=CephString,n=N', 'Add roles to user') def ac_user_add_roles_cmd(_, username, roles): rolesname = roles roles = [] for rolename in rolesname: try: roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename)) except RoleDoesNotExist as ex: if rolename not in SYSTEM_ROLES: return -errno.ENOENT, '', str(ex) roles.append(SYSTEM_ROLES[rolename]) try: user = mgr.ACCESS_CTRL_DB.get_user(username) user.add_roles(roles) mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(user.to_dict()), '' except UserDoesNotExist as ex: return -errno.ENOENT, '', str(ex) @CLIWriteCommand('dashboard ac-user-del-roles', 'name=username,type=CephString ' 'name=roles,type=CephString,n=N', 'Delete roles from user') def ac_user_del_roles_cmd(_, username, roles): rolesname = roles roles = [] for rolename in rolesname: try: roles.append(mgr.ACCESS_CTRL_DB.get_role(rolename)) except RoleDoesNotExist as ex: if rolename not in SYSTEM_ROLES: return -errno.ENOENT, '', str(ex) roles.append(SYSTEM_ROLES[rolename]) try: user = mgr.ACCESS_CTRL_DB.get_user(username) user.del_roles(roles) mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(user.to_dict()), '' except UserDoesNotExist as ex: return -errno.ENOENT, '', str(ex) except RoleNotInUser as ex: return -errno.ENOENT, '', str(ex) @CLIWriteCommand('dashboard ac-user-set-password', 'name=username,type=CephString', 'Set user password from -i ') @CLICheckNonemptyFileInput def ac_user_set_password(_, username, inbuf): password = inbuf try: user = mgr.ACCESS_CTRL_DB.get_user(username) user.set_password(password) mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(user.to_dict()), '' except UserDoesNotExist as ex: return -errno.ENOENT, '', str(ex) @CLIWriteCommand('dashboard ac-user-set-info', 'name=username,type=CephString ' 'name=name,type=CephString ' 'name=email,type=CephString', 'Set user info') def ac_user_set_info(_, username, name, email): try: user = mgr.ACCESS_CTRL_DB.get_user(username) if name: user.name = name if email: user.email = email mgr.ACCESS_CTRL_DB.save() return 0, json.dumps(user.to_dict()), '' except UserDoesNotExist as ex: return -errno.ENOENT, '', str(ex) class LocalAuthenticator(object): def __init__(self): load_access_control_db() def get_user(self, username): return mgr.ACCESS_CTRL_DB.get_user(username) def authenticate(self, username, password): try: user = mgr.ACCESS_CTRL_DB.get_user(username) if user.password: pass_hash = password_hash(password, user.password) if pass_hash == user.password: return user.permissions_dict() except UserDoesNotExist: logger.debug("User '%s' does not exist", username) return None def authorize(self, username, scope, permissions): user = mgr.ACCESS_CTRL_DB.get_user(username) return user.authorize(scope, permissions)