Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow dynamic repository credentials for authenticated Binderhub instances. #1169

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
40 changes: 38 additions & 2 deletions binderhub/base.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
"""Base classes for request handlers"""

import json
import os
import urllib.parse

import jwt
from http.client import responses
from tornado import web
from tornado.log import app_log
from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPError

from jupyterhub.services.auth import HubOAuthenticated, HubOAuth

from . import __version__ as binder_version
Expand All @@ -21,6 +24,7 @@ def initialize(self):
super().initialize()
if self.settings['auth_enabled']:
self.hub_auth = HubOAuth.instance(config=self.settings['traitlets_config'])
self.current_user_model = None

def prepare(self):
super().prepare()
Expand Down Expand Up @@ -156,14 +160,46 @@ def get_spec_from_request(self, prefix):
spec = self.request.path[idx + len(prefix) + 1:]
return spec

def get_provider(self, provider_prefix, spec):
async def get_provider(self, provider_prefix, spec):
"""Construct a provider object"""
providers = self.settings['repo_providers']
if provider_prefix not in providers:
raise web.HTTPError(404, "No provider found for prefix %s" % provider_prefix)

async def api_request(url, *args, **kwargs):
headers = kwargs.setdefault('headers', {})
headers.update({'Authorization': 'token %s' % self.hub_auth.api_token})
hub_api_url = os.getenv('JUPYTERHUB_API_URL', '') or self.hub_auth.api_url
request_url = hub_api_url + url
req = HTTPRequest(request_url, *args, **kwargs)

try:
return await AsyncHTTPClient().fetch(req)
except HTTPError as e:
app_log.error("Error accessing Hub API (using %s): %s", request_url, e)

async def get_current_user_model():
"""Get the current user model.
The user auth_state is only accessible to admin users.
"""
if not self.settings['auth_enabled']:
return None

if self.current_user_model is None:
username = self.get_current_user()['name']
resp = await api_request(
f'/users/{username}',
method='GET',
)
self.current_user_model = json.loads(resp.body.decode('utf-8'))

return self.current_user_model

return providers[provider_prefix](
config=self.settings['traitlets_config'], spec=spec)
config=self.settings['traitlets_config'],
spec=spec,
user_model=await get_current_user_model()
)

def get_badge_base_url(self):
badge_base_url = self.settings['badge_base_url']
Expand Down
2 changes: 1 addition & 1 deletion binderhub/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ async def get(self, provider_prefix, _unescaped_spec):

# get a provider object that encapsulates the provider and the spec
try:
provider = self.get_provider(provider_prefix, spec=spec)
provider = await self.get_provider(provider_prefix, spec=spec)
except Exception as e:
app_log.exception("Failed to get provider for %s", key)
await self.fail(str(e))
Expand Down
2 changes: 1 addition & 1 deletion binderhub/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async def get(self, provider_prefix, _unescaped_spec):
spec = self.get_spec_from_request(prefix)
spec = spec.rstrip("/")
try:
self.get_provider(provider_prefix, spec=spec)
await self.get_provider(provider_prefix, spec=spec)
except HTTPError:
raise
except Exception as e:
Expand Down