Skip to content
Snippets Groups Projects
Commit b7c4d270 authored by František Dvořák's avatar František Dvořák
Browse files

Set AuthZ - development and testing instances

parent 39de2df3
No related branches found
No related tags found
No related merge requests found
...@@ -72,6 +72,49 @@ singleuser: ...@@ -72,6 +72,49 @@ singleuser:
image: "valtri/single-user:jupyter-4e-collab" image: "valtri/single-user:jupyter-4e-collab"
extra_annotations: extra_annotations:
"egi.eu/flavor": "small-environment-2-vcpu-4-gb-ram" "egi.eu/flavor": "small-environment-2-vcpu-4-gb-ram"
- display_name: Small Environment - 2 vCPU / 4 GB RAM
description: >
The notebook environment includes Python, R, Julia and Octave kernels.
default: true
kubespawner_override:
args:
- "--CondaKernelSpecManager.env_filter='/opt/conda$'"
extra_annotations:
"egi.eu/flavor": "small-environment-2-vcpu-4-gb-ram"
vo_claims:
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:2-vcpu-4-gb-ram:act:ppa
- display_name: Medium Environment - 4 vCPU / 8 GB RAM
description: >
The notebook environment includes Python, R, Julia and Octave kernels.
kubespawner_override:
args:
- "--CondaKernelSpecManager.env_filter='/opt/conda$'"
extra_annotations:
"egi.eu/flavor": "medium-environment-4-vcpu-8-gb-ram"
cpu_guarantee: 0.4
cpu_limit: 4
mem_guarantee: 1G
mem_limit: 8G
vo_claims:
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:4-vcpu-8-gb-ram:act:ppa
- display_name: Large Environment - 8 vCPU / 16 GB RAM / GPU
description: >
The notebook environment includes Python, R, Julia and Octave kernels with GPU.
kubespawner_override:
args:
- "--CondaKernelSpecManager.env_filter='/opt/conda$'"
cpu_guarantee: 0.8
cpu_limit: 8
mem_guarantee: 2G
mem_limit: 16G
extra_annotations:
"egi.eu/flavor": "large-environment-8-vcpu-16-gb-ram-gpu"
extra_resource_guarantees:
nvidia.com/gpu: 1
extra_resource_limits:
nvidia.com/gpu: 1
vo_claims:
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:8-vcpu-16-gb-ram-gpu:act:ppa
cmd: jupyterhub-singleuser-webdav-wrapper cmd: jupyterhub-singleuser-webdav-wrapper
extraFiles: extraFiles:
wait-remote-home.sh: wait-remote-home.sh:
...@@ -144,8 +187,12 @@ hub: ...@@ -144,8 +187,12 @@ hub:
# valtri@civ.zcu.cz # valtri@civ.zcu.cz
- c36b18fe-e03a-4a22-ab14-5965e0171410@eosc-federation.eu - c36b18fe-e03a-4a22-ab14-5965e0171410@eosc-federation.eu
allowed_groups: allowed_groups:
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:2-vcpu-4-gb-ram:act:ppa
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:4-vcpu-8-gb-ram:act:ppa
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:8-vcpu-16-gb-ram-gpu:act:ppa
- urn:geant:eosc-federation.eu:testing:group:eosc - urn:geant:eosc-federation.eu:testing:group:eosc
auto_login: true admin_groups:
- urn:geant:eosc-federation.eu:group:asg:notebooks.open-science-cloud.ec.europa.eu:role=admin
claim_groups_key: "entitlements" claim_groups_key: "entitlements"
EGICheckinAuthenticator: EGICheckinAuthenticator:
checkin_host: "{{ secret['checkin_host'] }}" checkin_host: "{{ secret['checkin_host'] }}"
...@@ -174,84 +221,11 @@ hub: ...@@ -174,84 +221,11 @@ hub:
c.JupyterHub.extra_handlers = [(r'/welcome', WelcomeHandler)] c.JupyterHub.extra_handlers = [(r'/welcome', WelcomeHandler)]
egi-notebooks-b2drop: |- egi-notebooks-b2drop: |-
{%- raw %} {%- raw %}
import base64
import json import json
from jinja2 import BaseLoader
from jinja2 import Environment
from egi_notebooks_hub.onedata import OnedataSpawner from egi_notebooks_hub.onedata import OnedataSpawner
from kubernetes_asyncio.client.rest import ApiException
from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPRequest from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPRequest
class WebDavOIDCSpawner(OnedataSpawner):
class B2DropSpawner(OnedataSpawner):
async def auth_state_hook(self, spawner, auth_state):
await super().auth_state_hook(spawner, auth_state)
self.b2drop_ready = False
self.b2drop_user = ""
self.b2drop_pwd = ""
try:
secret = await self.api.read_namespaced_secret(self.token_secret_name, self.namespace)
except ApiException:
return
if secret and secret.data:
self.b2drop_user = base64.b64decode(secret.data.get("b2drop-user", "")).decode()
self.b2drop_pwd = base64.b64decode(secret.data.get("b2drop-pwd", "")).decode()
self.b2drop_ready = (self.b2drop_user and self.b2drop_pwd)
def _render_options_form(self, profile_list):
# old:self._profile_list = self._init_profile_list(profile_list)
self._profile_list = self._get_initialized_profile_list(profile_list)
profile_form_template = Environment(loader=BaseLoader).from_string(
self.profile_form_template
)
return profile_form_template.render(profile_list=self._profile_list, b2drop_ready=self.b2drop_ready, b2drop_user=self.b2drop_user, b2drop_pwd=self.b2drop_pwd)
async def pre_spawn_hook(self, spawner):
await super(B2DropSpawner, self).pre_spawn_hook(spawner)
b2drop_user = self.user_options.get("b2drop-user", "")
b2drop_pwd = self.user_options.get("b2drop-pwd", "")
if not (b2drop_user and b2drop_pwd):
secret = await self.api.read_namespaced_secret(self.token_secret_name, self.namespace)
if secret and secret.data:
b2drop_user = base64.b64decode(secret.data.get("b2drop-user", "")).decode()
b2drop_pwd = base64.b64decode(secret.data.get("b2drop-pwd", "")).decode()
if b2drop_user and b2drop_pwd:
volume_mounts = [
{"mountPath": "/owncloud:shared", "name": "owncloud-home"},
]
spawner.extra_containers.append(
{
"name": "b2drop",
"image": "eginotebooks/webdav-rclone-sidecar:sha-0a62679",
"env": [
{"name": "WEBDAV_URL", "value": "https://b2drop.eudat.eu/remote.php/webdav"},
{"name": "WEBDAV_PWD", "value": b2drop_pwd},
{"name": "WEBDAV_USER", "value": b2drop_user},
{"name": "WEBDAV_VENDOR", "value": "other"},
{"name": "MOUNT_PATH", "value": "/owncloud/b2drop"},
{"name": "MOUNT_WAIT_POINT", "value": "webdav-fs: /owncloud fuse.rclone"},
],
"resources": self.sidecar_resources,
"securityContext": {
"runAsUser": 1000,
"fsUser": 1000,
"fsGroup": 100,
"privileged": True,
"capabilities": {"add": ["SYS_ADMIN"]},
},
"volumeMounts": volume_mounts,
}
)
def options_from_form(self, formdata):
data = super(B2DropSpawner, self)._options_from_form(formdata)
data.update({'b2drop-user': formdata.get('b2drop-user', [None])[0],
'b2drop-pwd': formdata.get('b2drop-pwd', [None])[0]})
return data
class WebDavOIDCSpawner(B2DropSpawner):
# ownCloud Infinite Scale parameters # ownCloud Infinite Scale parameters
# (https://owncloud.dev/apis/http/graph/spaces/#list-my-spaces-get-medrives) # (https://owncloud.dev/apis/http/graph/spaces/#list-my-spaces-get-medrives)
OCIS_URL = "https://ocis.aaitest.owncloud.works" OCIS_URL = "https://ocis.aaitest.owncloud.works"
...@@ -346,100 +320,9 @@ hub: ...@@ -346,100 +320,9 @@ hub:
else: else:
self.log.info("No auth state, skipping ownCloud") self.log.info("No auth state, skipping ownCloud")
c.JupyterHub.spawner_class = WebDavOIDCSpawner c.JupyterHub.spawner_class = WebDavOIDCSpawner
c.B2DropSpawner.token_mount_path = "/var/run/secrets/oidc/" c.WebDavOIDCSpawner.token_mount_path = "/var/run/secrets/oidc/"
c.B2DropSpawner.http_timeout = 90 c.WebDavOIDCSpawner.http_timeout = 90
c.B2DropSpawner.profile_form_template = """
<style>
/*
.profile divs holds two div tags: one for a radio button, and one
for the profile's content.
*/
#kubespawner-profiles-list .profile {
display: flex;
flex-direction: row;
font-weight: normal;
border-bottom: 1px solid #ccc;
padding-bottom: 12px;
}
#kubespawner-profiles-list .profile .radio {
padding: 12px;
}
/* .option divs holds a label and a select tag */
#kubespawner-profiles-list .profile .option {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 12px;
}
#kubespawner-profiles-list .profile .option label {
font-weight: normal;
margin-right: 8px;
min-width: 96px;
}
</style>
<div class='form-group' id='kubespawner-profiles-list'>
{%- for profile in profile_list %}
{#- Wrap everything in a <label> so clicking anywhere selects the option #}
<label for='profile-item-{{ profile.slug }}' class='profile'>
<div class='radio'>
<input type='radio' name='profile' id='profile-item-{{ profile.slug }}' value='{{ profile.slug }}' {% if profile.default %}checked{% endif %} />
</div>
<div>
<h3>{{ profile.display_name }}</h3>
{%- if profile.description %}
<p>{{ profile.description }}</p>
{%- endif %}
{%- if profile.profile_options %}
<div>
{%- for k, option in profile.profile_options.items() %}
<div class='option'>
<label for='profile-option-{{profile.slug}}-{{k}}'>{{option.display_name}}</label>
<select name="profile-option-{{profile.slug}}-{{k}}" class="form-control">
{%- for k, choice in option['choices'].items() %}
<option value="{{ k }}" {% if choice.default %}selected{%endif %}>{{ choice.display_name }}</option>
{%- endfor %}
</select>
</div>
{%- endfor %}
</div>
{%- endif %}
</div>
</label>
{%- endfor %}
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a class="collabpsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
B2DROP connection
</a>
{%if b2drop_ready %}<span class="label label-success">Already configured!</span>{% endif %}
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<div class='form-group'>
<label for="b2drop-user" class="form-label">B2DROP app Username</label>
<input type="text" class="form-control" name="b2drop-user" id="b2drop-user" aria-describedby="b2drop-user-help" value="{{ b2drop_user }}">
<div id="b2drop-user-help" class="form-text">Create new app password at <a href="https://b2drop.eudat.eu/settings/user/security">B2DROP security configuration</a></div>
</div>
<div class='form-group'>
<label for="b2drop-pwd" class="form-label">B2DROP app Password</label>
<input type="password" class="form-control" name="b2drop-pwd" id="b2drop-pwd" value="{{ b2drop_pwd }}">
</div>
</div>
</div>
</div>
</div>
"""
{% endraw %} {% endraw %}
extraFiles: extraFiles:
welcome.html: welcome.html:
...@@ -448,6 +331,19 @@ hub: ...@@ -448,6 +331,19 @@ hub:
{%- raw %} {%- raw %}
{% extends "login.html" %} {% extends "login.html" %}
{% endraw %} {% endraw %}
403.html:
mountPath: /usr/local/share/jupyterhub/templates/403.html
stringData: |-
{%- raw %}
{% extends "error.html" %}
{% block main %}
<div class="error">
<h1>Unauthorized</h1>
<p>You don't have the correct entitlements to access this service.</p>
<p>If you think you should be granted access, please open an issue!</p>
</div>
{% endblock %}
{% endraw %}
debug: debug:
enabled: true enabled: true
...@@ -64,6 +64,8 @@ singleuser: ...@@ -64,6 +64,8 @@ singleuser:
- "--CondaKernelSpecManager.env_filter='/opt/conda$'" - "--CondaKernelSpecManager.env_filter='/opt/conda$'"
extra_annotations: extra_annotations:
"egi.eu/flavor": "small-environment-2-vcpu-4-gb-ram" "egi.eu/flavor": "small-environment-2-vcpu-4-gb-ram"
vo_claims:
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:2-vcpu-4-gb-ram:act:ppa
- display_name: Medium Environment - 4 vCPU / 8 GB RAM - display_name: Medium Environment - 4 vCPU / 8 GB RAM
description: > description: >
The notebook environment includes Python, R, Julia and Octave kernels. The notebook environment includes Python, R, Julia and Octave kernels.
...@@ -76,6 +78,8 @@ singleuser: ...@@ -76,6 +78,8 @@ singleuser:
cpu_limit: 4 cpu_limit: 4
mem_guarantee: 1G mem_guarantee: 1G
mem_limit: 8G mem_limit: 8G
vo_claims:
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:4-vcpu-8-gb-ram:act:ppa
- display_name: Large Environment - 8 vCPU / 16 GB RAM / GPU - display_name: Large Environment - 8 vCPU / 16 GB RAM / GPU
description: > description: >
The notebook environment includes Python, R, Julia and Octave kernels with GPU. The notebook environment includes Python, R, Julia and Octave kernels with GPU.
...@@ -92,6 +96,8 @@ singleuser: ...@@ -92,6 +96,8 @@ singleuser:
nvidia.com/gpu: 1 nvidia.com/gpu: 1
extra_resource_limits: extra_resource_limits:
nvidia.com/gpu: 1 nvidia.com/gpu: 1
vo_claims:
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:8-vcpu-16-gb-ram-gpu:act:ppa
cmd: jupyterhub-singleuser-webdav-wrapper cmd: jupyterhub-singleuser-webdav-wrapper
extraFiles: extraFiles:
wait-remote-home.sh: wait-remote-home.sh:
...@@ -164,8 +170,11 @@ hub: ...@@ -164,8 +170,11 @@ hub:
# valtri@civ.zcu.cz # valtri@civ.zcu.cz
- c36b18fe-e03a-4a22-ab14-5965e0171410@eosc-federation.eu - c36b18fe-e03a-4a22-ab14-5965e0171410@eosc-federation.eu
allowed_groups: allowed_groups:
- urn:geant:eosc-federation.eu:testing:group:eosc - urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:2-vcpu-4-gb-ram:act:ppa
auto_login: true - urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:4-vcpu-8-gb-ram:act:ppa
- urn:geant:eosc-federation.eu:res:notebooks.open-science-cloud.ec.europa.eu:8-vcpu-16-gb-ram-gpu:act:ppa
admin_groups:
- urn:geant:eosc-federation.eu:group:asg:notebooks.open-science-cloud.ec.europa.eu:role=admin
claim_groups_key: "entitlements" claim_groups_key: "entitlements"
EGICheckinAuthenticator: EGICheckinAuthenticator:
checkin_host: "{{ secret['checkin_host'] }}" checkin_host: "{{ secret['checkin_host'] }}"
...@@ -194,84 +203,11 @@ hub: ...@@ -194,84 +203,11 @@ hub:
c.JupyterHub.extra_handlers = [(r'/welcome', WelcomeHandler)] c.JupyterHub.extra_handlers = [(r'/welcome', WelcomeHandler)]
egi-notebooks-b2drop: |- egi-notebooks-b2drop: |-
{%- raw %} {%- raw %}
import base64
import json import json
from jinja2 import BaseLoader
from jinja2 import Environment
from egi_notebooks_hub.onedata import OnedataSpawner from egi_notebooks_hub.onedata import OnedataSpawner
from kubernetes_asyncio.client.rest import ApiException
from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPRequest from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPRequest
class WebDavOIDCSpawner(OnedataSpawner):
class B2DropSpawner(OnedataSpawner):
async def auth_state_hook(self, spawner, auth_state):
await super().auth_state_hook(spawner, auth_state)
self.b2drop_ready = False
self.b2drop_user = ""
self.b2drop_pwd = ""
try:
secret = await self.api.read_namespaced_secret(self.token_secret_name, self.namespace)
except ApiException:
return
if secret and secret.data:
self.b2drop_user = base64.b64decode(secret.data.get("b2drop-user", "")).decode()
self.b2drop_pwd = base64.b64decode(secret.data.get("b2drop-pwd", "")).decode()
self.b2drop_ready = (self.b2drop_user and self.b2drop_pwd)
def _render_options_form(self, profile_list):
# old:self._profile_list = self._init_profile_list(profile_list)
self._profile_list = self._get_initialized_profile_list(profile_list)
profile_form_template = Environment(loader=BaseLoader).from_string(
self.profile_form_template
)
return profile_form_template.render(profile_list=self._profile_list, b2drop_ready=self.b2drop_ready, b2drop_user=self.b2drop_user, b2drop_pwd=self.b2drop_pwd)
async def pre_spawn_hook(self, spawner):
await super(B2DropSpawner, self).pre_spawn_hook(spawner)
b2drop_user = self.user_options.get("b2drop-user", "")
b2drop_pwd = self.user_options.get("b2drop-pwd", "")
if not (b2drop_user and b2drop_pwd):
secret = await self.api.read_namespaced_secret(self.token_secret_name, self.namespace)
if secret and secret.data:
b2drop_user = base64.b64decode(secret.data.get("b2drop-user", "")).decode()
b2drop_pwd = base64.b64decode(secret.data.get("b2drop-pwd", "")).decode()
if b2drop_user and b2drop_pwd:
volume_mounts = [
{"mountPath": "/owncloud:shared", "name": "owncloud-home"},
]
spawner.extra_containers.append(
{
"name": "b2drop",
"image": "eginotebooks/webdav-rclone-sidecar:sha-0a62679",
"env": [
{"name": "WEBDAV_URL", "value": "https://b2drop.eudat.eu/remote.php/webdav"},
{"name": "WEBDAV_PWD", "value": b2drop_pwd},
{"name": "WEBDAV_USER", "value": b2drop_user},
{"name": "WEBDAV_VENDOR", "value": "other"},
{"name": "MOUNT_PATH", "value": "/owncloud/b2drop"},
{"name": "MOUNT_WAIT_POINT", "value": "webdav-fs: /owncloud fuse.rclone"},
],
"resources": self.sidecar_resources,
"securityContext": {
"runAsUser": 1000,
"fsUser": 1000,
"fsGroup": 100,
"privileged": True,
"capabilities": {"add": ["SYS_ADMIN"]},
},
"volumeMounts": volume_mounts,
}
)
def options_from_form(self, formdata):
data = super(B2DropSpawner, self)._options_from_form(formdata)
data.update({'b2drop-user': formdata.get('b2drop-user', [None])[0],
'b2drop-pwd': formdata.get('b2drop-pwd', [None])[0]})
return data
class WebDavOIDCSpawner(B2DropSpawner):
# ownCloud Infinite Scale parameters # ownCloud Infinite Scale parameters
# (https://owncloud.dev/apis/http/graph/spaces/#list-my-spaces-get-medrives) # (https://owncloud.dev/apis/http/graph/spaces/#list-my-spaces-get-medrives)
OCIS_URL = "https://ocis.aaitest.owncloud.works" OCIS_URL = "https://ocis.aaitest.owncloud.works"
...@@ -366,100 +302,9 @@ hub: ...@@ -366,100 +302,9 @@ hub:
else: else:
self.log.info("No auth state, skipping ownCloud") self.log.info("No auth state, skipping ownCloud")
c.JupyterHub.spawner_class = WebDavOIDCSpawner c.JupyterHub.spawner_class = WebDavOIDCSpawner
c.B2DropSpawner.token_mount_path = "/var/run/secrets/oidc/" c.WebDavOIDCSpawner.token_mount_path = "/var/run/secrets/oidc/"
c.B2DropSpawner.http_timeout = 90 c.WebDavOIDCSpawner.http_timeout = 90
c.B2DropSpawner.profile_form_template = """
<style>
/*
.profile divs holds two div tags: one for a radio button, and one
for the profile's content.
*/
#kubespawner-profiles-list .profile {
display: flex;
flex-direction: row;
font-weight: normal;
border-bottom: 1px solid #ccc;
padding-bottom: 12px;
}
#kubespawner-profiles-list .profile .radio {
padding: 12px;
}
/* .option divs holds a label and a select tag */
#kubespawner-profiles-list .profile .option {
display: flex;
flex-direction: row;
align-items: center;
padding-bottom: 12px;
}
#kubespawner-profiles-list .profile .option label {
font-weight: normal;
margin-right: 8px;
min-width: 96px;
}
</style>
<div class='form-group' id='kubespawner-profiles-list'>
{%- for profile in profile_list %}
{#- Wrap everything in a <label> so clicking anywhere selects the option #}
<label for='profile-item-{{ profile.slug }}' class='profile'>
<div class='radio'>
<input type='radio' name='profile' id='profile-item-{{ profile.slug }}' value='{{ profile.slug }}' {% if profile.default %}checked{% endif %} />
</div>
<div>
<h3>{{ profile.display_name }}</h3>
{%- if profile.description %}
<p>{{ profile.description }}</p>
{%- endif %}
{%- if profile.profile_options %}
<div>
{%- for k, option in profile.profile_options.items() %}
<div class='option'>
<label for='profile-option-{{profile.slug}}-{{k}}'>{{option.display_name}}</label>
<select name="profile-option-{{profile.slug}}-{{k}}" class="form-control">
{%- for k, choice in option['choices'].items() %}
<option value="{{ k }}" {% if choice.default %}selected{%endif %}>{{ choice.display_name }}</option>
{%- endfor %}
</select>
</div>
{%- endfor %}
</div>
{%- endif %}
</div>
</label>
{%- endfor %}
<div class="panel-group" id="accordion" role="tablist" aria-multiselectable="true">
<div class="panel panel-default">
<div class="panel-heading" role="tab" id="headingOne">
<h4 class="panel-title">
<a class="collabpsed" role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseOne" aria-expanded="false" aria-controls="collapseOne">
B2DROP connection
</a>
{%if b2drop_ready %}<span class="label label-success">Already configured!</span>{% endif %}
</h4>
</div>
<div id="collapseOne" class="panel-collapse collapse" role="tabpanel" aria-labelledby="headingOne">
<div class="panel-body">
<div class='form-group'>
<label for="b2drop-user" class="form-label">B2DROP app Username</label>
<input type="text" class="form-control" name="b2drop-user" id="b2drop-user" aria-describedby="b2drop-user-help" value="{{ b2drop_user }}">
<div id="b2drop-user-help" class="form-text">Create new app password at <a href="https://b2drop.eudat.eu/settings/user/security">B2DROP security configuration</a></div>
</div>
<div class='form-group'>
<label for="b2drop-pwd" class="form-label">B2DROP app Password</label>
<input type="password" class="form-control" name="b2drop-pwd" id="b2drop-pwd" value="{{ b2drop_pwd }}">
</div>
</div>
</div>
</div>
</div>
"""
{% endraw %} {% endraw %}
extraFiles: extraFiles:
welcome.html: welcome.html:
...@@ -467,4 +312,17 @@ hub: ...@@ -467,4 +312,17 @@ hub:
stringData: |- stringData: |-
{%- raw %} {%- raw %}
{% extends "login.html" %} {% extends "login.html" %}
{% endraw %}
403.html:
mountPath: /usr/local/share/jupyterhub/templates/403.html
stringData: |-
{%- raw %}
{% extends "error.html" %}
{% block main %}
<div class="error">
<h1>Unauthorized</h1>
<p>You don't have the correct entitlements to access this service.</p>
<p>If you think you should be granted access, please open an issue!</p>
</div>
{% endblock %}
{% endraw %} {% endraw %}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment