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:
parent
a185685f0c
commit
c23cb378cd
3 changed files with 128 additions and 8 deletions
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue