Allow login of the same user from multiple browsers without side effects

So far when logging in from two different browsers, then logging out in one of them the user was logged out across all browsers. This should now be changed in so far as that each individual browser session is tracked and only that session is ended by a logout that belongs to the browser where the logout button was clicked.

Should fix #556
This commit is contained in:
Gina Häußge 2014-10-24 13:05:07 +02:00
parent a185685f0c
commit c23cb378cd
3 changed files with 128 additions and 8 deletions

View file

@ -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()

View file

@ -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)

View file

@ -1,6 +1,9 @@
# 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
@ -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):