Refactored some things in octoprint.util.comm and octoprint.settings, added migration function to get users of the devel version up to date with their gcode scripts. Migration function will be removed again one week from now.
414 lines
11 KiB
Python
414 lines
11 KiB
Python
# coding=utf-8
|
|
from __future__ import absolute_import
|
|
|
|
__author__ = "Gina Häußge <osd@foosel.net>"
|
|
__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().getBaseFolder("base"), "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)
|