Add simple session management using redis

This commit is contained in:
richard-dds 2018-06-27 15:35:30 -04:00
parent e3cd982d58
commit 118a84560a
10 changed files with 147 additions and 40 deletions

View File

@ -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
View File

@ -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": [

View File

@ -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"]
),
}

View File

@ -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"]

View File

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

View File

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

View File

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

View File

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

View File

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