# coding=utf-8 from __future__ import absolute_import __author__ = "Gina Häußge " __license__ = 'GNU Affero General Public License http://www.gnu.org/licenses/agpl.html' __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms of the AGPLv3 License" from flask.ext.login import UserMixin from flask.ext.principal import Identity from werkzeug.local import LocalProxy import hashlib import os import yaml import uuid import logging from octoprint.settings import settings class UserManager(object): valid_roles = ["user", "admin"] def __init__(self): self._logger = logging.getLogger(__name__) self._session_users_by_session = dict() self._session_users_by_username = dict() def login_user(self, user): self._cleanup_sessions() if user is None \ or (isinstance(user, LocalProxy) and not isinstance(user._get_current_object(), User)) \ or (not isinstance(user, LocalProxy) and not isinstance(user, User)): return None if not isinstance(user, SessionUser): user = SessionUser(user) self._session_users_by_session[user.get_session()] = user if not user.get_name() in self._session_users_by_username: self._session_users_by_username[user.get_name()] = [] self._session_users_by_username[user.get_name()].append(user) self._logger.debug("Logged in user: %r" % user) return user def logout_user(self, user): if user is None: return if not isinstance(user, SessionUser): return if user.get_name() in self._session_users_by_username: users_by_username = self._session_users_by_username[user.get_name()] for u in users_by_username: if u.get_session() == user.get_session(): users_by_username.remove(u) break if user.get_session() in self._session_users_by_session: del self._session_users_by_session[user.get_session()] self._logger.debug("Logged out user: %r" % user) def _cleanup_sessions(self): import time for session, user in self._session_users_by_session.items(): if not isinstance(user, SessionUser): continue if user._created + (24 * 60 * 60) < time.time(): self.logout_user(user) @staticmethod def createPasswordHash(password, salt=None): if not salt: salt = settings().get(["accessControl", "salt"]) if salt is None: import string from random import choice chars = string.ascii_lowercase + string.ascii_uppercase + string.digits salt = "".join(choice(chars) for _ in xrange(32)) settings().set(["accessControl", "salt"], salt) settings().save() return hashlib.sha512(password + salt).hexdigest() def checkPassword(self, username, password): user = self.findUser(username) if not user: return False hash = UserManager.createPasswordHash(password) if user.check_password(hash): # new hash matches, correct password return True else: # new hash doesn't match, but maybe the old one does, so check that! oldHash = UserManager.createPasswordHash(password, salt="mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW") if user.check_password(oldHash): # old hash matches, we migrate the stored password hash to the new one and return True since it's the correct password self.changeUserPassword(username, password) return True else: # old hash doesn't match either, wrong password return False def addUser(self, username, password, active, roles): pass def changeUserActivation(self, username, active): pass def changeUserRoles(self, username, roles): pass def addRolesToUser(self, username, roles): pass def removeRolesFromUser(self, username, roles): pass def changeUserPassword(self, username, password): pass def removeUser(self, username): if username in self._session_users_by_username: users = self._session_users_by_username[username] sessions = [user.get_session() for user in users if isinstance(user, SessionUser)] for session in sessions: if session in self._session_users_by_session: del self._session_users_by_session[session] del self._session_users_by_username[username] def findUser(self, username=None, session=None): if session is not None: for session in self._session_users_by_session: user = self._session_users_by_session[session] if username is None or username == user.get_name(): return user break return None def getAllUsers(self): return [] def hasBeenCustomized(self): return False ##~~ FilebasedUserManager, takes available users from users.yaml file class FilebasedUserManager(UserManager): def __init__(self): UserManager.__init__(self) userfile = settings().get(["accessControl", "userfile"]) if userfile is None: userfile = os.path.join(settings().settings_dir, "users.yaml") self._userfile = userfile self._users = {} self._dirty = False self._customized = None self._load() def _load(self): if os.path.exists(self._userfile) and os.path.isfile(self._userfile): self._customized = True with open(self._userfile, "r") as f: data = yaml.safe_load(f) for name in data.keys(): attributes = data[name] apikey = None if "apikey" in attributes: apikey = attributes["apikey"] self._users[name] = User(name, attributes["password"], attributes["active"], attributes["roles"], apikey) else: self._customized = False def _save(self, force=False): if not self._dirty and not force: return data = {} for name in self._users.keys(): user = self._users[name] data[name] = { "password": user._passwordHash, "active": user._active, "roles": user._roles, "apikey": user._apikey } with open(self._userfile, "wb") as f: yaml.safe_dump(data, f, default_flow_style=False, indent=" ", allow_unicode=True) self._dirty = False self._load() def addUser(self, username, password, active=False, roles=None, apikey=None): if not roles: roles = ["user"] if username in self._users.keys(): raise UserAlreadyExists(username) self._users[username] = User(username, UserManager.createPasswordHash(password), active, roles, apikey) self._dirty = True self._save() def changeUserActivation(self, username, active): if not username in self._users.keys(): raise UnknownUser(username) if self._users[username]._active != active: self._users[username]._active = active self._dirty = True self._save() def changeUserRoles(self, username, roles): if not username in self._users.keys(): raise UnknownUser(username) user = self._users[username] removedRoles = set(user._roles) - set(roles) self.removeRolesFromUser(username, removedRoles) addedRoles = set(roles) - set(user._roles) self.addRolesToUser(username, addedRoles) def addRolesToUser(self, username, roles): if not username in self._users.keys(): raise UnknownUser(username) user = self._users[username] for role in roles: if not role in user._roles: user._roles.append(role) self._dirty = True self._save() def removeRolesFromUser(self, username, roles): if not username in self._users.keys(): raise UnknownUser(username) user = self._users[username] for role in roles: if role in user._roles: user._roles.remove(role) self._dirty = True self._save() def changeUserPassword(self, username, password): if not username in self._users.keys(): raise UnknownUser(username) passwordHash = UserManager.createPasswordHash(password) user = self._users[username] if user._passwordHash != passwordHash: user._passwordHash = passwordHash self._dirty = True self._save() def generateApiKey(self, username): if not username in self._users.keys(): raise UnknownUser(username) user = self._users[username] user._apikey = ''.join('%02X' % ord(z) for z in uuid.uuid4().bytes) self._dirty = True self._save() return user._apikey def deleteApikey(self, username): if not username in self._users.keys(): raise UnknownUser(username) user = self._users[username] user._apikey = None self._dirty = True self._save() def removeUser(self, username): UserManager.removeUser(self, username) if not username in self._users.keys(): raise UnknownUser(username) del self._users[username] self._dirty = True self._save() def findUser(self, username=None, apikey=None, session=None): user = UserManager.findUser(self, username=username, session=session) if user is not None: return user if username is not None: if username not in self._users.keys(): return None return self._users[username] elif apikey is not None: for user in self._users.values(): if apikey == user._apikey: return user return None else: return None def getAllUsers(self): return map(lambda x: x.asDict(), self._users.values()) def hasBeenCustomized(self): return self._customized ##~~ Exceptions class UserAlreadyExists(Exception): def __init__(self, username): Exception.__init__(self, "User %s already exists" % username) class UnknownUser(Exception): def __init__(self, username): Exception.__init__(self, "Unknown user: %s" % username) class UnknownRole(Exception): def _init_(self, role): Exception.__init__(self, "Unknown role: %s" % role) ##~~ User object class User(UserMixin): def __init__(self, username, passwordHash, active, roles, apikey=None): self._username = username self._passwordHash = passwordHash self._active = active self._roles = roles self._apikey = apikey def asDict(self): return { "name": self._username, "active": self.is_active(), "admin": self.is_admin(), "user": self.is_user(), "apikey": self._apikey } def check_password(self, passwordHash): return self._passwordHash == passwordHash def get_id(self): return self._username def get_name(self): return self._username def is_active(self): return self._active def is_user(self): return "user" in self._roles def is_admin(self): return "admin" in self._roles def __repr__(self): return "User(id=%s,name=%s,active=%r,user=%r,admin=%r)" % (self.get_id(), self.get_name(), self.is_active(), self.is_user(), self.is_admin()) class SessionUser(User): def __init__(self, user): User.__init__(self, user._username, user._passwordHash, user._active, user._roles, user._apikey) import string import random import time chars = string.ascii_uppercase + string.ascii_lowercase + string.digits self._session = "".join(random.choice(chars) for _ in xrange(10)) self._created = time.time() def get_session(self): return self._session def __repr__(self): return "SessionUser(id=%s,name=%s,active=%r,user=%r,admin=%r,session=%s,created=%s)" % (self.get_id(), self.get_name(), self.is_active(), self.is_user(), self.is_admin(), self._session, self._created) ##~~ DummyUser object to use when accessControl is disabled class DummyUser(User): def __init__(self): User.__init__(self, "dummy", "", True, UserManager.valid_roles) def check_password(self, passwordHash): return True class DummyIdentity(Identity): def __init__(self): Identity.__init__(self, "dummy") def dummy_identity_loader(): return DummyIdentity() ##~~ Apiuser object to use when global api key is used to access the API class ApiUser(User): def __init__(self): User.__init__(self, "_api", "", True, UserManager.valid_roles)