Skip to content
2 changes: 2 additions & 0 deletions remoteappmanager/jupyterhub/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .world_authenticator import WorldAuthenticator # noqa
from .github_whitelist_authenticator import GitHubWhitelistAuthenticator # noqa
53 changes: 53 additions & 0 deletions remoteappmanager/jupyterhub/auth/github_whitelist_authenticator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import os
from oauthenticator import GitHubOAuthenticator
from traitlets.config import LoggingConfigurable
from traitlets import Unicode, Float, Set


class FileWhitelistMixin(LoggingConfigurable):
"""
"""

#: The path of the whitelist file.
whitelist_file = Unicode()

#: When the file was last modified, so that we can reload appropriately.
_whitelist_file_last_modified = Float()

#: Cached whitelist to return every time the file hasn't changed.
_whitelist = Set()

@property
def whitelist(self):
try:
cur_mtime = os.path.getmtime(self.whitelist_file)
if cur_mtime <= self._whitelist_file_last_modified:
# File older than last change.
# keep using the current cached whitelist
return self._whitelist

self.log.info("Whitelist file more recent than the old one. "
"Updating whitelist.")

with open(self.whitelist_file, "r") as f:
whitelisted_users = set(x.strip() for x in f.readlines())
except FileNotFoundError:
# empty set means everybody is allowed
return {}
except Exception:
# For other exceptions, assume the file is broken, log it
# and return what we have.
self.log.exception("Unable to access whitelist.")
return self._whitelist

self._whitelist = whitelisted_users
self._whitelist_file_last_modified = cur_mtime

return self._whitelist


class GitHubWhitelistAuthenticator(FileWhitelistMixin, GitHubOAuthenticator):
"""A github authenticator that also verifies that the
user belongs to a specified whitelisted user as provided
by an external file (so that we don't have to restart
the service to change the whitelisted users"""
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import os

import time
from tornado.testing import AsyncTestCase, gen_test

from remoteappmanager.tests.temp_mixin import TempMixin
from remoteappmanager.tests.utils import mock_coro_factory

from unittest.mock import Mock, patch

from remoteappmanager.jupyterhub.auth import GitHubWhitelistAuthenticator


class TestGithubWhiteListAuthenticator(TempMixin, AsyncTestCase):
def setUp(self):
self.auth = GitHubWhitelistAuthenticator()
self.auth.authenticate = mock_coro_factory(return_value="foo")
super().setUp()

@gen_test
def test_basic_auth(self):
auth = self.auth

response = yield auth.authenticate(Mock(), {"username": "foo"})
self.assertEqual(response, "foo")

@gen_test
def test_basic_auth_with_whitelist_file(self):
whitelist_path = os.path.join(self.tempdir, "whitelist.txt")
with open(whitelist_path, "w") as f:
f.write("foo\n")
f.write("bar\n")

auth = self.auth
auth.whitelist_file = whitelist_path

response = yield auth.get_authenticated_user(Mock(),
{"username": "foo"})
self.assertEqual(response, "foo")

# Check again to touch the code that does not trigger another load
response = yield auth.get_authenticated_user(Mock(),
{"username": "foo"})
self.assertEqual(response, "foo")

# wait one second, so that we see a change in mtime.
time.sleep(1)

# Change the file and see if we get a different behavior
with open(whitelist_path, "w") as f:
f.write("bar\n")

response = yield auth.get_authenticated_user(Mock(),
{"username": "foo"})
self.assertEqual(response, None)

@gen_test
def test_basic_auth_without_whitelist_file(self):
auth = self.auth
auth.whitelist_file = "/does/not/exist.txt"

response = yield auth.get_authenticated_user(Mock(),
{"username": "foo"})

# Should be equivalent to no whitelist, so everybody allowed
self.assertEqual(response, "foo")

@gen_test
def test_exception_during_read(self):
whitelist_path = os.path.join(self.tempdir, "whitelist.txt")
with open(whitelist_path, "w") as f:
f.write("bar\n")

auth = self.auth
auth.whitelist_file = whitelist_path

# Do the first triggering, so that we load the file content.
response = yield auth.get_authenticated_user(Mock(),
{"username": "foo"})

self.assertEqual(response, None)

# Then try again with an exception occurring
with patch("os.path.getmtime") as p:
p.side_effect = Exception("BOOM!")

response = yield auth.get_authenticated_user(Mock(),
{"username": "foo"})
self.assertEqual(response, None)
12 changes: 12 additions & 0 deletions remoteappmanager/jupyterhub/auth/tests/test_world_authenticator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from tornado.testing import AsyncTestCase, gen_test
from unittest.mock import Mock

from remoteappmanager.jupyterhub.auth import WorldAuthenticator


class TestWorldAuthenticator(AsyncTestCase):
@gen_test
def test_basic_auth(self):
auth = WorldAuthenticator()
response = yield auth.authenticate(Mock(), {"username": "foo"})
self.assertEqual(response, "foo")
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class WorldAuthenticator(Authenticator):
''' This authenticator authenticates everyone '''
""" This authenticator authenticates everyone """

@gen.coroutine
def authenticate(self, handler, data):
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ jupyter_client==4.3.0
click==6.6
tabulate==0.7.5
git+git://github.com/simphony/tornado-webapi.git#egg=tornadowebapi
oauthenticator==0.5.1
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"jupyter_client>=4.3.0",
"click>=6.6",
"tabulate>=0.7.5",
"oauthenticator>=0.5",
]

# Unfortunately RTD cannot install jupyterhub because jupyterhub needs bower,
Expand Down