Skip to content

Commit 2bd24f6

Browse files
rasmunkjonasbardino
authored andcommitted
Merge the changes introduces in PR #254
git-svn-id: svn+ssh://svn.code.sf.net/p/migrid/code/trunk@6244 b75ad72c-e7d7-11dd-a971-7dbc132099af
1 parent b8df8fe commit 2bd24f6

File tree

6 files changed

+137
-52
lines changed

6 files changed

+137
-52
lines changed

README

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,8 @@ settings:
413413
--ftps_pasv_ports=FTPS_PASV_PORTS
414414
--davs_address=DAVS_ADDRESS
415415
--jupyter_services=JUPYTER_SERVICES
416+
--jupyter_services_enable_proxy_https=JUPYTER_SERVICES_ENABLE_PROXY_HTTPS
417+
--jupyter_services_proxy_config=JUPYTER_SERVICES_PROXY_CONFIG
416418
--jupyter_services_desc=JUPYTER_SERVICES_DESC
417419
--cloud_fqdn=CLOUD_FQDN
418420
--cloud_services=CLOUD_SERVICES
@@ -798,6 +800,7 @@ integration for data analysis:
798800
--enable_vhost_certs=True --enable_verify_certs=True \
799801
--enable_notify=True --enable_jupyter=True \
800802
--jupyter_services='DAG.https://dag002.science DAG.https://dag003.science DAG.https://dag004.science DAG.https://dag005.science DAG.https://dag006.science DAG.https://dag007.science DAG.https://dag008.science DAG.https://dag009.science DAG.https://dag010.science DAG.https://dag203.science DAG.https://dag204.science MODI.https://dag100.science' \
803+
--jupyter_services_proxy_config="{'DAG': {'SSLProxyCACertificateFile': '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem'}, 'MODI': {'SSLProxyCACertificateFile': '/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem'}}"
801804
--jupyter_services_desc="{'DAG': '/home/mig/state/wwwpublic/dag_desc.html', 'MODI': '/home/mig/state/wwwpublic/modi_desc.html'}" \
802805
--enable_cloud=True --enable_migadmin=True \
803806
--enable_peers=True --peers_mandatory=True \

mig/install/generateconfs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ def main(argv, _generate_confs=generate_confs, _print=print):
9999
'ftps_pasv_ports',
100100
'davs_address',
101101
'jupyter_services',
102+
'jupyter_services_enable_proxy_https',
103+
'jupyter_services_proxy_config',
102104
'jupyter_services_desc',
103105
'cloud_fqdn',
104106
'cloud_services',

mig/shared/functionality/reqjupyterservice.py

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,11 @@
5454
import random
5555
try:
5656
import requests
57-
except ImportError as err:
57+
except ImportError:
5858
requests = None
5959

6060
from mig.shared import returnvalues
61-
from mig.shared.base import client_id_dir, extract_field
61+
from mig.shared.base import client_id_dir, extract_field, force_native_str
6262
from mig.shared.conf import get_configuration_object
6363
from mig.shared.defaults import session_id_bytes
6464
from mig.shared.fileio import make_symlink, pickle, unpickle, write_file, \
@@ -68,6 +68,7 @@
6868
from mig.shared.init import initialize_main_variables
6969
from mig.shared.pwcrypto import generate_random_ascii
7070
from mig.shared.ssh import generate_ssh_rsa_key_pair, tighten_key_perms
71+
from mig.shared.url import urljoin
7172
from mig.shared.workflows import create_workflow_session_id, \
7273
get_workflow_session_id
7374

@@ -281,6 +282,23 @@ def jupyter_host(configuration, output_objects, user, url):
281282
return (output_objects, returnvalues.OK)
282283

283284

