Compare commits
No commits in common. "331cee1abcb90233721f62243c05abb97256c2cf" and "54519540e3e144fd6febda1ac5cf1b7cf823f9da" have entirely different histories.
331cee1abc
...
54519540e3
|
@ -7,6 +7,7 @@ kind: Kustomization
|
||||||
helmCharts:
|
helmCharts:
|
||||||
- includeCRDs: true
|
- includeCRDs: true
|
||||||
name: jupyterhub
|
name: jupyterhub
|
||||||
|
namespace: jupyterhub
|
||||||
releaseName: jupyterhub
|
releaseName: jupyterhub
|
||||||
valuesFile: ./values/values.yaml
|
valuesFile: ./values/values.yaml
|
||||||
version: 3.0.3
|
version: 3.0.3
|
||||||
|
|
|
@ -1,280 +1,6 @@
|
||||||
debug:
|
debug:
|
||||||
enabled: true
|
enabled: true
|
||||||
hub:
|
hub:
|
||||||
config:
|
|
||||||
Authenticator:
|
|
||||||
auto_login: true
|
|
||||||
enable_auth_state: true
|
|
||||||
JupyterHub:
|
|
||||||
tornado_settings:
|
|
||||||
headers: { 'Content-Security-Policy': "frame-ancestors *;" }
|
|
||||||
db:
|
|
||||||
pvc:
|
|
||||||
storageClassName: csi-sc-cinderplugin
|
|
||||||
extraConfig:
|
|
||||||
oauthCode: |
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
from oauthenticator.generic import GenericOAuthenticator
|
|
||||||
token_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/index.php/apps/oauth2/api/v1/token'
|
|
||||||
debug = os.environ.get('NEXTCLOUD_DEBUG_OAUTH', 'false').lower() in ['true', '1', 'yes']
|
|
||||||
|
|
||||||
def get_nextcloud_access_token(refresh_token):
|
|
||||||
client_id = os.environ['NEXTCLOUD_CLIENT_ID']
|
|
||||||
client_secret = os.environ['NEXTCLOUD_CLIENT_SECRET']
|
|
||||||
|
|
||||||
code = refresh_token
|
|
||||||
data = {
|
|
||||||
'grant_type': 'refresh_token',
|
|
||||||
'code': code,
|
|
||||||
'refresh_token': refresh_token,
|
|
||||||
'client_id': client_id,
|
|
||||||
'client_secret': client_secret
|
|
||||||
}
|
|
||||||
response = requests.post(token_url, data=data)
|
|
||||||
if debug:
|
|
||||||
print(response.text)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def post_auth_hook(authenticator, handler, authentication):
|
|
||||||
user = authentication['auth_state']['oauth_user']['ocs']['data']['id']
|
|
||||||
auth_state = authentication['auth_state']
|
|
||||||
auth_state['token_expires'] = time.time() + auth_state['token_response']['expires_in']
|
|
||||||
authentication['auth_state'] = auth_state
|
|
||||||
return authentication
|
|
||||||
|
|
||||||
class NextcloudOAuthenticator(GenericOAuthenticator):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.user_dict = {}
|
|
||||||
|
|
||||||
async def pre_spawn_start(self, user, spawner):
|
|
||||||
super().pre_spawn_start(user, spawner)
|
|
||||||
auth_state = await user.get_auth_state()
|
|
||||||
if not auth_state:
|
|
||||||
return
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
spawner.environment['NEXTCLOUD_ACCESS_TOKEN'] = access_token
|
|
||||||
|
|
||||||
async def refresh_user(self, user, handler=None):
|
|
||||||
auth_state = await user.get_auth_state()
|
|
||||||
if not auth_state:
|
|
||||||
if debug:
|
|
||||||
print(f'auth_state missing for {user}')
|
|
||||||
return False
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
refresh_token = auth_state['refresh_token']
|
|
||||||
token_response = auth_state['token_response']
|
|
||||||
now = time.time()
|
|
||||||
now_hr = datetime.fromtimestamp(now)
|
|
||||||
expires = auth_state['token_expires']
|
|
||||||
expires_hr = datetime.fromtimestamp(expires)
|
|
||||||
expires = 0
|
|
||||||
if debug:
|
|
||||||
print(f'auth_state for {user}: {auth_state}')
|
|
||||||
if now >= expires:
|
|
||||||
if debug:
|
|
||||||
print(f'Time is: {now_hr}, token expired: {expires_hr}')
|
|
||||||
print(f'Refreshing token for {user}')
|
|
||||||
try:
|
|
||||||
token_response = get_nextcloud_access_token(refresh_token)
|
|
||||||
auth_state['access_token'] = token_response['access_token']
|
|
||||||
auth_state['refresh_token'] = token_response['refresh_token']
|
|
||||||
auth_state['token_expires'] = now + token_response['expires_in']
|
|
||||||
auth_state['token_response'] = token_response
|
|
||||||
if debug:
|
|
||||||
print(f'Successfully refreshed token for {user.name}')
|
|
||||||
print(f'auth_state for {user.name}: {auth_state}')
|
|
||||||
return {'name': user.name, 'auth_state': auth_state}
|
|
||||||
except Exception as e:
|
|
||||||
if debug:
|
|
||||||
print(f'Failed to refresh token for {user}')
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
if debug:
|
|
||||||
print(f'Time is: {now_hr}, token expires: {expires_hr}')
|
|
||||||
return True
|
|
||||||
|
|
||||||
c.JupyterHub.authenticator_class = NextcloudOAuthenticator
|
|
||||||
c.NextcloudOAuthenticator.client_id = os.environ['NEXTCLOUD_CLIENT_ID']
|
|
||||||
c.NextcloudOAuthenticator.client_secret = os.environ['NEXTCLOUD_CLIENT_SECRET']
|
|
||||||
c.NextcloudOAuthenticator.login_service = 'Sunet Drive'
|
|
||||||
c.NextcloudOAuthenticator.username_claim = lambda r: r.get('ocs', {}).get('data', {}).get('id')
|
|
||||||
c.NextcloudOAuthenticator.userdata_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/ocs/v2.php/cloud/user?format=json'
|
|
||||||
c.NextcloudOAuthenticator.authorize_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/index.php/apps/oauth2/authorize'
|
|
||||||
c.NextcloudOAuthenticator.token_url = token_url
|
|
||||||
c.NextcloudOAuthenticator.oauth_callback_url = 'https://' + os.environ['JUPYTER_HOST'] + '/hub/oauth_callback'
|
|
||||||
c.NextcloudOAuthenticator.allow_all = True
|
|
||||||
c.NextcloudOAuthenticator.refresh_pre_spawn = True
|
|
||||||
c.NextcloudOAuthenticator.enable_auth_state = True
|
|
||||||
c.NextcloudOAuthenticator.auth_refresh_age = 3600
|
|
||||||
c.NextcloudOAuthenticator.post_auth_hook = post_auth_hook
|
|
||||||
|
|
||||||
serviceCode: |
|
|
||||||
import sys
|
|
||||||
c.JupyterHub.load_roles = [
|
|
||||||
{
|
|
||||||
"name": "refresh-token",
|
|
||||||
"services": [
|
|
||||||
"refresh-token"
|
|
||||||
],
|
|
||||||
"scopes": [
|
|
||||||
"read:users",
|
|
||||||
"admin:auth_state"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user",
|
|
||||||
"scopes": [
|
|
||||||
"access:services!service=refresh-token",
|
|
||||||
"read:services!service=refresh-token",
|
|
||||||
"self",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "server",
|
|
||||||
"scopes": [
|
|
||||||
"access:services!service=refresh-token",
|
|
||||||
"read:services!service=refresh-token",
|
|
||||||
"inherit",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
c.JupyterHub.services = [
|
|
||||||
{
|
|
||||||
'name': 'refresh-token',
|
|
||||||
'url': 'http://' + os.environ.get('HUB_SERVICE_HOST', 'hub') + ':' + os.environ.get('HUB_SERVICE_PORT_REFRESH_TOKEN', '8082'),
|
|
||||||
'display': False,
|
|
||||||
'oauth_no_confirm': True,
|
|
||||||
'api_token': os.environ['JUPYTERHUB_API_KEY'],
|
|
||||||
'command': [sys.executable, '/usr/local/etc/jupyterhub/refresh-token.py']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
c.JupyterHub.admin_users = {"refresh-token"}
|
|
||||||
c.JupyterHub.api_tokens = {
|
|
||||||
os.environ['JUPYTERHUB_API_KEY']: "refresh-token",
|
|
||||||
}
|
|
||||||
extraFiles:
|
|
||||||
refresh-token.py:
|
|
||||||
mountPath: /usr/local/etc/jupyterhub/refresh-token.py
|
|
||||||
stringData: |
|
|
||||||
"""A token refresh service authenticating with the Hub.
|
|
||||||
|
|
||||||
This service serves `/services/refresh-token/`,
|
|
||||||
authenticated with the Hub,
|
|
||||||
showing the user their own info.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import socket
|
|
||||||
from jupyterhub.services.auth import HubAuthenticated
|
|
||||||
from jupyterhub.utils import url_path_join
|
|
||||||
from tornado.httpserver import HTTPServer
|
|
||||||
from tornado.ioloop import IOLoop
|
|
||||||
from tornado.web import Application, HTTPError, RequestHandler, authenticated
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
debug = os.environ.get('NEXTCLOUD_DEBUG_OAUTH', 'false').lower() in ['true', '1', 'yes']
|
|
||||||
def my_debug(s):
|
|
||||||
if debug:
|
|
||||||
with open("/proc/1/fd/1", "a") as stdout:
|
|
||||||
print(s, file=stdout)
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshHandler(HubAuthenticated, RequestHandler):
|
|
||||||
def api_request(self, method, url, **kwargs):
|
|
||||||
my_debug(f'{self.hub_auth}')
|
|
||||||
url = url_path_join(self.hub_auth.api_url, url)
|
|
||||||
allow_404 = kwargs.pop('allow_404', False)
|
|
||||||
headers = kwargs.setdefault('headers', {})
|
|
||||||
headers.setdefault('Authorization', f'token {self.hub_auth.api_token}')
|
|
||||||
try:
|
|
||||||
r = requests.request(method, url, **kwargs)
|
|
||||||
except requests.ConnectionError as e:
|
|
||||||
my_debug(f'Error connecting to {url}: {e}')
|
|
||||||
msg = f'Failed to connect to Hub API at {url}.'
|
|
||||||
msg += f' Is the Hub accessible at this URL (from host: {socket.gethostname()})?'
|
|
||||||
|
|
||||||
if '127.0.0.1' in url:
|
|
||||||
msg += ' Make sure to set c.JupyterHub.hub_ip to an IP accessible to' + \
|
|
||||||
' single-user servers if the servers are not on the same host as the Hub.'
|
|
||||||
raise HTTPError(500, msg)
|
|
||||||
|
|
||||||
data = None
|
|
||||||
if r.status_code == 404 and allow_404:
|
|
||||||
pass
|
|
||||||
elif r.status_code == 403:
|
|
||||||
my_debug(
|
|
||||||
'Lacking permission to check authorization with JupyterHub,' +
|
|
||||||
f' my auth token may have expired: [{r.status_code}] {r.reason}'
|
|
||||||
)
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(
|
|
||||||
500,
|
|
||||||
'Permission failure checking authorization, I may need a new token'
|
|
||||||
)
|
|
||||||
elif r.status_code >= 500:
|
|
||||||
my_debug(f'Upstream failure verifying auth token: [{r.status_code}] {r.reason}')
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(
|
|
||||||
502, 'Failed to check authorization (upstream problem)')
|
|
||||||
elif r.status_code >= 400:
|
|
||||||
my_debug(f'Failed to check authorization: [{r.status_code}] {r.reason}')
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(500, 'Failed to check authorization')
|
|
||||||
else:
|
|
||||||
data = r.json()
|
|
||||||
return data
|
|
||||||
|
|
||||||
@authenticated
|
|
||||||
def get(self):
|
|
||||||
user_model = self.get_current_user()
|
|
||||||
# Fetch current auth state
|
|
||||||
user_data = self.api_request('GET', url_path_join('users', user_model['name']))
|
|
||||||
auth_state = user_data['auth_state']
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
token_expires = auth_state['token_expires']
|
|
||||||
|
|
||||||
self.set_header('content-type', 'application/json')
|
|
||||||
self.write(json.dumps({'access_token': access_token, 'token_expires': token_expires}, indent=1, sort_keys=True))
|
|
||||||
|
|
||||||
class PingHandler(RequestHandler):
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
my_debug(f"DEBUG: In ping get")
|
|
||||||
self.set_header('content-type', 'application/json')
|
|
||||||
self.write(json.dumps({'ping': 1}))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = Application([
|
|
||||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + 'tokens', RefreshHandler),
|
|
||||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', PingHandler),
|
|
||||||
])
|
|
||||||
|
|
||||||
http_server = HTTPServer(app)
|
|
||||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
|
||||||
|
|
||||||
http_server.listen(url.port)
|
|
||||||
|
|
||||||
IOLoop.current().start()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
networkPolicy:
|
|
||||||
ingress:
|
|
||||||
- ports:
|
|
||||||
- port: 8082
|
|
||||||
from:
|
|
||||||
- podSelector:
|
|
||||||
matchLabels:
|
|
||||||
hub.jupyter.org/network-access-hub: "true"
|
|
||||||
service:
|
|
||||||
extraPorts:
|
|
||||||
- port: 8082
|
|
||||||
targetPort: 8082
|
|
||||||
name: refresh-token
|
|
||||||
extraEnv:
|
extraEnv:
|
||||||
NEXTCLOUD_DEBUG_OAUTH: "no"
|
NEXTCLOUD_DEBUG_OAUTH: "no"
|
||||||
NEXTCLOUD_HOST: hig.drive.test.sunet.se
|
NEXTCLOUD_HOST: hig.drive.test.sunet.se
|
||||||
|
@ -301,35 +27,3 @@ hub:
|
||||||
key: client-secret
|
key: client-secret
|
||||||
networkPolicy:
|
networkPolicy:
|
||||||
enabled: false
|
enabled: false
|
||||||
proxy:
|
|
||||||
chp:
|
|
||||||
networkPolicy:
|
|
||||||
egress:
|
|
||||||
- to:
|
|
||||||
- podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: jupyterhub
|
|
||||||
component: hub
|
|
||||||
ports:
|
|
||||||
- port: 8082
|
|
||||||
singleuser:
|
|
||||||
image:
|
|
||||||
name: docker.sunet.se/drive/jupyter-custom
|
|
||||||
tag: lab-4.0.10-sunet3
|
|
||||||
storage:
|
|
||||||
dynamic:
|
|
||||||
storageClass: csi-sc-cinderplugin
|
|
||||||
extraEnv:
|
|
||||||
JUPYTER_ENABLE_LAB: "yes"
|
|
||||||
extraFiles:
|
|
||||||
jupyter_notebook_config:
|
|
||||||
mountPath: /home/jovyan/.jupyter/jupyter_server_config.py
|
|
||||||
stringData: |
|
|
||||||
import os
|
|
||||||
c = get_config()
|
|
||||||
c.NotebookApp.allow_origin = '*'
|
|
||||||
c.NotebookApp.tornado_settings = {
|
|
||||||
'headers': { 'Content-Security-Policy': "frame-ancestors *;" }
|
|
||||||
}
|
|
||||||
os.system('/usr/local/bin/nc-sync')
|
|
||||||
mode: 0644
|
|
||||||
|
|
|
@ -1,280 +1,6 @@
|
||||||
debug:
|
debug:
|
||||||
enabled: true
|
enabled: true
|
||||||
hub:
|
hub:
|
||||||
config:
|
|
||||||
Authenticator:
|
|
||||||
auto_login: true
|
|
||||||
enable_auth_state: true
|
|
||||||
JupyterHub:
|
|
||||||
tornado_settings:
|
|
||||||
headers: { 'Content-Security-Policy': "frame-ancestors *;" }
|
|
||||||
db:
|
|
||||||
pvc:
|
|
||||||
storageClassName: csi-sc-cinderplugin
|
|
||||||
extraConfig:
|
|
||||||
oauthCode: |
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
from oauthenticator.generic import GenericOAuthenticator
|
|
||||||
token_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/index.php/apps/oauth2/api/v1/token'
|
|
||||||
debug = os.environ.get('NEXTCLOUD_DEBUG_OAUTH', 'false').lower() in ['true', '1', 'yes']
|
|
||||||
|
|
||||||
def get_nextcloud_access_token(refresh_token):
|
|
||||||
client_id = os.environ['NEXTCLOUD_CLIENT_ID']
|
|
||||||
client_secret = os.environ['NEXTCLOUD_CLIENT_SECRET']
|
|
||||||
|
|
||||||
code = refresh_token
|
|
||||||
data = {
|
|
||||||
'grant_type': 'refresh_token',
|
|
||||||
'code': code,
|
|
||||||
'refresh_token': refresh_token,
|
|
||||||
'client_id': client_id,
|
|
||||||
'client_secret': client_secret
|
|
||||||
}
|
|
||||||
response = requests.post(token_url, data=data)
|
|
||||||
if debug:
|
|
||||||
print(response.text)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def post_auth_hook(authenticator, handler, authentication):
|
|
||||||
user = authentication['auth_state']['oauth_user']['ocs']['data']['id']
|
|
||||||
auth_state = authentication['auth_state']
|
|
||||||
auth_state['token_expires'] = time.time() + auth_state['token_response']['expires_in']
|
|
||||||
authentication['auth_state'] = auth_state
|
|
||||||
return authentication
|
|
||||||
|
|
||||||
class NextcloudOAuthenticator(GenericOAuthenticator):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.user_dict = {}
|
|
||||||
|
|
||||||
async def pre_spawn_start(self, user, spawner):
|
|
||||||
super().pre_spawn_start(user, spawner)
|
|
||||||
auth_state = await user.get_auth_state()
|
|
||||||
if not auth_state:
|
|
||||||
return
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
spawner.environment['NEXTCLOUD_ACCESS_TOKEN'] = access_token
|
|
||||||
|
|
||||||
async def refresh_user(self, user, handler=None):
|
|
||||||
auth_state = await user.get_auth_state()
|
|
||||||
if not auth_state:
|
|
||||||
if debug:
|
|
||||||
print(f'auth_state missing for {user}')
|
|
||||||
return False
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
refresh_token = auth_state['refresh_token']
|
|
||||||
token_response = auth_state['token_response']
|
|
||||||
now = time.time()
|
|
||||||
now_hr = datetime.fromtimestamp(now)
|
|
||||||
expires = auth_state['token_expires']
|
|
||||||
expires_hr = datetime.fromtimestamp(expires)
|
|
||||||
expires = 0
|
|
||||||
if debug:
|
|
||||||
print(f'auth_state for {user}: {auth_state}')
|
|
||||||
if now >= expires:
|
|
||||||
if debug:
|
|
||||||
print(f'Time is: {now_hr}, token expired: {expires_hr}')
|
|
||||||
print(f'Refreshing token for {user}')
|
|
||||||
try:
|
|
||||||
token_response = get_nextcloud_access_token(refresh_token)
|
|
||||||
auth_state['access_token'] = token_response['access_token']
|
|
||||||
auth_state['refresh_token'] = token_response['refresh_token']
|
|
||||||
auth_state['token_expires'] = now + token_response['expires_in']
|
|
||||||
auth_state['token_response'] = token_response
|
|
||||||
if debug:
|
|
||||||
print(f'Successfully refreshed token for {user.name}')
|
|
||||||
print(f'auth_state for {user.name}: {auth_state}')
|
|
||||||
return {'name': user.name, 'auth_state': auth_state}
|
|
||||||
except Exception as e:
|
|
||||||
if debug:
|
|
||||||
print(f'Failed to refresh token for {user}')
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
if debug:
|
|
||||||
print(f'Time is: {now_hr}, token expires: {expires_hr}')
|
|
||||||
return True
|
|
||||||
|
|
||||||
c.JupyterHub.authenticator_class = NextcloudOAuthenticator
|
|
||||||
c.NextcloudOAuthenticator.client_id = os.environ['NEXTCLOUD_CLIENT_ID']
|
|
||||||
c.NextcloudOAuthenticator.client_secret = os.environ['NEXTCLOUD_CLIENT_SECRET']
|
|
||||||
c.NextcloudOAuthenticator.login_service = 'Sunet Drive'
|
|
||||||
c.NextcloudOAuthenticator.username_claim = lambda r: r.get('ocs', {}).get('data', {}).get('id')
|
|
||||||
c.NextcloudOAuthenticator.userdata_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/ocs/v2.php/cloud/user?format=json'
|
|
||||||
c.NextcloudOAuthenticator.authorize_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/index.php/apps/oauth2/authorize'
|
|
||||||
c.NextcloudOAuthenticator.token_url = token_url
|
|
||||||
c.NextcloudOAuthenticator.oauth_callback_url = 'https://' + os.environ['JUPYTER_HOST'] + '/hub/oauth_callback'
|
|
||||||
c.NextcloudOAuthenticator.allow_all = True
|
|
||||||
c.NextcloudOAuthenticator.refresh_pre_spawn = True
|
|
||||||
c.NextcloudOAuthenticator.enable_auth_state = True
|
|
||||||
c.NextcloudOAuthenticator.auth_refresh_age = 3600
|
|
||||||
c.NextcloudOAuthenticator.post_auth_hook = post_auth_hook
|
|
||||||
|
|
||||||
serviceCode: |
|
|
||||||
import sys
|
|
||||||
c.JupyterHub.load_roles = [
|
|
||||||
{
|
|
||||||
"name": "refresh-token",
|
|
||||||
"services": [
|
|
||||||
"refresh-token"
|
|
||||||
],
|
|
||||||
"scopes": [
|
|
||||||
"read:users",
|
|
||||||
"admin:auth_state"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user",
|
|
||||||
"scopes": [
|
|
||||||
"access:services!service=refresh-token",
|
|
||||||
"read:services!service=refresh-token",
|
|
||||||
"self",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "server",
|
|
||||||
"scopes": [
|
|
||||||
"access:services!service=refresh-token",
|
|
||||||
"read:services!service=refresh-token",
|
|
||||||
"inherit",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
c.JupyterHub.services = [
|
|
||||||
{
|
|
||||||
'name': 'refresh-token',
|
|
||||||
'url': 'http://' + os.environ.get('HUB_SERVICE_HOST', 'hub') + ':' + os.environ.get('HUB_SERVICE_PORT_REFRESH_TOKEN', '8082'),
|
|
||||||
'display': False,
|
|
||||||
'oauth_no_confirm': True,
|
|
||||||
'api_token': os.environ['JUPYTERHUB_API_KEY'],
|
|
||||||
'command': [sys.executable, '/usr/local/etc/jupyterhub/refresh-token.py']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
c.JupyterHub.admin_users = {"refresh-token"}
|
|
||||||
c.JupyterHub.api_tokens = {
|
|
||||||
os.environ['JUPYTERHUB_API_KEY']: "refresh-token",
|
|
||||||
}
|
|
||||||
extraFiles:
|
|
||||||
refresh-token.py:
|
|
||||||
mountPath: /usr/local/etc/jupyterhub/refresh-token.py
|
|
||||||
stringData: |
|
|
||||||
"""A token refresh service authenticating with the Hub.
|
|
||||||
|
|
||||||
This service serves `/services/refresh-token/`,
|
|
||||||
authenticated with the Hub,
|
|
||||||
showing the user their own info.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import socket
|
|
||||||
from jupyterhub.services.auth import HubAuthenticated
|
|
||||||
from jupyterhub.utils import url_path_join
|
|
||||||
from tornado.httpserver import HTTPServer
|
|
||||||
from tornado.ioloop import IOLoop
|
|
||||||
from tornado.web import Application, HTTPError, RequestHandler, authenticated
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
debug = os.environ.get('NEXTCLOUD_DEBUG_OAUTH', 'false').lower() in ['true', '1', 'yes']
|
|
||||||
def my_debug(s):
|
|
||||||
if debug:
|
|
||||||
with open("/proc/1/fd/1", "a") as stdout:
|
|
||||||
print(s, file=stdout)
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshHandler(HubAuthenticated, RequestHandler):
|
|
||||||
def api_request(self, method, url, **kwargs):
|
|
||||||
my_debug(f'{self.hub_auth}')
|
|
||||||
url = url_path_join(self.hub_auth.api_url, url)
|
|
||||||
allow_404 = kwargs.pop('allow_404', False)
|
|
||||||
headers = kwargs.setdefault('headers', {})
|
|
||||||
headers.setdefault('Authorization', f'token {self.hub_auth.api_token}')
|
|
||||||
try:
|
|
||||||
r = requests.request(method, url, **kwargs)
|
|
||||||
except requests.ConnectionError as e:
|
|
||||||
my_debug(f'Error connecting to {url}: {e}')
|
|
||||||
msg = f'Failed to connect to Hub API at {url}.'
|
|
||||||
msg += f' Is the Hub accessible at this URL (from host: {socket.gethostname()})?'
|
|
||||||
|
|
||||||
if '127.0.0.1' in url:
|
|
||||||
msg += ' Make sure to set c.JupyterHub.hub_ip to an IP accessible to' + \
|
|
||||||
' single-user servers if the servers are not on the same host as the Hub.'
|
|
||||||
raise HTTPError(500, msg)
|
|
||||||
|
|
||||||
data = None
|
|
||||||
if r.status_code == 404 and allow_404:
|
|
||||||
pass
|
|
||||||
elif r.status_code == 403:
|
|
||||||
my_debug(
|
|
||||||
'Lacking permission to check authorization with JupyterHub,' +
|
|
||||||
f' my auth token may have expired: [{r.status_code}] {r.reason}'
|
|
||||||
)
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(
|
|
||||||
500,
|
|
||||||
'Permission failure checking authorization, I may need a new token'
|
|
||||||
)
|
|
||||||
elif r.status_code >= 500:
|
|
||||||
my_debug(f'Upstream failure verifying auth token: [{r.status_code}] {r.reason}')
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(
|
|
||||||
502, 'Failed to check authorization (upstream problem)')
|
|
||||||
elif r.status_code >= 400:
|
|
||||||
my_debug(f'Failed to check authorization: [{r.status_code}] {r.reason}')
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(500, 'Failed to check authorization')
|
|
||||||
else:
|
|
||||||
data = r.json()
|
|
||||||
return data
|
|
||||||
|
|
||||||
@authenticated
|
|
||||||
def get(self):
|
|
||||||
user_model = self.get_current_user()
|
|
||||||
# Fetch current auth state
|
|
||||||
user_data = self.api_request('GET', url_path_join('users', user_model['name']))
|
|
||||||
auth_state = user_data['auth_state']
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
token_expires = auth_state['token_expires']
|
|
||||||
|
|
||||||
self.set_header('content-type', 'application/json')
|
|
||||||
self.write(json.dumps({'access_token': access_token, 'token_expires': token_expires}, indent=1, sort_keys=True))
|
|
||||||
|
|
||||||
class PingHandler(RequestHandler):
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
my_debug(f"DEBUG: In ping get")
|
|
||||||
self.set_header('content-type', 'application/json')
|
|
||||||
self.write(json.dumps({'ping': 1}))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = Application([
|
|
||||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + 'tokens', RefreshHandler),
|
|
||||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', PingHandler),
|
|
||||||
])
|
|
||||||
|
|
||||||
http_server = HTTPServer(app)
|
|
||||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
|
||||||
|
|
||||||
http_server.listen(url.port)
|
|
||||||
|
|
||||||
IOLoop.current().start()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
networkPolicy:
|
|
||||||
ingress:
|
|
||||||
- ports:
|
|
||||||
- port: 8082
|
|
||||||
from:
|
|
||||||
- podSelector:
|
|
||||||
matchLabels:
|
|
||||||
hub.jupyter.org/network-access-hub: "true"
|
|
||||||
service:
|
|
||||||
extraPorts:
|
|
||||||
- port: 8082
|
|
||||||
targetPort: 8082
|
|
||||||
name: refresh-token
|
|
||||||
extraEnv:
|
extraEnv:
|
||||||
NEXTCLOUD_DEBUG_OAUTH: "no"
|
NEXTCLOUD_DEBUG_OAUTH: "no"
|
||||||
NEXTCLOUD_HOST: su.drive.test.sunet.se
|
NEXTCLOUD_HOST: su.drive.test.sunet.se
|
||||||
|
@ -301,35 +27,3 @@ hub:
|
||||||
key: client-secret
|
key: client-secret
|
||||||
networkPolicy:
|
networkPolicy:
|
||||||
enabled: false
|
enabled: false
|
||||||
proxy:
|
|
||||||
chp:
|
|
||||||
networkPolicy:
|
|
||||||
egress:
|
|
||||||
- to:
|
|
||||||
- podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: jupyterhub
|
|
||||||
component: hub
|
|
||||||
ports:
|
|
||||||
- port: 8082
|
|
||||||
singleuser:
|
|
||||||
image:
|
|
||||||
name: docker.sunet.se/drive/jupyter-custom
|
|
||||||
tag: lab-4.0.10-sunet3
|
|
||||||
storage:
|
|
||||||
dynamic:
|
|
||||||
storageClass: csi-sc-cinderplugin
|
|
||||||
extraEnv:
|
|
||||||
JUPYTER_ENABLE_LAB: "yes"
|
|
||||||
extraFiles:
|
|
||||||
jupyter_notebook_config:
|
|
||||||
mountPath: /home/jovyan/.jupyter/jupyter_server_config.py
|
|
||||||
stringData: |
|
|
||||||
import os
|
|
||||||
c = get_config()
|
|
||||||
c.NotebookApp.allow_origin = '*'
|
|
||||||
c.NotebookApp.tornado_settings = {
|
|
||||||
'headers': { 'Content-Security-Policy': "frame-ancestors *;" }
|
|
||||||
}
|
|
||||||
os.system('/usr/local/bin/nc-sync')
|
|
||||||
mode: 0644
|
|
||||||
|
|
|
@ -1,13 +1,8 @@
|
||||||
---
|
|
||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
kind: Kustomization
|
kind: Kustomization
|
||||||
resources: [../../../base/]
|
resources:
|
||||||
helmCharts:
|
- ../../../base/
|
||||||
- includeCRDs: true
|
|
||||||
name: jupyterhub
|
|
||||||
releaseName: jupyterhub
|
|
||||||
valuesFile: ./values/values.yaml
|
|
||||||
version: 3.0.3
|
|
||||||
patches:
|
patches:
|
||||||
- path: jupyterhub-ingress.yml
|
- path: jupyterhub-ingress.yml
|
||||||
- path: jupyterhub-service.yml
|
- path: jupyterhub-service.yml
|
||||||
|
- path: values/values.yaml
|
||||||
|
|
|
@ -1,280 +1,6 @@
|
||||||
debug:
|
debug:
|
||||||
enabled: true
|
enabled: true
|
||||||
hub:
|
hub:
|
||||||
config:
|
|
||||||
Authenticator:
|
|
||||||
auto_login: true
|
|
||||||
enable_auth_state: true
|
|
||||||
JupyterHub:
|
|
||||||
tornado_settings:
|
|
||||||
headers: { 'Content-Security-Policy': "frame-ancestors *;" }
|
|
||||||
db:
|
|
||||||
pvc:
|
|
||||||
storageClassName: csi-sc-cinderplugin
|
|
||||||
extraConfig:
|
|
||||||
oauthCode: |
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
from oauthenticator.generic import GenericOAuthenticator
|
|
||||||
token_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/index.php/apps/oauth2/api/v1/token'
|
|
||||||
debug = os.environ.get('NEXTCLOUD_DEBUG_OAUTH', 'false').lower() in ['true', '1', 'yes']
|
|
||||||
|
|
||||||
def get_nextcloud_access_token(refresh_token):
|
|
||||||
client_id = os.environ['NEXTCLOUD_CLIENT_ID']
|
|
||||||
client_secret = os.environ['NEXTCLOUD_CLIENT_SECRET']
|
|
||||||
|
|
||||||
code = refresh_token
|
|
||||||
data = {
|
|
||||||
'grant_type': 'refresh_token',
|
|
||||||
'code': code,
|
|
||||||
'refresh_token': refresh_token,
|
|
||||||
'client_id': client_id,
|
|
||||||
'client_secret': client_secret
|
|
||||||
}
|
|
||||||
response = requests.post(token_url, data=data)
|
|
||||||
if debug:
|
|
||||||
print(response.text)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def post_auth_hook(authenticator, handler, authentication):
|
|
||||||
user = authentication['auth_state']['oauth_user']['ocs']['data']['id']
|
|
||||||
auth_state = authentication['auth_state']
|
|
||||||
auth_state['token_expires'] = time.time() + auth_state['token_response']['expires_in']
|
|
||||||
authentication['auth_state'] = auth_state
|
|
||||||
return authentication
|
|
||||||
|
|
||||||
class NextcloudOAuthenticator(GenericOAuthenticator):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.user_dict = {}
|
|
||||||
|
|
||||||
async def pre_spawn_start(self, user, spawner):
|
|
||||||
super().pre_spawn_start(user, spawner)
|
|
||||||
auth_state = await user.get_auth_state()
|
|
||||||
if not auth_state:
|
|
||||||
return
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
spawner.environment['NEXTCLOUD_ACCESS_TOKEN'] = access_token
|
|
||||||
|
|
||||||
async def refresh_user(self, user, handler=None):
|
|
||||||
auth_state = await user.get_auth_state()
|
|
||||||
if not auth_state:
|
|
||||||
if debug:
|
|
||||||
print(f'auth_state missing for {user}')
|
|
||||||
return False
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
refresh_token = auth_state['refresh_token']
|
|
||||||
token_response = auth_state['token_response']
|
|
||||||
now = time.time()
|
|
||||||
now_hr = datetime.fromtimestamp(now)
|
|
||||||
expires = auth_state['token_expires']
|
|
||||||
expires_hr = datetime.fromtimestamp(expires)
|
|
||||||
expires = 0
|
|
||||||
if debug:
|
|
||||||
print(f'auth_state for {user}: {auth_state}')
|
|
||||||
if now >= expires:
|
|
||||||
if debug:
|
|
||||||
print(f'Time is: {now_hr}, token expired: {expires_hr}')
|
|
||||||
print(f'Refreshing token for {user}')
|
|
||||||
try:
|
|
||||||
token_response = get_nextcloud_access_token(refresh_token)
|
|
||||||
auth_state['access_token'] = token_response['access_token']
|
|
||||||
auth_state['refresh_token'] = token_response['refresh_token']
|
|
||||||
auth_state['token_expires'] = now + token_response['expires_in']
|
|
||||||
auth_state['token_response'] = token_response
|
|
||||||
if debug:
|
|
||||||
print(f'Successfully refreshed token for {user.name}')
|
|
||||||
print(f'auth_state for {user.name}: {auth_state}')
|
|
||||||
return {'name': user.name, 'auth_state': auth_state}
|
|
||||||
except Exception as e:
|
|
||||||
if debug:
|
|
||||||
print(f'Failed to refresh token for {user}')
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
if debug:
|
|
||||||
print(f'Time is: {now_hr}, token expires: {expires_hr}')
|
|
||||||
return True
|
|
||||||
|
|
||||||
c.JupyterHub.authenticator_class = NextcloudOAuthenticator
|
|
||||||
c.NextcloudOAuthenticator.client_id = os.environ['NEXTCLOUD_CLIENT_ID']
|
|
||||||
c.NextcloudOAuthenticator.client_secret = os.environ['NEXTCLOUD_CLIENT_SECRET']
|
|
||||||
c.NextcloudOAuthenticator.login_service = 'Sunet Drive'
|
|
||||||
c.NextcloudOAuthenticator.username_claim = lambda r: r.get('ocs', {}).get('data', {}).get('id')
|
|
||||||
c.NextcloudOAuthenticator.userdata_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/ocs/v2.php/cloud/user?format=json'
|
|
||||||
c.NextcloudOAuthenticator.authorize_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/index.php/apps/oauth2/authorize'
|
|
||||||
c.NextcloudOAuthenticator.token_url = token_url
|
|
||||||
c.NextcloudOAuthenticator.oauth_callback_url = 'https://' + os.environ['JUPYTER_HOST'] + '/hub/oauth_callback'
|
|
||||||
c.NextcloudOAuthenticator.allow_all = True
|
|
||||||
c.NextcloudOAuthenticator.refresh_pre_spawn = True
|
|
||||||
c.NextcloudOAuthenticator.enable_auth_state = True
|
|
||||||
c.NextcloudOAuthenticator.auth_refresh_age = 3600
|
|
||||||
c.NextcloudOAuthenticator.post_auth_hook = post_auth_hook
|
|
||||||
|
|
||||||
serviceCode: |
|
|
||||||
import sys
|
|
||||||
c.JupyterHub.load_roles = [
|
|
||||||
{
|
|
||||||
"name": "refresh-token",
|
|
||||||
"services": [
|
|
||||||
"refresh-token"
|
|
||||||
],
|
|
||||||
"scopes": [
|
|
||||||
"read:users",
|
|
||||||
"admin:auth_state"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user",
|
|
||||||
"scopes": [
|
|
||||||
"access:services!service=refresh-token",
|
|
||||||
"read:services!service=refresh-token",
|
|
||||||
"self",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "server",
|
|
||||||
"scopes": [
|
|
||||||
"access:services!service=refresh-token",
|
|
||||||
"read:services!service=refresh-token",
|
|
||||||
"inherit",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
c.JupyterHub.services = [
|
|
||||||
{
|
|
||||||
'name': 'refresh-token',
|
|
||||||
'url': 'http://' + os.environ.get('HUB_SERVICE_HOST', 'hub') + ':' + os.environ.get('HUB_SERVICE_PORT_REFRESH_TOKEN', '8082'),
|
|
||||||
'display': False,
|
|
||||||
'oauth_no_confirm': True,
|
|
||||||
'api_token': os.environ['JUPYTERHUB_API_KEY'],
|
|
||||||
'command': [sys.executable, '/usr/local/etc/jupyterhub/refresh-token.py']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
c.JupyterHub.admin_users = {"refresh-token"}
|
|
||||||
c.JupyterHub.api_tokens = {
|
|
||||||
os.environ['JUPYTERHUB_API_KEY']: "refresh-token",
|
|
||||||
}
|
|
||||||
extraFiles:
|
|
||||||
refresh-token.py:
|
|
||||||
mountPath: /usr/local/etc/jupyterhub/refresh-token.py
|
|
||||||
stringData: |
|
|
||||||
"""A token refresh service authenticating with the Hub.
|
|
||||||
|
|
||||||
This service serves `/services/refresh-token/`,
|
|
||||||
authenticated with the Hub,
|
|
||||||
showing the user their own info.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import socket
|
|
||||||
from jupyterhub.services.auth import HubAuthenticated
|
|
||||||
from jupyterhub.utils import url_path_join
|
|
||||||
from tornado.httpserver import HTTPServer
|
|
||||||
from tornado.ioloop import IOLoop
|
|
||||||
from tornado.web import Application, HTTPError, RequestHandler, authenticated
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
debug = os.environ.get('NEXTCLOUD_DEBUG_OAUTH', 'false').lower() in ['true', '1', 'yes']
|
|
||||||
def my_debug(s):
|
|
||||||
if debug:
|
|
||||||
with open("/proc/1/fd/1", "a") as stdout:
|
|
||||||
print(s, file=stdout)
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshHandler(HubAuthenticated, RequestHandler):
|
|
||||||
def api_request(self, method, url, **kwargs):
|
|
||||||
my_debug(f'{self.hub_auth}')
|
|
||||||
url = url_path_join(self.hub_auth.api_url, url)
|
|
||||||
allow_404 = kwargs.pop('allow_404', False)
|
|
||||||
headers = kwargs.setdefault('headers', {})
|
|
||||||
headers.setdefault('Authorization', f'token {self.hub_auth.api_token}')
|
|
||||||
try:
|
|
||||||
r = requests.request(method, url, **kwargs)
|
|
||||||
except requests.ConnectionError as e:
|
|
||||||
my_debug(f'Error connecting to {url}: {e}')
|
|
||||||
msg = f'Failed to connect to Hub API at {url}.'
|
|
||||||
msg += f' Is the Hub accessible at this URL (from host: {socket.gethostname()})?'
|
|
||||||
|
|
||||||
if '127.0.0.1' in url:
|
|
||||||
msg += ' Make sure to set c.JupyterHub.hub_ip to an IP accessible to' + \
|
|
||||||
' single-user servers if the servers are not on the same host as the Hub.'
|
|
||||||
raise HTTPError(500, msg)
|
|
||||||
|
|
||||||
data = None
|
|
||||||
if r.status_code == 404 and allow_404:
|
|
||||||
pass
|
|
||||||
elif r.status_code == 403:
|
|
||||||
my_debug(
|
|
||||||
'Lacking permission to check authorization with JupyterHub,' +
|
|
||||||
f' my auth token may have expired: [{r.status_code}] {r.reason}'
|
|
||||||
)
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(
|
|
||||||
500,
|
|
||||||
'Permission failure checking authorization, I may need a new token'
|
|
||||||
)
|
|
||||||
elif r.status_code >= 500:
|
|
||||||
my_debug(f'Upstream failure verifying auth token: [{r.status_code}] {r.reason}')
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(
|
|
||||||
502, 'Failed to check authorization (upstream problem)')
|
|
||||||
elif r.status_code >= 400:
|
|
||||||
my_debug(f'Failed to check authorization: [{r.status_code}] {r.reason}')
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(500, 'Failed to check authorization')
|
|
||||||
else:
|
|
||||||
data = r.json()
|
|
||||||
return data
|
|
||||||
|
|
||||||
@authenticated
|
|
||||||
def get(self):
|
|
||||||
user_model = self.get_current_user()
|
|
||||||
# Fetch current auth state
|
|
||||||
user_data = self.api_request('GET', url_path_join('users', user_model['name']))
|
|
||||||
auth_state = user_data['auth_state']
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
token_expires = auth_state['token_expires']
|
|
||||||
|
|
||||||
self.set_header('content-type', 'application/json')
|
|
||||||
self.write(json.dumps({'access_token': access_token, 'token_expires': token_expires}, indent=1, sort_keys=True))
|
|
||||||
|
|
||||||
class PingHandler(RequestHandler):
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
my_debug(f"DEBUG: In ping get")
|
|
||||||
self.set_header('content-type', 'application/json')
|
|
||||||
self.write(json.dumps({'ping': 1}))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = Application([
|
|
||||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + 'tokens', RefreshHandler),
|
|
||||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', PingHandler),
|
|
||||||
])
|
|
||||||
|
|
||||||
http_server = HTTPServer(app)
|
|
||||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
|
||||||
|
|
||||||
http_server.listen(url.port)
|
|
||||||
|
|
||||||
IOLoop.current().start()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
networkPolicy:
|
|
||||||
ingress:
|
|
||||||
- ports:
|
|
||||||
- port: 8082
|
|
||||||
from:
|
|
||||||
- podSelector:
|
|
||||||
matchLabels:
|
|
||||||
hub.jupyter.org/network-access-hub: "true"
|
|
||||||
service:
|
|
||||||
extraPorts:
|
|
||||||
- port: 8082
|
|
||||||
targetPort: 8082
|
|
||||||
name: refresh-token
|
|
||||||
extraEnv:
|
extraEnv:
|
||||||
NEXTCLOUD_DEBUG_OAUTH: "no"
|
NEXTCLOUD_DEBUG_OAUTH: "no"
|
||||||
NEXTCLOUD_HOST: sunet.drive.test.sunet.se
|
NEXTCLOUD_HOST: sunet.drive.test.sunet.se
|
||||||
|
@ -301,35 +27,3 @@ hub:
|
||||||
key: client-secret
|
key: client-secret
|
||||||
networkPolicy:
|
networkPolicy:
|
||||||
enabled: false
|
enabled: false
|
||||||
proxy:
|
|
||||||
chp:
|
|
||||||
networkPolicy:
|
|
||||||
egress:
|
|
||||||
- to:
|
|
||||||
- podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: jupyterhub
|
|
||||||
component: hub
|
|
||||||
ports:
|
|
||||||
- port: 8082
|
|
||||||
singleuser:
|
|
||||||
image:
|
|
||||||
name: docker.sunet.se/drive/jupyter-custom
|
|
||||||
tag: lab-4.0.10-sunet3
|
|
||||||
storage:
|
|
||||||
dynamic:
|
|
||||||
storageClass: csi-sc-cinderplugin
|
|
||||||
extraEnv:
|
|
||||||
JUPYTER_ENABLE_LAB: "yes"
|
|
||||||
extraFiles:
|
|
||||||
jupyter_notebook_config:
|
|
||||||
mountPath: /home/jovyan/.jupyter/jupyter_server_config.py
|
|
||||||
stringData: |
|
|
||||||
import os
|
|
||||||
c = get_config()
|
|
||||||
c.NotebookApp.allow_origin = '*'
|
|
||||||
c.NotebookApp.tornado_settings = {
|
|
||||||
'headers': { 'Content-Security-Policy': "frame-ancestors *;" }
|
|
||||||
}
|
|
||||||
os.system('/usr/local/bin/nc-sync')
|
|
||||||
mode: 0644
|
|
||||||
|
|
|
@ -1,280 +1,6 @@
|
||||||
debug:
|
debug:
|
||||||
enabled: true
|
enabled: true
|
||||||
hub:
|
hub:
|
||||||
config:
|
|
||||||
Authenticator:
|
|
||||||
auto_login: true
|
|
||||||
enable_auth_state: true
|
|
||||||
JupyterHub:
|
|
||||||
tornado_settings:
|
|
||||||
headers: { 'Content-Security-Policy': "frame-ancestors *;" }
|
|
||||||
db:
|
|
||||||
pvc:
|
|
||||||
storageClassName: csi-sc-cinderplugin
|
|
||||||
extraConfig:
|
|
||||||
oauthCode: |
|
|
||||||
import time
|
|
||||||
import requests
|
|
||||||
from datetime import datetime
|
|
||||||
from oauthenticator.generic import GenericOAuthenticator
|
|
||||||
token_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/index.php/apps/oauth2/api/v1/token'
|
|
||||||
debug = os.environ.get('NEXTCLOUD_DEBUG_OAUTH', 'false').lower() in ['true', '1', 'yes']
|
|
||||||
|
|
||||||
def get_nextcloud_access_token(refresh_token):
|
|
||||||
client_id = os.environ['NEXTCLOUD_CLIENT_ID']
|
|
||||||
client_secret = os.environ['NEXTCLOUD_CLIENT_SECRET']
|
|
||||||
|
|
||||||
code = refresh_token
|
|
||||||
data = {
|
|
||||||
'grant_type': 'refresh_token',
|
|
||||||
'code': code,
|
|
||||||
'refresh_token': refresh_token,
|
|
||||||
'client_id': client_id,
|
|
||||||
'client_secret': client_secret
|
|
||||||
}
|
|
||||||
response = requests.post(token_url, data=data)
|
|
||||||
if debug:
|
|
||||||
print(response.text)
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
def post_auth_hook(authenticator, handler, authentication):
|
|
||||||
user = authentication['auth_state']['oauth_user']['ocs']['data']['id']
|
|
||||||
auth_state = authentication['auth_state']
|
|
||||||
auth_state['token_expires'] = time.time() + auth_state['token_response']['expires_in']
|
|
||||||
authentication['auth_state'] = auth_state
|
|
||||||
return authentication
|
|
||||||
|
|
||||||
class NextcloudOAuthenticator(GenericOAuthenticator):
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.user_dict = {}
|
|
||||||
|
|
||||||
async def pre_spawn_start(self, user, spawner):
|
|
||||||
super().pre_spawn_start(user, spawner)
|
|
||||||
auth_state = await user.get_auth_state()
|
|
||||||
if not auth_state:
|
|
||||||
return
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
spawner.environment['NEXTCLOUD_ACCESS_TOKEN'] = access_token
|
|
||||||
|
|
||||||
async def refresh_user(self, user, handler=None):
|
|
||||||
auth_state = await user.get_auth_state()
|
|
||||||
if not auth_state:
|
|
||||||
if debug:
|
|
||||||
print(f'auth_state missing for {user}')
|
|
||||||
return False
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
refresh_token = auth_state['refresh_token']
|
|
||||||
token_response = auth_state['token_response']
|
|
||||||
now = time.time()
|
|
||||||
now_hr = datetime.fromtimestamp(now)
|
|
||||||
expires = auth_state['token_expires']
|
|
||||||
expires_hr = datetime.fromtimestamp(expires)
|
|
||||||
expires = 0
|
|
||||||
if debug:
|
|
||||||
print(f'auth_state for {user}: {auth_state}')
|
|
||||||
if now >= expires:
|
|
||||||
if debug:
|
|
||||||
print(f'Time is: {now_hr}, token expired: {expires_hr}')
|
|
||||||
print(f'Refreshing token for {user}')
|
|
||||||
try:
|
|
||||||
token_response = get_nextcloud_access_token(refresh_token)
|
|
||||||
auth_state['access_token'] = token_response['access_token']
|
|
||||||
auth_state['refresh_token'] = token_response['refresh_token']
|
|
||||||
auth_state['token_expires'] = now + token_response['expires_in']
|
|
||||||
auth_state['token_response'] = token_response
|
|
||||||
if debug:
|
|
||||||
print(f'Successfully refreshed token for {user.name}')
|
|
||||||
print(f'auth_state for {user.name}: {auth_state}')
|
|
||||||
return {'name': user.name, 'auth_state': auth_state}
|
|
||||||
except Exception as e:
|
|
||||||
if debug:
|
|
||||||
print(f'Failed to refresh token for {user}')
|
|
||||||
return False
|
|
||||||
return False
|
|
||||||
if debug:
|
|
||||||
print(f'Time is: {now_hr}, token expires: {expires_hr}')
|
|
||||||
return True
|
|
||||||
|
|
||||||
c.JupyterHub.authenticator_class = NextcloudOAuthenticator
|
|
||||||
c.NextcloudOAuthenticator.client_id = os.environ['NEXTCLOUD_CLIENT_ID']
|
|
||||||
c.NextcloudOAuthenticator.client_secret = os.environ['NEXTCLOUD_CLIENT_SECRET']
|
|
||||||
c.NextcloudOAuthenticator.login_service = 'Sunet Drive'
|
|
||||||
c.NextcloudOAuthenticator.username_claim = lambda r: r.get('ocs', {}).get('data', {}).get('id')
|
|
||||||
c.NextcloudOAuthenticator.userdata_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/ocs/v2.php/cloud/user?format=json'
|
|
||||||
c.NextcloudOAuthenticator.authorize_url = 'https://' + os.environ['NEXTCLOUD_HOST'] + '/index.php/apps/oauth2/authorize'
|
|
||||||
c.NextcloudOAuthenticator.token_url = token_url
|
|
||||||
c.NextcloudOAuthenticator.oauth_callback_url = 'https://' + os.environ['JUPYTER_HOST'] + '/hub/oauth_callback'
|
|
||||||
c.NextcloudOAuthenticator.allow_all = True
|
|
||||||
c.NextcloudOAuthenticator.refresh_pre_spawn = True
|
|
||||||
c.NextcloudOAuthenticator.enable_auth_state = True
|
|
||||||
c.NextcloudOAuthenticator.auth_refresh_age = 3600
|
|
||||||
c.NextcloudOAuthenticator.post_auth_hook = post_auth_hook
|
|
||||||
|
|
||||||
serviceCode: |
|
|
||||||
import sys
|
|
||||||
c.JupyterHub.load_roles = [
|
|
||||||
{
|
|
||||||
"name": "refresh-token",
|
|
||||||
"services": [
|
|
||||||
"refresh-token"
|
|
||||||
],
|
|
||||||
"scopes": [
|
|
||||||
"read:users",
|
|
||||||
"admin:auth_state"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "user",
|
|
||||||
"scopes": [
|
|
||||||
"access:services!service=refresh-token",
|
|
||||||
"read:services!service=refresh-token",
|
|
||||||
"self",
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "server",
|
|
||||||
"scopes": [
|
|
||||||
"access:services!service=refresh-token",
|
|
||||||
"read:services!service=refresh-token",
|
|
||||||
"inherit",
|
|
||||||
],
|
|
||||||
}
|
|
||||||
]
|
|
||||||
c.JupyterHub.services = [
|
|
||||||
{
|
|
||||||
'name': 'refresh-token',
|
|
||||||
'url': 'http://' + os.environ.get('HUB_SERVICE_HOST', 'hub') + ':' + os.environ.get('HUB_SERVICE_PORT_REFRESH_TOKEN', '8082'),
|
|
||||||
'display': False,
|
|
||||||
'oauth_no_confirm': True,
|
|
||||||
'api_token': os.environ['JUPYTERHUB_API_KEY'],
|
|
||||||
'command': [sys.executable, '/usr/local/etc/jupyterhub/refresh-token.py']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
c.JupyterHub.admin_users = {"refresh-token"}
|
|
||||||
c.JupyterHub.api_tokens = {
|
|
||||||
os.environ['JUPYTERHUB_API_KEY']: "refresh-token",
|
|
||||||
}
|
|
||||||
extraFiles:
|
|
||||||
refresh-token.py:
|
|
||||||
mountPath: /usr/local/etc/jupyterhub/refresh-token.py
|
|
||||||
stringData: |
|
|
||||||
"""A token refresh service authenticating with the Hub.
|
|
||||||
|
|
||||||
This service serves `/services/refresh-token/`,
|
|
||||||
authenticated with the Hub,
|
|
||||||
showing the user their own info.
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import requests
|
|
||||||
import socket
|
|
||||||
from jupyterhub.services.auth import HubAuthenticated
|
|
||||||
from jupyterhub.utils import url_path_join
|
|
||||||
from tornado.httpserver import HTTPServer
|
|
||||||
from tornado.ioloop import IOLoop
|
|
||||||
from tornado.web import Application, HTTPError, RequestHandler, authenticated
|
|
||||||
from urllib.parse import urlparse
|
|
||||||
debug = os.environ.get('NEXTCLOUD_DEBUG_OAUTH', 'false').lower() in ['true', '1', 'yes']
|
|
||||||
def my_debug(s):
|
|
||||||
if debug:
|
|
||||||
with open("/proc/1/fd/1", "a") as stdout:
|
|
||||||
print(s, file=stdout)
|
|
||||||
|
|
||||||
|
|
||||||
class RefreshHandler(HubAuthenticated, RequestHandler):
|
|
||||||
def api_request(self, method, url, **kwargs):
|
|
||||||
my_debug(f'{self.hub_auth}')
|
|
||||||
url = url_path_join(self.hub_auth.api_url, url)
|
|
||||||
allow_404 = kwargs.pop('allow_404', False)
|
|
||||||
headers = kwargs.setdefault('headers', {})
|
|
||||||
headers.setdefault('Authorization', f'token {self.hub_auth.api_token}')
|
|
||||||
try:
|
|
||||||
r = requests.request(method, url, **kwargs)
|
|
||||||
except requests.ConnectionError as e:
|
|
||||||
my_debug(f'Error connecting to {url}: {e}')
|
|
||||||
msg = f'Failed to connect to Hub API at {url}.'
|
|
||||||
msg += f' Is the Hub accessible at this URL (from host: {socket.gethostname()})?'
|
|
||||||
|
|
||||||
if '127.0.0.1' in url:
|
|
||||||
msg += ' Make sure to set c.JupyterHub.hub_ip to an IP accessible to' + \
|
|
||||||
' single-user servers if the servers are not on the same host as the Hub.'
|
|
||||||
raise HTTPError(500, msg)
|
|
||||||
|
|
||||||
data = None
|
|
||||||
if r.status_code == 404 and allow_404:
|
|
||||||
pass
|
|
||||||
elif r.status_code == 403:
|
|
||||||
my_debug(
|
|
||||||
'Lacking permission to check authorization with JupyterHub,' +
|
|
||||||
f' my auth token may have expired: [{r.status_code}] {r.reason}'
|
|
||||||
)
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(
|
|
||||||
500,
|
|
||||||
'Permission failure checking authorization, I may need a new token'
|
|
||||||
)
|
|
||||||
elif r.status_code >= 500:
|
|
||||||
my_debug(f'Upstream failure verifying auth token: [{r.status_code}] {r.reason}')
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(
|
|
||||||
502, 'Failed to check authorization (upstream problem)')
|
|
||||||
elif r.status_code >= 400:
|
|
||||||
my_debug(f'Failed to check authorization: [{r.status_code}] {r.reason}')
|
|
||||||
my_debug(r.text)
|
|
||||||
raise HTTPError(500, 'Failed to check authorization')
|
|
||||||
else:
|
|
||||||
data = r.json()
|
|
||||||
return data
|
|
||||||
|
|
||||||
@authenticated
|
|
||||||
def get(self):
|
|
||||||
user_model = self.get_current_user()
|
|
||||||
# Fetch current auth state
|
|
||||||
user_data = self.api_request('GET', url_path_join('users', user_model['name']))
|
|
||||||
auth_state = user_data['auth_state']
|
|
||||||
access_token = auth_state['access_token']
|
|
||||||
token_expires = auth_state['token_expires']
|
|
||||||
|
|
||||||
self.set_header('content-type', 'application/json')
|
|
||||||
self.write(json.dumps({'access_token': access_token, 'token_expires': token_expires}, indent=1, sort_keys=True))
|
|
||||||
|
|
||||||
class PingHandler(RequestHandler):
|
|
||||||
|
|
||||||
def get(self):
|
|
||||||
my_debug(f"DEBUG: In ping get")
|
|
||||||
self.set_header('content-type', 'application/json')
|
|
||||||
self.write(json.dumps({'ping': 1}))
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
app = Application([
|
|
||||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + 'tokens', RefreshHandler),
|
|
||||||
(os.environ['JUPYTERHUB_SERVICE_PREFIX'] + '/?', PingHandler),
|
|
||||||
])
|
|
||||||
|
|
||||||
http_server = HTTPServer(app)
|
|
||||||
url = urlparse(os.environ['JUPYTERHUB_SERVICE_URL'])
|
|
||||||
|
|
||||||
http_server.listen(url.port)
|
|
||||||
|
|
||||||
IOLoop.current().start()
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
networkPolicy:
|
|
||||||
ingress:
|
|
||||||
- ports:
|
|
||||||
- port: 8082
|
|
||||||
from:
|
|
||||||
- podSelector:
|
|
||||||
matchLabels:
|
|
||||||
hub.jupyter.org/network-access-hub: "true"
|
|
||||||
service:
|
|
||||||
extraPorts:
|
|
||||||
- port: 8082
|
|
||||||
targetPort: 8082
|
|
||||||
name: refresh-token
|
|
||||||
extraEnv:
|
extraEnv:
|
||||||
NEXTCLOUD_DEBUG_OAUTH: "no"
|
NEXTCLOUD_DEBUG_OAUTH: "no"
|
||||||
NEXTCLOUD_HOST: uu.drive.test.sunet.se
|
NEXTCLOUD_HOST: uu.drive.test.sunet.se
|
||||||
|
@ -301,35 +27,3 @@ hub:
|
||||||
key: client-secret
|
key: client-secret
|
||||||
networkPolicy:
|
networkPolicy:
|
||||||
enabled: false
|
enabled: false
|
||||||
proxy:
|
|
||||||
chp:
|
|
||||||
networkPolicy:
|
|
||||||
egress:
|
|
||||||
- to:
|
|
||||||
- podSelector:
|
|
||||||
matchLabels:
|
|
||||||
app: jupyterhub
|
|
||||||
component: hub
|
|
||||||
ports:
|
|
||||||
- port: 8082
|
|
||||||
singleuser:
|
|
||||||
image:
|
|
||||||
name: docker.sunet.se/drive/jupyter-custom
|
|
||||||
tag: lab-4.0.10-sunet3
|
|
||||||
storage:
|
|
||||||
dynamic:
|
|
||||||
storageClass: csi-sc-cinderplugin
|
|
||||||
extraEnv:
|
|
||||||
JUPYTER_ENABLE_LAB: "yes"
|
|
||||||
extraFiles:
|
|
||||||
jupyter_notebook_config:
|
|
||||||
mountPath: /home/jovyan/.jupyter/jupyter_server_config.py
|
|
||||||
stringData: |
|
|
||||||
import os
|
|
||||||
c = get_config()
|
|
||||||
c.NotebookApp.allow_origin = '*'
|
|
||||||
c.NotebookApp.tornado_settings = {
|
|
||||||
'headers': { 'Content-Security-Policy': "frame-ancestors *;" }
|
|
||||||
}
|
|
||||||
os.system('/usr/local/bin/nc-sync')
|
|
||||||
mode: 0644
|
|
||||||
|
|
Loading…
Reference in a new issue