Add simple session management using redis
This commit is contained in:
parent
e3cd982d58
commit
118a84560a
1
Pipfile
1
Pipfile
@ -9,6 +9,7 @@ webassets = "==0.12.1"
|
||||
Unipath = "==1.1"
|
||||
wtforms-tornado = "*"
|
||||
pendulum = "*"
|
||||
redis = "*"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "==3.6.0"
|
||||
|
16
Pipfile.lock
generated
16
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "5f91978d61968a720e4edc404ea36123ff96d857c1d7ff865f5aaef9b133db37"
|
||||
"sha256": "e424765acd32453d13c35cade0b4d0cb57cac5742ac020f32352411f15df71c5"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -46,6 +46,14 @@
|
||||
],
|
||||
"version": "==2018.5"
|
||||
},
|
||||
"redis": {
|
||||
"hashes": [
|
||||
"sha256:8a1900a9f2a0a44ecf6e8b5eb3e967a9909dfed219ad66df094f27f7d6f330fb",
|
||||
"sha256:a22ca993cea2962dbb588f9f30d0015ac4afcc45bee27d3978c0dbe9e97c6c0f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.10.6"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
@ -293,10 +301,10 @@
|
||||
},
|
||||
"py": {
|
||||
"hashes": [
|
||||
"sha256:29c9fab495d7528e80ba1e343b958684f4ace687327e6f789a94bf3d1915f881",
|
||||
"sha256:983f77f3331356039fdd792e9220b7b8ee1aa6bd2b25f567a963ff1de5a64f6a"
|
||||
"sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7",
|
||||
"sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e"
|
||||
],
|
||||
"version": "==1.5.3"
|
||||
"version": "==1.5.4"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
|
26
atst/app.py
26
atst/app.py
@ -2,6 +2,7 @@ import os
|
||||
from configparser import ConfigParser
|
||||
import tornado.web
|
||||
from tornado.web import url
|
||||
from redis import StrictRedis
|
||||
|
||||
from atst.handlers.main import MainHandler
|
||||
from atst.handlers.home import Home
|
||||
@ -12,6 +13,7 @@ from atst.handlers.request_new import RequestNew
|
||||
from atst.handlers.dev import Dev
|
||||
from atst.home import home
|
||||
from atst.api_client import ApiClient
|
||||
from atst.sessions import RedisSessions
|
||||
|
||||
ENV = os.getenv("TORNADO_ENV", "dev")
|
||||
|
||||
@ -20,7 +22,12 @@ def make_app(config, deps, **kwargs):
|
||||
|
||||
routes = [
|
||||
url(r"/", Home, {"page": "login"}, name="main"),
|
||||
url(r"/login", Login, {"authnid_client": deps["authnid_client"]}, name="login"),
|
||||
url(
|
||||
r"/login",
|
||||
Login,
|
||||
{"sessions": deps["sessions"], "authnid_client": deps["authnid_client"]},
|
||||
name="login",
|
||||
),
|
||||
url(r"/home", MainHandler, {"page": "home"}, name="home"),
|
||||
url(
|
||||
r"/workspaces/blank",
|
||||
@ -64,7 +71,14 @@ def make_app(config, deps, **kwargs):
|
||||
]
|
||||
|
||||
if not ENV == "production":
|
||||
routes += [url(r"/login-dev", Dev, {"action": "login"}, name="dev-login")]
|
||||
routes += [
|
||||
url(
|
||||
r"/login-dev",
|
||||
Dev,
|
||||
{"action": "login", "sessions": deps["sessions"]},
|
||||
name="dev-login",
|
||||
)
|
||||
]
|
||||
|
||||
app = tornado.web.Application(
|
||||
routes,
|
||||
@ -76,12 +90,17 @@ def make_app(config, deps, **kwargs):
|
||||
**kwargs,
|
||||
)
|
||||
app.config = config
|
||||
app.sessions = deps["sessions"]
|
||||
return app
|
||||
|
||||
|
||||
def make_deps(config):
|
||||
# we do not want to do SSL verify services in test and development
|
||||
validate_cert = ENV == "production"
|
||||
redis_client = StrictRedis.from_url(
|
||||
config["default"]["REDIS_URI"], decode_responses=True
|
||||
)
|
||||
|
||||
return {
|
||||
"authz_client": ApiClient(
|
||||
config["default"]["AUTHZ_BASE_URL"],
|
||||
@ -98,6 +117,9 @@ def make_deps(config):
|
||||
api_version="v1",
|
||||
validate_cert=validate_cert,
|
||||
),
|
||||
"sessions": RedisSessions(
|
||||
redis_client, config["default"]["SESSION_TTL_SECONDS"]
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
from webassets import Environment, Bundle
|
||||
import tornado.web
|
||||
from atst.home import home
|
||||
from atst.sessions import SessionNotFoundError
|
||||
|
||||
assets = Environment(directory=home.child("scss"), url="/static")
|
||||
css = Bundle(
|
||||
@ -21,17 +22,19 @@ class BaseHandler(tornado.web.RequestHandler):
|
||||
ns.update(helpers)
|
||||
return ns
|
||||
|
||||
def login(self, user):
|
||||
session_id = self.sessions.start_session(user)
|
||||
self.set_secure_cookie("atat", session_id)
|
||||
self.redirect("/home")
|
||||
|
||||
def get_current_user(self):
|
||||
if self.get_secure_cookie("atst"):
|
||||
return {
|
||||
"id": "9cb348f0-8102-4962-88c4-dac8180c904c",
|
||||
"email": "fake.user@mail.com",
|
||||
"first_name": "Fake",
|
||||
"last_name": "User",
|
||||
}
|
||||
cookie = self.get_secure_cookie("atat")
|
||||
if cookie:
|
||||
try:
|
||||
session = self.application.sessions.get_session(cookie)
|
||||
except SessionNotFoundError:
|
||||
self.redirect("/login")
|
||||
else:
|
||||
return None
|
||||
|
||||
# this is a temporary implementation until we have real sessions
|
||||
def _start_session(self):
|
||||
self.set_secure_cookie("atst", "valid-user-session")
|
||||
return session["user"]
|
||||
|
@ -2,13 +2,10 @@ from atst.handler import BaseHandler
|
||||
|
||||
|
||||
class Dev(BaseHandler):
|
||||
def initialize(self, action):
|
||||
def initialize(self, action, sessions):
|
||||
self.action = action
|
||||
self.sessions = sessions
|
||||
|
||||
def get(self):
|
||||
if self.action == "login":
|
||||
self._login()
|
||||
|
||||
def _login(self):
|
||||
self._start_session()
|
||||
self.redirect("/home")
|
||||
user = {"id": "164497f6-c1ea-4f42-a5ef-101da278c012"}
|
||||
self.login(user)
|
||||
|
@ -3,34 +3,35 @@ from atst.handler import BaseHandler
|
||||
|
||||
|
||||
class Login(BaseHandler):
|
||||
def initialize(self, authnid_client):
|
||||
def initialize(self, authnid_client, sessions):
|
||||
self.authnid_client = authnid_client
|
||||
self.sessions = sessions
|
||||
|
||||
@tornado.gen.coroutine
|
||||
def get(self):
|
||||
token = self.get_query_argument("bearer-token")
|
||||
if token:
|
||||
valid = yield self._validate_login_token(token)
|
||||
if valid:
|
||||
self._start_session()
|
||||
self.redirect("/home")
|
||||
return
|
||||
user = yield self._fetch_user_info(token)
|
||||
if user:
|
||||
self.login(user)
|
||||
else:
|
||||
self.write_error(401)
|
||||
|
||||
url = self.get_login_url()
|
||||
self.redirect(url)
|
||||
return
|
||||
|
||||
@tornado.gen.coroutine
|
||||
def _validate_login_token(self, token):
|
||||
def _fetch_user_info(self, token):
|
||||
try:
|
||||
response = yield self.authnid_client.post(
|
||||
"/validate", json={"token": token}
|
||||
)
|
||||
return response.code == 200
|
||||
if response.code == 200:
|
||||
return response.json["user"]
|
||||
|
||||
except tornado.httpclient.HTTPError as error:
|
||||
if error.response.code == 401:
|
||||
return False
|
||||
return None
|
||||
|
||||
else:
|
||||
raise error
|
||||
|
71
atst/sessions.py
Normal file
71
atst/sessions.py
Normal file
@ -0,0 +1,71 @@
|
||||
from uuid import uuid4
|
||||
import json
|
||||
from redis import exceptions
|
||||
|
||||
|
||||
class SessionStorageError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SessionNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class Sessions(object):
|
||||
def start_session(self, user):
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_session(self, session_id):
|
||||
raise NotImplementedError()
|
||||
|
||||
def generate_session_id(self):
|
||||
return str(uuid4())
|
||||
|
||||
def build_session_dict(self, user=None):
|
||||
return {"user": user or {}}
|
||||
|
||||
|
||||
class DictSessions(Sessions):
|
||||
def __init__(self):
|
||||
self.sessions = {}
|
||||
|
||||
def start_session(self, user):
|
||||
session_id = self.generate_session_id()
|
||||
self.sessions[session_id] = self.build_session_dict(user=user)
|
||||
return session_id
|
||||
|
||||
def get_session(self, session_id):
|
||||
try:
|
||||
session = self.sessions[session_id]
|
||||
except KeyError:
|
||||
raise SessionNotFoundError
|
||||
|
||||
return session
|
||||
|
||||
|
||||
class RedisSessions(Sessions):
|
||||
def __init__(self, redis, ttl_seconds):
|
||||
self.redis = redis
|
||||
self.ttl_seconds = ttl_seconds
|
||||
|
||||
def start_session(self, user):
|
||||
session_id = self.generate_session_id()
|
||||
session_dict = self.build_session_dict(user=user)
|
||||
session_serialized = json.dumps(session_dict)
|
||||
try:
|
||||
self.redis.setex(session_id, self.ttl_seconds, session_serialized)
|
||||
except exceptions.ConnectionError:
|
||||
raise SessionStorageError
|
||||
return session_id
|
||||
|
||||
def get_session(self, session_id):
|
||||
try:
|
||||
session_serialized = self.redis.get(session_id)
|
||||
except exceptions.ConnectionError:
|
||||
raise
|
||||
|
||||
if session_serialized:
|
||||
self.redis.expire(session_id, self.ttl_seconds)
|
||||
return json.loads(session_serialized)
|
||||
else:
|
||||
raise SessionNotFoundError
|
@ -8,3 +8,5 @@ COOKIE_SECRET = some-secret-please-replace
|
||||
SECRET = change_me_into_something_secret
|
||||
CAC_URL = https://localhost:8001
|
||||
REQUESTS_QUEUE_BASE_URL = http://localhost:8003
|
||||
REDIS_URI = redis://localhost:6379
|
||||
SESSION_TTL_SECONDS = 3600
|
||||
|
@ -2,6 +2,7 @@ import pytest
|
||||
|
||||
from atst.app import make_app, make_deps, make_config
|
||||
from tests.mocks import MockApiClient, MockRequestsClient
|
||||
from atst.sessions import DictSessions
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -10,6 +11,7 @@ def app():
|
||||
"authz_client": MockApiClient("authz"),
|
||||
"requests_client": MockRequestsClient("requests"),
|
||||
"authnid_client": MockApiClient("authnid"),
|
||||
"sessions": DictSessions(),
|
||||
}
|
||||
|
||||
config = make_config()
|
||||
|
@ -3,6 +3,8 @@ import pytest
|
||||
import tornado.web
|
||||
import tornado.gen
|
||||
|
||||
MOCK_USER = {"user": {"id": "438567dd-25fa-4d83-a8cc-8aa8366cb24a"}}
|
||||
|
||||
|
||||
@pytest.mark.gen_test
|
||||
def test_redirects_when_not_logged_in(http_client, base_url):
|
||||
@ -18,18 +20,16 @@ def test_redirects_when_not_logged_in(http_client, base_url):
|
||||
@pytest.mark.gen_test
|
||||
def test_login_with_valid_bearer_token(app, monkeypatch, http_client, base_url):
|
||||
@tornado.gen.coroutine
|
||||
def _validate_login_token(c, t):
|
||||
return True
|
||||
def _fetch_user_info(c, t):
|
||||
return MOCK_USER
|
||||
|
||||
monkeypatch.setattr(
|
||||
"atst.handlers.login.Login._validate_login_token", _validate_login_token
|
||||
)
|
||||
monkeypatch.setattr("atst.handlers.login.Login._fetch_user_info", _fetch_user_info)
|
||||
response = yield http_client.fetch(
|
||||
base_url + "/login?bearer-token=abc-123",
|
||||
follow_redirects=False,
|
||||
raise_error=False,
|
||||
)
|
||||
assert response.headers["Set-Cookie"].startswith("atst")
|
||||
assert response.headers["Set-Cookie"].startswith("atat")
|
||||
assert response.headers["Location"] == "/home"
|
||||
assert response.code == 302
|
||||
|
||||
@ -39,7 +39,7 @@ def test_login_via_dev_endpoint(app, http_client, base_url):
|
||||
response = yield http_client.fetch(
|
||||
base_url + "/login-dev", raise_error=False, follow_redirects=False
|
||||
)
|
||||
assert response.headers["Set-Cookie"].startswith("atst")
|
||||
assert response.headers["Set-Cookie"].startswith("atat")
|
||||
assert response.code == 302
|
||||
assert response.headers["Location"] == "/home"
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user