Merge pull request #30 from dod-ccpo/basic-auth

Basic authentication
This commit is contained in:
Jason Garber - Ctr 2018-06-14 13:18:08 -04:00 committed by GitHub
commit 6cf7a7bffa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 186 additions and 62 deletions

View File

@ -7,7 +7,6 @@ name = "pypi"
tornado = "==5.0.2" tornado = "==5.0.2"
webassets = "==0.12.1" webassets = "==0.12.1"
Unipath = "==1.1" Unipath = "==1.1"
requests = "*"
[dev-packages] [dev-packages]
pytest = "==3.6.0" pytest = "==3.6.0"

38
Pipfile.lock generated
View File

@ -1,7 +1,7 @@
{ {
"_meta": { "_meta": {
"hash": { "hash": {
"sha256": "68a0d5093979093899f0f86faa82eb55f90f9a67a16b11a5701ea85096e72ee8" "sha256": "391e254ddb902877afca9c07aa2306710ce6d1e207b029c1a8b5dc0115ee99a5"
}, },
"pipfile-spec": 6, "pipfile-spec": 6,
"requires": { "requires": {
@ -16,35 +16,6 @@
] ]
}, },
"default": { "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": { "tornado": {
"hashes": [ "hashes": [
"sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7", "sha256:1b83d5c10550f2653380b4c77331d6f8850f287c4f67d7ce1e1c639d9222fbc7",
@ -64,13 +35,6 @@
"index": "pypi", "index": "pypi",
"version": "==1.1" "version": "==1.1"
}, },
"urllib3": {
"hashes": [
"sha256:06330f386d6e4b195fbfc736b297f58c5a892e4440e54d294d7004e3a9bbea1b",
"sha256:cc44da8e1145637334317feebd728bd869a35285b93cbb4cca2577da7e62db4f"
],
"version": "==1.22"
},
"webassets": { "webassets": {
"hashes": [ "hashes": [
"sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db" "sha256:e7d9c8887343123fd5b32309b33167428cb1318cdda97ece12d0907fd69d38db"

View File

@ -33,7 +33,7 @@ class ApiClient(object):
kwargs['body'] = dumps(kwargs['json']) kwargs['body'] = dumps(kwargs['json'])
del kwargs['json'] del kwargs['json']
headers = kwargs.get('headers', {}) headers = kwargs.get('headers', {})
headers['Content-Type'] = 'application-json' headers['Content-Type'] = 'application/json'
kwargs['headers'] = headers kwargs['headers'] = headers
response = yield self.client.fetch(url, method=method, **kwargs) response = yield self.client.fetch(url, method=method, **kwargs)

View File

@ -4,34 +4,62 @@ import tornado.web
from tornado.web import url from tornado.web import url
from atst.handlers.main import MainHandler 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.workspace import Workspace
from atst.handlers.request import Request from atst.handlers.request import Request
from atst.handlers.request_new import RequestNew from atst.handlers.request_new import RequestNew
from atst.handlers.dev import Dev
from atst.home import home from atst.home import home
from atst.api_client import ApiClient from atst.api_client import ApiClient
ENV = os.getenv("TORNADO_ENV", "dev")
def make_app(config): 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"])
app = tornado.web.Application([ routes = [
url( r"/", MainHandler, {'page': 'login'}, name='login' ), url(r"/", Home, {"page": "login"}, name="main"),
url( r"/home", MainHandler, {'page': 'home'}, name='home' ), 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/blank", MainHandler, {'page': 'workspaces_blank'}, name='workspaces_blank' ),
url( r"/workspaces", url(
r"/workspaces",
Workspace, Workspace,
{'page': 'workspaces', 'authz_client': authz_client}, {"page": "workspaces", "authz_client": authz_client},
name='workspaces'), name="workspaces",
url( r"/requests", Request, {'page': 'requests'}, name='requests' ), ),
url( r"/requests/new", RequestNew, {'page': 'requests_new'}, name='request_new' ), url(r"/requests", Request, {"page": "requests"}, name="requests"),
url( r"/requests/new/([0-9])", RequestNew, {'page': 'requests_new'}, name='request_form' ), url(r"/requests/new", RequestNew, {"page": "requests_new"}, name="request_new"),
url( r"/users", MainHandler, {'page': 'users'}, name='users' ), url(
url( r"/reports", MainHandler, {'page': 'reports'}, name='reports' ), r"/requests/new/([0-9])",
url( r"/calculator", MainHandler, {'page': 'calculator'}, name='calculator' ), 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="/",
template_path = home.child('templates'), template_path = home.child('templates'),
static_path = home.child('static'), static_path = home.child('static'),
cookie_secret=config["default"]["COOKIE_SECRET"],
debug=config['default'].getboolean('DEBUG') debug=config['default'].getboolean('DEBUG')
) )
return app return app
@ -40,12 +68,12 @@ def make_app(config):
def make_config(): def make_config():
BASE_CONFIG_FILENAME = os.path.join( BASE_CONFIG_FILENAME = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
'../config/base.ini', "../config/base.ini"
) )
ENV_CONFIG_FILENAME = os.path.join( ENV_CONFIG_FILENAME = os.path.join(
os.path.dirname(__file__), os.path.dirname(__file__),
'../config/', "../config/",
'{}.ini'.format(os.getenv('TORNADO_ENV', 'dev').lower()) "{}.ini".format(ENV.lower())
) )
config = ConfigParser() config = ConfigParser()

View File

@ -25,3 +25,13 @@ class BaseHandler(tornado.web.RequestHandler):
ns = super(BaseHandler, self).get_template_namespace() ns = super(BaseHandler, self).get_template_namespace()
ns.update(helpers) ns.update(helpers)
return ns 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')

13
atst/handlers/dev.py Normal file
View File

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

10
atst/handlers/home.py Normal file
View File

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

37
atst/handlers/login.py Normal file
View File

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

View File

@ -1,3 +1,5 @@
import atst
import tornado
from atst.handler import BaseHandler from atst.handler import BaseHandler
class MainHandler(BaseHandler): class MainHandler(BaseHandler):
@ -5,5 +7,6 @@ class MainHandler(BaseHandler):
def initialize(self, page): def initialize(self, page):
self.page = page self.page = page
@tornado.web.authenticated
def get(self): def get(self):
self.render( '%s.html.to' % self.page, page = self.page ) self.render( '%s.html.to' % self.page, page = self.page )

View File

@ -1,3 +1,4 @@
import tornado
from atst.handler import BaseHandler from atst.handler import BaseHandler
mock_requests = [ mock_requests = [
@ -31,5 +32,6 @@ class Request(BaseHandler):
def initialize(self, page): def initialize(self, page):
self.page = page self.page = page
@tornado.web.authenticated
def get(self): def get(self):
self.render('requests.html.to', page = self.page, requests = mock_requests ) self.render('requests.html.to', page = self.page, requests = mock_requests )

View File

@ -1,3 +1,4 @@
import tornado
from atst.handler import BaseHandler from atst.handler import BaseHandler
class RequestNew(BaseHandler): class RequestNew(BaseHandler):
@ -22,6 +23,7 @@ class RequestNew(BaseHandler):
def initialize(self, page): def initialize(self, page):
self.page = page self.page = page
@tornado.web.authenticated
def get(self, screen = 1): def get(self, screen = 1):
self.render( 'requests/screen-%d.html.to' % int(screen), self.render( 'requests/screen-%d.html.to' % int(screen),
page = self.page, page = self.page,

View File

@ -1,5 +1,5 @@
from atst.handler import BaseHandler from atst.handler import BaseHandler
import requests import tornado
import tornado.gen import tornado.gen
mock_workspaces = [ mock_workspaces = [
@ -13,8 +13,6 @@ mock_workspaces = [
} }
] ]
session = requests.Session()
class Workspace(BaseHandler): class Workspace(BaseHandler):
def initialize(self, page, authz_client): def initialize(self, page, authz_client):
@ -22,5 +20,6 @@ class Workspace(BaseHandler):
self.authz_client = authz_client self.authz_client = authz_client
@tornado.gen.coroutine @tornado.gen.coroutine
@tornado.web.authenticated
def get(self): def get(self):
self.render( 'workspaces.html.to', page = self.page, workspaces = mock_workspaces ) self.render( 'workspaces.html.to', page = self.page, workspaces = mock_workspaces )

View File

@ -1,5 +1,7 @@
[default] [default]
PORT=8000
ENVIRONMENT = dev ENVIRONMENT = dev
DEBUG = true DEBUG = true
AUTHZ_BASE_URL = http://localhost AUTHZ_BASE_URL = http://localhost
PORT = 8000 AUTHNID_BASE_URL= http://localhost
COOKIE_SECRET = some-secret-please-replace

55
tests/test_auth.py Normal file
View File

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