diff --git a/flask_security/__init__.py b/flask_security/__init__.py index 418d0e6f..fb796299 100644 --- a/flask_security/__init__.py +++ b/flask_security/__init__.py @@ -35,7 +35,7 @@ RegisterForm, ResetPasswordForm from .signals import confirm_instructions_sent, password_reset, \ reset_password_instructions_sent, user_confirmed, user_registered -from .utils import login_user, logout_user, url_for_security +from .utils import impersonate_user, login_user, logout_user, url_for_security __version__ = '3.3.1' __all__ = ( @@ -45,6 +45,7 @@ 'ConfirmRegisterForm', 'current_user', 'ForgotPasswordForm', + 'impersonate_user', 'login_required', 'login_user', 'LoginForm', diff --git a/flask_security/utils.py b/flask_security/utils.py index af2297a5..219d32e3 100644 --- a/flask_security/utils.py +++ b/flask_security/utils.py @@ -45,6 +45,26 @@ def _(translate): return translate +def impersonate_user(user, impersonator_idty): + """Impersonate a user. + + Logouts the current user, and login the new user. + + Can login inactive users and doesn't track login time. + """ + logout_user() + + session["_impersonator_id"] = impersonator_idty.id + if not _login_user(user, force=True): + if "_impersonator_id" in session: + session.pop("_impersonator_id") + return False + + identity_changed.send(current_app._get_current_object(), + identity=Identity(user.id)) + return True + + def login_user(user, **kwargs): """Perform the login routine. diff --git a/tests/conftest.py b/tests/conftest.py index e266930c..9a257b92 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,7 +14,7 @@ from json import JSONEncoder as BaseEncoder import pytest -from flask import Flask, render_template +from flask import Flask, current_app, render_template from flask_babel import Babel from flask_mail import Mail from speaklater import is_lazy_string @@ -22,7 +22,8 @@ from flask_security import RoleMixin, Security, \ SQLAlchemySessionUserDatastore, SQLAlchemyUserDatastore, UserMixin, \ - auth_required, login_required, roles_accepted, roles_required + auth_required, impersonate_user, login_required, roles_accepted, \ + roles_required class JSONEncoder(BaseEncoder): @@ -125,6 +126,15 @@ def unauthorized(): def page_1(): return "Page 1" + @login_required + @app.route("/impersonate/", methods=["POST"]) + def impersonate(user): + from flask import g + user = current_app.security.datastore.get_user(user) + + impersonate_user(user, g.identity) + return "Page 1" + def delete_user_from_g(exception): """Delete user from `flask.g` when the request is tearing down. Flask-login==0.6.2 changed the way the user is saved i.e uses `flask.g`. diff --git a/tests/test_common.py b/tests/test_common.py index c1a2d973..e81aecd2 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -190,3 +190,15 @@ def test_user_deleted_during_session_reverts_to_anonymous_user(app, client): response = client.get('/') assert b'Hello matt@lp.com' not in response.data + +def test_impersonate_user(app, client): + authenticate(client) + response = client.get('/') + assert b'Hello matt@lp.com' in response.data + + # Impersonate + client.post('/impersonate/joe@lp.com') + + response = client.get('/') + assert b'Hello matt@lp.com' not in response.data + assert b'Hello joe@lp.com' in response.data