285+
def jupyterhub_session_post_request(session, url, params=None, **kwargs):
286+
"""
287+
Sends a post request to a URL
288+
:param session: the session object that can be used to conduct the post request
289+
:param url: the designated URL that the post request is sent to
290+
:param params: parameters to pass to the post request
291+
:return: the response object from the post request
292+
"""
293+
if not params:
294+
params = {}
295+
296+
if "_xsrf" in session.cookies:
297+
params["_xsrf"] = session.cookies['_xsrf']
298+
299+
return session.post(url, params=params, **kwargs)
300+
301+
284302
def reset(configuration):
285303
"""Helper function to clean up all jupyter directories and mounts
286304
:param configuration: the MiG Configuration object
@@ -445,10 +463,13 @@ def main(client_id, user_arguments_dict):
445463
# Make sure ssh daemon does not complain
446464
tighten_key_perms(configuration, client_id)
447465

448-
url_base = '/' + service['service_name']
449-
url_home = url_base + '/home'
450-
url_auth = host + url_base + '/hub/login'
451-
url_data = host + url_base + '/hub/set-user-data'
466+
url_service = urljoin('/', service['service_name'])
467+
url_home = urljoin(url_service + "/", 'home')
468+
469+
url_base = urljoin(host, service['service_name'])
470+
url_hub = urljoin(url_base + "/", 'hub')
471+
url_auth = urljoin(url_hub + "/", 'login')
472+
url_data = urljoin(url_hub + "/", 'set-user-data')
452473

453474
# Does the client home dir contain an active mount key
454475
# If so just keep on using it.
@@ -520,15 +541,12 @@ def main(client_id, user_arguments_dict):
520541

521542
with requests.session() as session:
522543
# Refresh cookies
523-
session.get(url_auth)
524-
auth_params = {}
525-
if "_xsrf" in session.cookies:
526-
auth_params = {"_xsrf": session.cookies['_xsrf']}
544+
session.get(url_hub)
527545
# Authenticate and submit data
528-
response = session.post(url_auth, headers=auth_header, params=auth_params)
546+
response = jupyterhub_session_post_request(session, url_auth, headers=auth_header)
529547
if response.status_code == 200:
530548
for user_data_type, user_data in user_post_data.items():
531-
response = session.post(url_data, json={user_data_type: user_data}, params=auth_params)
549+
response = jupyterhub_session_post_request(session, url_data, json={user_data_type: user_data})
532550
if response.status_code != 200:
533551
logger.error(
534552
"Jupyter: User %s failed to submit data %s to %s"
@@ -549,28 +567,36 @@ def main(client_id, user_arguments_dict):
549567

550568
# Generate private/public keys
551569
(mount_private_key, mount_public_key) = generate_ssh_rsa_key_pair(
552-
encode_utf8=True)
570+
encode_utf8=True
571+
)
572+
573+
logger.debug("User: %s - Creating a new jupyter mount keyset - "
574+
"private_key: %s public_key: %s "
575+
% (client_id, mount_private_key, mount_public_key))
553576

554577
# Known hosts
555578
sftp_addresses = socket.gethostbyname_ex(
556579
configuration.user_sftp_show_address or socket.getfqdn())
557580

581+
# Write the authorization file
582+
auth_content = []
583+
str_mount_public_key = force_native_str(mount_public_key)
558584
# Subsys sftp support
559585
if configuration.site_enable_sftp_subsys:
560586
# Restrict possible mount agent
561-
auth_content = []
562587
restrict_opts = 'no-agent-forwarding,no-port-forwarding,no-pty,'
563588
restrict_opts += 'no-user-rc,no-X11-forwarding'
564589
restrictions = '%s' % restrict_opts
565-
auth_content.append('%s %s\n' % (restrictions, mount_public_key))
566-
# Write auth file
567-
write_file('\n'.join(auth_content),
568-
os.path.join(subsys_path, session_id
569-
+ '.authorized_keys'), logger, umask=0o27)
570-
571-
logger.debug("User: %s - Creating a new jupyter mount keyset - "
572-
"private_key: %s public_key: %s "
573-
% (client_id, mount_private_key, mount_public_key))
590+
auth_content.append('%s %s\n' % (restrictions, str_mount_public_key))
591+
else:
592+
auth_content.append('%s\n' % str_mount_public_key)
593+
594+
# Write auth file
595+
write_file(
596+
'\n'.join(auth_content),
597+
os.path.join(subsys_path, session_id + '.authorized_keys'),
598+
logger, umask=0o27
599+
)
574600

575601
jupyter_dict = {
576602
'MOUNT_HOST': configuration.short_title,
@@ -626,15 +652,12 @@ def main(client_id, user_arguments_dict):
626652
# First login
627653
with requests.session() as session:
628654
# Refresh cookies
629-
session.get(url_auth)
630-
auth_params = {}
631-
if "_xsrf" in session.cookies:
632-
auth_params = {"_xsrf": session.cookies['_xsrf']}
655+
session.get(url_hub)
633656
# Authenticate
634-
response = session.post(url_auth, headers=auth_header, params=auth_params)
657+
response = jupyterhub_session_post_request(session, url_auth, headers=auth_header)
635658
if response.status_code == 200:
636659
for user_data_type, user_data in user_post_data.items():
637-
response = session.post(url_data, json={user_data_type: user_data}, params=auth_params)
660+
response = jupyterhub_session_post_request(session, url_data, json={user_data_type: user_data})
638661
if response.status_code != 200:
639662
logger.error("Jupyter: User %s failed to submit data %s to %s"
640663
% (client_id, user_data, url_data))

mig/shared/install.py

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,22 @@ def abspath(path, start):
8484
return path
8585
return os.path.normpath(os.path.join(start, path))
8686

87+
def transform_str_to_dict(input_str):
88+
"""
89+
Transforms a string input into a Python literal or container.
90+
The function will only return the transformed object if it becomes a dictionary.
91+
input_str: The input string that is expected to be
92+
structured as a dictionary. A valid input_str for example could be '{'hello': 'world'}'.
93+
"""
94+
try:
95+
output_dict = ast.literal_eval(input_str)
96+
except SyntaxError as err:
97+
return False
98+
99+
if not isinstance(output_dict, dict):
100+
return False
101+
return output_dict
102+
87103

88104
def determine_timezone(_environ=os.environ, _path_exists=os.path.exists, _print=print):
89105
"""Attempt to detect the timezone in various known portable ways."""
@@ -318,6 +334,8 @@ def generate_confs(
318334
ftps_address='',
319335
davs_address='',
320336
jupyter_services='',
337+
jupyter_services_enable_proxy_https=True,
338+
jupyter_services_proxy_config='{}',
321339
jupyter_services_desc='{}',
322340
cloud_services='',
323341
cloud_services_desc='{}',
@@ -640,6 +658,8 @@ def _generate_confs_prepare(
640658
ftps_address,
641659
davs_address,
642660
jupyter_services,
661+
jupyter_services_enable_proxy_https,
662+
jupyter_services_proxy_config,
643663
jupyter_services_desc,
644664
cloud_services,
645665
cloud_services_desc,
@@ -1496,17 +1516,18 @@ def _generate_confs_prepare(
14961516
jupyter_openids, jupyter_oidcs, jupyter_rewrites = [], [], []
14971517
services = user_dict['__JUPYTER_SERVICES__'].split()
14981518

1499-
try:
1500-
descs = ast.literal_eval(jupyter_services_desc)
1501-
except SyntaxError as err:
1502-
print('Error: jupyter_services_desc '
1503-
'could not be intepreted correctly. Double check that your '
1519+
jupyter_services_proxy_configs = transform_str_to_dict(jupyter_services_proxy_config)
1520+
if not isinstance(jupyter_services_proxy_configs, dict):
1521+
print('Error: jupyter_services_proxy_config '
1522+
'could not be interpreted correctly. Double check that your '
15041523
'formatting is correct, a dictionary formatted string is expected.')
15051524
sys.exit(1)
15061525

1507-
if not isinstance(descs, dict):
1508-
print('Error: %s was incorrectly formatted,'
1509-
' expects a string formatted as a dictionary' % descs)
1526+
jupyter_services_descs = transform_str_to_dict(jupyter_services_desc)
1527+
if not isinstance(jupyter_services_descs, dict):
1528+
print('Error: jupyter_services_desc '
1529+
'could not be interpreted correctly. Double check that your '
1530+
'formatting is correct, a dictionary formatted string is expected.')
15101531
sys.exit(1)
15111532

15121533
service_hosts = {}
@@ -1560,8 +1581,8 @@ def _generate_confs_prepare(
15601581
'__JUPYTER_%s_HOSTS__' % u_name: ' '.join(values['hosts'])
15611582
}
15621583

1563-
if name in descs:
1564-
desc_value = descs[name] + "\n"
1584+
if name in jupyter_services_descs:
1585+
desc_value = jupyter_services_descs[name] + "\n"
15651586
else:
15661587
desc_value = "\n"
15671588

@@ -1599,8 +1620,7 @@ def _generate_confs_prepare(
15991620
ws_host = host.replace(
16001621
"https://", "wss://").replace("http://", "ws://")
16011622
member_def = "Define JUPYTER_%s %s" % (name_index, host)
1602-
ws_member_def = "Define WS_JUPYTER_%s %s" % (name_index,
1603-
ws_host)
1623+
ws_member_def = "Define WS_JUPYTER_%s %s" % (name_index, ws_host)
16041624

16051625
# No user supplied port, assign based on url prefix
16061626
if len(host.split(":")) < 3:
@@ -1617,9 +1637,14 @@ def _generate_confs_prepare(
16171637
ws_member_def += ":%s\n" % port
16181638

16191639
jupyter_defs.extend([member_def, ws_member_def])
1640+
1641+
service_proxy_config_kwargs = jupyter_services_proxy_configs.get(name, {})
16201642
# Get proxy template and append to template conf
1621-
proxy_template = gen_balancer_proxy_template(url, def_name, name,
1622-
hosts, ws_hosts)
1643+
proxy_template = gen_balancer_proxy_template(
1644+
url, def_name, name, hosts, ws_hosts,
1645+
enable_proxy_https=jupyter_services_enable_proxy_https,
1646+
proxy_balancer_template_kwargs=service_proxy_config_kwargs
1647+
)
16231648
jupyter_proxies.append(proxy_template)
16241649

16251650
user_dict['__JUPYTER_DEFS__'] = '\n'.join(jupyter_defs)

mig/shared/jupyter.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434

3535
from past.builtins import basestring
3636
def gen_balancer_proxy_template(url, define, name, member_hosts,
37-
ws_member_hosts, timeout=600):
37+
ws_member_hosts,
38+
timeout=600,
39+
enable_proxy_https=True,
40+
proxy_balancer_template_kwargs=None):
3841
""" Generates an apache proxy balancer configuration section template
3942
for a particular jupyter service. Relies on the
4043
https://httpd.apache.org/docs/2.4/mod/mod_proxy_balancer.html module to
@@ -46,14 +49,26 @@ def gen_balancer_proxy_template(url, define, name, member_hosts,
4649
in balancer member defitions.
4750
ws_member_hosts: The list of unique identifiers that should be used to fill
4851
in websocket balancer member defitions.
52+
timeout: The proxy timeout in seconds.
53+
enable_proxy_https: Whether or not to enable SSL/TLS proxying.
54+
proxy_balancer_template_kwargs: The optional extra apache config options that are used to
55+
generate the proxy balancer templates. This for instance can be used to pass SSL options
56+
such as a custom self-signed CA that should be used to establish a
57+
trusted SSL/TLS connection to the designated jupyter service.
58+
An example of this could be {'SSLProxyCACertificateFile': 'path/to/local/ca-certificate.pem'}.
4959
"""
5060

61+
if not proxy_balancer_template_kwargs:
62+
proxy_balancer_template_kwargs = {}
63+
5164
assert isinstance(url, basestring)
5265
assert isinstance(define, basestring)
5366
assert isinstance(name, basestring)
5467
assert isinstance(member_hosts, list)
5568
assert isinstance(ws_member_hosts, list)
5669
assert isinstance(timeout, int)
70+
assert isinstance(enable_proxy_https, bool)
71+
assert isinstance(proxy_balancer_template_kwargs, dict)
5772

5873
fill_helpers = {
5974
'url': url,
@@ -66,44 +81,61 @@ def gen_balancer_proxy_template(url, define, name, member_hosts,
6681
'ws_hosts': '',
6782
'timeout': timeout,
6883
'referer_fqdn': name.upper() + "_PROXY_FQDN=$1",
69-
'referer_url': '%{' + name.upper() + "_PROXY_PROTOCOL}e://%{" + name.upper() + "_PROXY_FQDN}e/" + name + "/hub"
84+
'referer_url': '%{' + name.upper() + "_PROXY_PROTOCOL}e://%{" + name.upper() + "_PROXY_FQDN}e/" + name + "/hub",
85+
'proxy_balancer_template': ''
7086
}
7187

7288
for host in member_hosts:
7389
fill_helpers['hosts'] += ''.join([' ', host])
7490

7591
for ws_host in ws_member_hosts:
7692
fill_helpers['ws_hosts'] += ''.join([' ', ws_host])
77-
print("filling in jupyter gen_balancer_proxy_template with helper: (%s)" %
78-
fill_helpers)
93+
94+
proxy_balancer_options, proxy_balancer_template = {}, ''
95+
if enable_proxy_https:
96+
if "SSLProxyVerify" not in proxy_balancer_template_kwargs:
97+
proxy_balancer_options["SSLProxyVerify"] = "require"
98+
if "SSLProxyCheckPeerCN" not in proxy_balancer_template_kwargs:
99+
proxy_balancer_options["SSLProxyCheckPeerCN"] = "on"
100+
if "SSLProxyCheckPeerName" not in proxy_balancer_template_kwargs:
101+
proxy_balancer_options["SSLProxyCheckPeerName"] = "on"
102+
103+
for key, value in proxy_balancer_template_kwargs.items():
104+
proxy_balancer_options[key] = value
105+
106+
for key, value in proxy_balancer_options.items():
107+
proxy_balancer_template += ''.join([' ', '%s %s\n' % (key, value)])
108+
109+
fill_helpers["proxy_balancer_template"] = proxy_balancer_template
79110

80111
template = """
81112
<IfDefine %(define)s>
82113
Header add Set-Cookie "%(route_cookie)s=%(balancer_worker_env)s; path=%(url)s" env=BALANCER_ROUTE_CHANGED
83-
SetEnvIf Host (.*) %(referer_fqdn)s
84114
85115
ProxyTimeout %(timeout)s
86116
<Proxy balancer://%(name)s_hosts>
117+
%(proxy_balancer_template)s
87118
%(hosts)s
88119
ProxySet stickysession=%(route_cookie)s
89120
</Proxy>
90121
# Websocket cluster
91122
<Proxy balancer://ws_%(name)s_hosts>
123+
%(proxy_balancer_template)s
92124
%(ws_hosts)s
93125
ProxySet stickysession=%(route_cookie)s
94126
</Proxy>
95127
<Location %(url)s>
96-
ProxyPreserveHost on
97128
ProxyPass balancer://%(name)s_hosts%(url)s
98129
ProxyPassReverse balancer://%(name)s_hosts%(url)s
99130
RequestHeader set Remote-User %(remote_user_env)s
100-
RequestHeader set Referer %(referer_url)s
131+
RequestHeader set "X-Forwarded-Proto" expr=%%{REQUEST_SCHEME}
101132
</Location>
102133
<LocationMatch "%(url)s/(user/[^/]+)/(api/kernels/[^/]+/channels|terminals/websocket|api/events/subscribe)(/?|)">
103134
ProxyPass balancer://ws_%(name)s_hosts
104135
ProxyPassReverse balancer://ws_%(name)s_hosts
105136
</LocationMatch>
106137
</IfDefine>""" % fill_helpers
138+
107139
return template
108140

109141

0 commit comments

Comments
 (0)