diff --git a/Pipfile b/Pipfile index 1cedb3f5..8d79d3aa 100644 --- a/Pipfile +++ b/Pipfile @@ -7,7 +7,6 @@ name = "pypi" tornado = "==5.0.2" webassets = "==0.12.1" Unipath = "==1.1" -requests = "*" [dev-packages] pytest = "==3.6.0" diff --git a/Pipfile.lock b/Pipfile.lock index d4ccb88b..4edfecc2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "68a0d5093979093899f0f86faa82eb55f90f9a67a16b11a5701ea85096e72ee8" + "sha256": "391e254ddb902877afca9c07aa2306710ce6d1e207b029c1a8b5dc0115ee99a5" }, "pipfile-spec": 6, "requires": { @@ -16,35 +16,6 @@ ] }, "default": { - "certifi": { - "hashes": [ - "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", - "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" - ], - "version": "==2018.4.16" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "idna": { - "hashes": [ - "sha256:2c6a5de3089009e3da7c5dde64a141dbc8551d5b7f6cf4ed7c2568d0cc520a8f", - "sha256:8c7309c718f94b3a625cb648ace320157ad16ff131ae0af362c9f21b80ef6ec4" - ], - "version": "==2.6" - }, - "requests": { - "hashes": [ - "sha256:6a1b267aa90cac58ac3a765d067950e7dbbf75b1da07e895d1f594193a40a38b", - "sha256:9c443e7324ba5b85070c4a818ade28bfabedf16ea10206da1132edaa6dda237e" - ], - "index": "pypi", - "version": "==2.18.4" - }, "tornado": { "hashes": [ "sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7", @@ -64,13 +35,6 @@ "index": "pypi", "version": "==1.1" }, - "urllib3": { - "hashes": [ - "sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b", - "sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f" - ], - "version": "==1.22" - }, "webassets": { "hashes": [ "sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db" diff --git a/atst/api_client.py b/atst/api_client.py index 3a68f707..c7e35fe7 100644 --- a/atst/api_client.py +++ b/atst/api_client.py @@ -33,7 +33,7 @@ class ApiClient(object): kwargs['body'] = dumps(kwargs['json']) del kwargs['json'] headers = kwargs.get('headers', {}) - headers['Content-Type'] = 'application-json' + headers['Content-Type'] = 'application/json' kwargs['headers'] = headers response = yield self.client.fetch(url, method=method, **kwargs) diff --git a/atst/app.py b/atst/app.py index 9334171d..06e00a9e 100644 --- a/atst/app.py +++ b/atst/app.py @@ -4,34 +4,62 @@ import tornado.web from tornado.web import url from atst.handlers.main import MainHandler +from atst.handlers.home import Home +from atst.handlers.login import Login from atst.handlers.workspace import Workspace from atst.handlers.request import Request from atst.handlers.request_new import RequestNew +from atst.handlers.dev import Dev from atst.home import home from atst.api_client import ApiClient +ENV = os.getenv("TORNADO_ENV", "dev") + def make_app(config): - authz_client = ApiClient(config['default']['AUTHZ_BASE_URL']) + authz_client = ApiClient(config["default"]["AUTHZ_BASE_URL"]) + authnid_client = ApiClient(config["default"]["AUTHNID_BASE_URL"]) + + routes = [ + url(r"/", Home, {"page": "login"}, name="main"), + url( + r"/login", + Login, + {"authnid_client": authnid_client}, + name="login", + ), + url(r"/home", MainHandler, {"page": "home"}, name="home"), + url( r"/workspaces/blank", MainHandler, {'page': 'workspaces_blank'}, name='workspaces_blank' ), + url( + r"/workspaces", + Workspace, + {"page": "workspaces", "authz_client": authz_client}, + name="workspaces", + ), + url(r"/requests", Request, {"page": "requests"}, name="requests"), + url(r"/requests/new", RequestNew, {"page": "requests_new"}, name="request_new"), + url( + r"/requests/new/([0-9])", + RequestNew, + {"page": "requests_new"}, + name="request_form", + ), + url(r"/users", MainHandler, {"page": "users"}, name="users"), + url(r"/reports", MainHandler, {"page": "reports"}, name="reports"), + url(r"/calculator", MainHandler, {"page": "calculator"}, name="calculator"), + ] + + if not ENV == "production": + routes += [url(r"/login-dev", Dev, {"action": "login"}, name="dev-login")] + + app = tornado.web.Application( + routes, + login_url="/", - app = tornado.web.Application([ - url( r"/", MainHandler, {'page': 'login'}, name='login' ), - url( r"/home", MainHandler, {'page': 'home'}, name='home' ), - url( r"/workspaces/blank", MainHandler, {'page': 'workspaces_blank'}, name='workspaces_blank' ), - url( r"/workspaces", - Workspace, - {'page': 'workspaces', 'authz_client': authz_client}, - name='workspaces'), - url( r"/requests", Request, {'page': 'requests'}, name='requests' ), - url( r"/requests/new", RequestNew, {'page': 'requests_new'}, name='request_new' ), - url( r"/requests/new/([0-9])", RequestNew, {'page': 'requests_new'}, name='request_form' ), - url( r"/users", MainHandler, {'page': 'users'}, name='users' ), - url( r"/reports", MainHandler, {'page': 'reports'}, name='reports' ), - url( r"/calculator", MainHandler, {'page': 'calculator'}, name='calculator' ), - ], template_path = home.child('templates'), static_path = home.child('static'), + cookie_secret=config["default"]["COOKIE_SECRET"], debug=config['default'].getboolean('DEBUG') ) return app @@ -40,12 +68,12 @@ def make_app(config): def make_config(): BASE_CONFIG_FILENAME = os.path.join( os.path.dirname(__file__), - '../config/base.ini', + "../config/base.ini" ) ENV_CONFIG_FILENAME = os.path.join( os.path.dirname(__file__), - '../config/', - '{}.ini'.format(os.getenv('TORNADO_ENV', 'dev').lower()) + "../config/", + "{}.ini".format(ENV.lower()) ) config = ConfigParser() diff --git a/atst/handler.py b/atst/handler.py index baa6f0ef..26595b3d 100644 --- a/atst/handler.py +++ b/atst/handler.py @@ -25,3 +25,13 @@ class BaseHandler(tornado.web.RequestHandler): ns = super(BaseHandler, self).get_template_namespace() ns.update(helpers) return ns + + def get_current_user(self): + if self.get_secure_cookie('atst'): + return True + else: + False + + # this is a temporary implementation until we have real sessions + def _start_session(self): + self.set_secure_cookie('atst', 'valid-user-session') diff --git a/atst/handlers/dev.py b/atst/handlers/dev.py new file mode 100644 index 00000000..a22fd456 --- /dev/null +++ b/atst/handlers/dev.py @@ -0,0 +1,13 @@ +from atst.handler import BaseHandler + +class Dev(BaseHandler): + def initialize(self, action): + self.action = action + + def get(self): + if self.action == 'login': + self._login() + + def _login(self): + self._start_session() + self.redirect("/home") diff --git a/atst/handlers/home.py b/atst/handlers/home.py new file mode 100644 index 00000000..d600dfaa --- /dev/null +++ b/atst/handlers/home.py @@ -0,0 +1,10 @@ +import tornado +from atst.handler import BaseHandler + +class Home(BaseHandler): + + def initialize(self, page): + self.page = page + + def get(self): + self.render( '%s.html.to' % self.page, page = self.page ) diff --git a/atst/handlers/login.py b/atst/handlers/login.py new file mode 100644 index 00000000..3a141340 --- /dev/null +++ b/atst/handlers/login.py @@ -0,0 +1,37 @@ +import tornado +from atst.handler import BaseHandler + + +class Login(BaseHandler): + + def initialize(self, authnid_client): + self.authnid_client = authnid_client + + @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 + + url = self.get_login_url() + self.redirect(url) + return + + @tornado.gen.coroutine + def _validate_login_token(self, token): + try: + response = yield self.authnid_client.post( + "/api/v1/validate", json={"token": token} + ) + return response.code == 200 + + except tornado.httpclient.HTTPError as error: + if error.response.code == 401: + return False + + else: + raise error diff --git a/atst/handlers/main.py b/atst/handlers/main.py index 62c6fa7e..71ff1395 100644 --- a/atst/handlers/main.py +++ b/atst/handlers/main.py @@ -1,3 +1,5 @@ +import atst +import tornado from atst.handler import BaseHandler class MainHandler(BaseHandler): @@ -5,5 +7,6 @@ class MainHandler(BaseHandler): def initialize(self, page): self.page = page + @tornado.web.authenticated def get(self): self.render( '%s.html.to' % self.page, page = self.page ) diff --git a/atst/handlers/request.py b/atst/handlers/request.py index 3ec27477..9ce3da34 100644 --- a/atst/handlers/request.py +++ b/atst/handlers/request.py @@ -1,3 +1,4 @@ +import tornado from atst.handler import BaseHandler mock_requests = [ @@ -31,5 +32,6 @@ class Request(BaseHandler): def initialize(self, page): self.page = page + @tornado.web.authenticated def get(self): self.render('requests.html.to', page = self.page, requests = mock_requests ) diff --git a/atst/handlers/request_new.py b/atst/handlers/request_new.py index aa405e3a..55a39bc9 100644 --- a/atst/handlers/request_new.py +++ b/atst/handlers/request_new.py @@ -1,3 +1,4 @@ +import tornado from atst.handler import BaseHandler class RequestNew(BaseHandler): @@ -22,6 +23,7 @@ class RequestNew(BaseHandler): def initialize(self, page): self.page = page + @tornado.web.authenticated def get(self, screen = 1): self.render( 'requests/screen-%d.html.to' % int(screen), page = self.page, diff --git a/atst/handlers/workspace.py b/atst/handlers/workspace.py index bbed8f6d..5e05ad1d 100644 --- a/atst/handlers/workspace.py +++ b/atst/handlers/workspace.py @@ -1,5 +1,5 @@ from atst.handler import BaseHandler -import requests +import tornado import tornado.gen mock_workspaces = [ @@ -13,8 +13,6 @@ mock_workspaces = [ } ] -session = requests.Session() - class Workspace(BaseHandler): def initialize(self, page, authz_client): @@ -22,5 +20,6 @@ class Workspace(BaseHandler): self.authz_client = authz_client @tornado.gen.coroutine + @tornado.web.authenticated def get(self): self.render( 'workspaces.html.to', page = self.page, workspaces = mock_workspaces ) diff --git a/config/base.ini b/config/base.ini index 98d53555..9747c782 100644 --- a/config/base.ini +++ b/config/base.ini @@ -1,5 +1,7 @@ [default] +PORT=8000 ENVIRONMENT = dev DEBUG = true AUTHZ_BASE_URL = http://localhost -PORT = 8000 +AUTHNID_BASE_URL= http://localhost +COOKIE_SECRET = some-secret-please-replace diff --git a/tests/test_auth.py b/tests/test_auth.py new file mode 100644 index 00000000..242083af --- /dev/null +++ b/tests/test_auth.py @@ -0,0 +1,55 @@ +import re +import pytest +import tornado.web +import tornado.gen + + +@pytest.mark.gen_test +def test_redirects_when_not_logged_in(http_client, base_url): + response = yield http_client.fetch( + base_url + "/home", raise_error=False, follow_redirects=False + ) + location = response.headers['Location'] + assert response.code == 302 + assert response.error + assert re.match('/\??', location) + + +@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 + + monkeypatch.setattr( + "atst.handlers.login.Login._validate_login_token", + _validate_login_token + ) + 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['Location'] == '/home' + assert response.code == 302 + + +@pytest.mark.gen_test +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.code == 302 + assert response.headers["Location"] == "/home" + + +@pytest.mark.gen_test +@pytest.mark.skip(reason="need to work out auth error user paths") +def test_login_with_invalid_bearer_token(http_client, base_url): + response = yield http_client.fetch( + base_url + "/home", + raise_error=False, + headers={"Cookie": "bearer-token=anything"}, + )