diff --git a/src/octoprint/server/__init__.py b/src/octoprint/server/__init__.py index 3063de22..efc56ecf 100644 --- a/src/octoprint/server/__init__.py +++ b/src/octoprint/server/__init__.py @@ -7,7 +7,7 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms import uuid from sockjs.tornado import SockJSRouter -from flask import Flask, render_template, send_from_directory, g, request, make_response +from flask import Flask, render_template, send_from_directory, g, request, make_response, session from flask.ext.login import LoginManager from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed from flask.ext.babel import Babel @@ -160,8 +160,16 @@ def on_identity_loaded(sender, identity): def load_user(id): + if session and "usersession.id" in session: + sessionid = session["usersession.id"] + else: + sessionid = None + if userManager is not None: - return userManager.findUser(id) + if sessionid: + return userManager.findUser(username=id, session=sessionid) + else: + return userManager.findUser(username=id) return users.DummyUser() diff --git a/src/octoprint/server/api/__init__.py b/src/octoprint/server/api/__init__.py index 1fac789c..603b4cd2 100644 --- a/src/octoprint/server/api/__init__.py +++ b/src/octoprint/server/api/__init__.py @@ -244,15 +244,26 @@ def login(): else: remember = False + if "usersession.id" in session: + _logout(current_user) + user = octoprint.server.userManager.findUser(username) if user is not None: if user.check_password(octoprint.users.UserManager.createPasswordHash(password)): + if octoprint.server.userManager is not None: + user = octoprint.server.userManager.login_user(user) + session["usersession.id"] = user.get_session() login_user(user, remember=remember) identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id())) return jsonify(user.asDict()) return make_response(("User unknown or password incorrect", 401, [])) + elif "passive" in request.values.keys(): - user = current_user + if octoprint.server.userManager is not None: + user = octoprint.server.userManager.login_user(current_user) + else: + user = current_user + if user is not None and not user.is_anonymous(): identity_changed.send(current_app._get_current_object(), identity=Identity(user.get_id())) return jsonify(user.asDict()) @@ -288,7 +299,12 @@ def logout(): del session[key] identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity()) + _logout(current_user) logout_user() return NO_CONTENT +def _logout(user): + if "usersession.id" in session: + del session["usersession.id"] + octoprint.server.userManager.logout_user(user) diff --git a/src/octoprint/users.py b/src/octoprint/users.py index 58f2f732..8ee7bb6b 100644 --- a/src/octoprint/users.py +++ b/src/octoprint/users.py @@ -1,6 +1,9 @@ # 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 @@ -9,11 +12,63 @@ 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: + 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): return hashlib.sha512(password + "mvBUTvwzBzD3yPwvnJ4E4tXNf3CGJvvW").hexdigest() @@ -37,9 +92,22 @@ class UserManager(object): pass def removeUser(self, username): - pass + 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 - def findUser(self, username=None): return None def getAllUsers(self): @@ -179,6 +247,8 @@ class FilebasedUserManager(UserManager): self._save() def removeUser(self, username): + UserManager.removeUser(self, username) + if not username in self._users.keys(): raise UnknownUser(username) @@ -186,17 +256,23 @@ class FilebasedUserManager(UserManager): self._dirty = True self._save() - def findUser(self, username=None, apikey=None): + 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 @@ -257,6 +333,26 @@ class User(UserMixin): 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): @@ -274,7 +370,7 @@ def dummy_identity_loader(): return DummyIdentity() -##~~ Apiuser object to use when api key is used to access the API +##~~ Apiuser object to use when global api key is used to access the API class ApiUser(User):