Update service examples and documentation with access scopes and roles

This commit is contained in:
Min RK
2021-05-21 10:17:57 +02:00
parent 69d2839ba3
commit 40de16e0e1
16 changed files with 291 additions and 138 deletions

View File

@@ -86,10 +86,18 @@ Hub-Managed Service would include:
This example would be configured as follows in `jupyterhub_config.py`: This example would be configured as follows in `jupyterhub_config.py`:
```python ```python
c.JupyterHub.load_roles = [
{
"name": "idle-culler",
"scopes": [
"users:servers",
# also 'admin:users' if culling idle users as well
]
}
c.JupyterHub.services = [ c.JupyterHub.services = [
{ {
'name': 'idle-culler', 'name': 'idle-culler',
'admin': True,
'command': [sys.executable, '-m', 'jupyterhub_idle_culler', '--timeout=3600'] 'command': [sys.executable, '-m', 'jupyterhub_idle_culler', '--timeout=3600']
} }
] ]
@@ -114,6 +122,7 @@ JUPYTERHUB_BASE_URL: Base URL of the Hub (https://mydomain[:port]/)
JUPYTERHUB_SERVICE_PREFIX: URL path prefix of this service (/services/:service-name/) JUPYTERHUB_SERVICE_PREFIX: URL path prefix of this service (/services/:service-name/)
JUPYTERHUB_SERVICE_URL: Local URL where the service is expected to be listening. JUPYTERHUB_SERVICE_URL: Local URL where the service is expected to be listening.
Only for proxied web services. Only for proxied web services.
JUPYTERHUB_OAUTH_SCOPES: JSON-serialized list of scopes to use for allowing access to the service.
``` ```
For the previous 'cull idle' Service example, these environment variables For the previous 'cull idle' Service example, these environment variables
@@ -231,50 +240,8 @@ service. See the `service-whoami-flask` example in the
[JupyterHub GitHub repo](https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-whoami-flask) [JupyterHub GitHub repo](https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-whoami-flask)
for more details. for more details.
```python ```{literalinclude} ../../../examples/service-whoami-flask/whoami-flask.py
from functools import wraps :language: python
import json
import os
from urllib.parse import quote
from flask import Flask, redirect, request, Response
from jupyterhub.services.auth import HubOAuth
prefix = os.environ.get('JUPYTERHUB_SERVICE_PREFIX', '/')
auth = HubOAuth(
api_token=os.environ['JUPYTERHUB_API_TOKEN'],
cache_max_age=60,
)
app = Flask(__name__)
def authenticated(f):
"""Decorator for authenticating with the Hub"""
@wraps(f)
def decorated(*args, **kwargs):
token = request.headers.get(auth.auth_header_name)
if token:
user = auth.user_for_token(token)
else:
user = None
if user:
return f(user, *args, **kwargs)
else:
# redirect to login url on failed auth
return redirect(auth.login_url + '?next=%s' % quote(request.path))
return decorated
@app.route(prefix)
@authenticated
def whoami(user):
return Response(
json.dumps(user, indent=1, sort_keys=True),
mimetype='application/json',
)
``` ```
### Authenticating tornado services with JupyterHub ### Authenticating tornado services with JupyterHub
@@ -315,25 +282,38 @@ undefined, then any user will be allowed.
If you don't want to use the reference implementation If you don't want to use the reference implementation
(e.g. you find the implementation a poor fit for your Flask app), (e.g. you find the implementation a poor fit for your Flask app),
you can implement authentication via the Hub yourself. you can implement authentication via the Hub yourself.
We recommend looking at the [`HubAuth`][hubauth] class implementation for reference, JupyterHub is a standard OAuth2 provider,
so you can any OAuth 2 implementation appropriate for
See the [FastAPI example][] for an example of using JupyterHub as an OAuth provider with fastapi,
without using any code imported from JupyterHub.
On completion of OAuth, you will have an access token for JupyterHub,
which can be used to identify the user and the permissions (scopes)
the user has authorized for your service.
You will only get to this stage if the user has the required `access:services!service=$service-name` scope.
To retrieve the user model for the token, make a request to `GET /hub/api/user` with the token in the Authorization header.
For example, using flask:
```{literal-include} ../../../examples/service-whoami-flask/whoami-flask.py
:language: python
```
We recommend looking at the [`HubOAuth`][huboauth] class implementation for reference,
and taking note of the following process: and taking note of the following process:
1. retrieve the cookie `jupyterhub-services` from the request. 1. retrieve the token from the request.
2. Make an API request `GET /hub/api/authorizations/cookie/jupyterhub-services/cookie-value`, 2. Make an API request `GET /hub/api/user`,
where cookie-value is the url-encoded value of the `jupyterhub-services` cookie. with the token in the `Authorization` header.
This request must be authenticated with a Hub API token in the `Authorization` header,
for example using the `api_token` from your [external service's configuration](#externally-managed-services).
For example, with [requests][]: For example, with [requests][]:
```python ```python
r = requests.get( r = requests.get(
'/'.join(["http://127.0.0.1:8081/hub/api", "http://127.0.0.1:8081/hub/api/user",
"authorizations/cookie/jupyterhub-services",
quote(encrypted_cookie, safe=''),
]),
headers = { headers = {
'Authorization' : 'token %s' % api_token, 'Authorization' : f'token {api_token}',
}, },
) )
r.raise_for_status() r.raise_for_status()
@@ -342,13 +322,27 @@ and taking note of the following process:
3. On success, the reply will be a JSON model describing the user: 3. On success, the reply will be a JSON model describing the user:
```json ```python
{ {
"name": "inara", "name": "inara",
"groups": ["serenity", "guild"] # groups may be omitted, depending on permissions
"groups": ["serenity", "guild"],
# scopes is new in JupyterHub 2.0
"scopes": [
"access:services",
"read:users:name",
"read:users!user=inara",
"..."
]
} }
``` ```
The `scopes` field can be used to manage access.
Note: a user will have access to a service to complete oauth access to the service for the first time.
Individual permissions may be revoked at any later point without revoking the token,
in which case the `scopes` field in this model should be checked on each access.
The default required scopes for access are available from `hub_auth.oauth_scopes` or `$JUPYTERHUB_OAUTH_SCOPES`.
An example of using an Externally-Managed Service and authentication is An example of using an Externally-Managed Service and authentication is
in [nbviewer README][nbviewer example] section on securing the notebook viewer, in [nbviewer README][nbviewer example] section on securing the notebook viewer,
and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/ed942b10a52b6259099e2dd687930871dc8aac22/nbviewer/providers/base.py#L95). and an example of its configuration is found [here](https://github.com/jupyter/nbviewer/blob/ed942b10a52b6259099e2dd687930871dc8aac22/nbviewer/providers/base.py#L95).
@@ -357,9 +351,9 @@ section on securing the notebook viewer.
[requests]: http://docs.python-requests.org/en/master/ [requests]: http://docs.python-requests.org/en/master/
[services_auth]: ../api/services.auth.html [services_auth]: ../api/services.auth.html
[hubauth]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth [huboauth]: ../api/services.auth.html#jupyterhub.services.auth.HubOAuth
[hubauth.user_for_cookie]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_cookie
[hubauth.user_for_token]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token [hubauth.user_for_token]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token
[hubauthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubAuthenticated [hubauthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubAuthenticated
[nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer [nbviewer example]: https://github.com/jupyter/nbviewer#securing-the-notebook-viewer
[fastapi example]: https://github.com/jupyterhub/jupyterhub/tree/HEAD/examples/service-fastapi
[jupyterhub_idle_culler]: https://github.com/jupyterhub/jupyterhub-idle-culler [jupyterhub_idle_culler]: https://github.com/jupyterhub/jupyterhub-idle-culler

View File

@@ -1,10 +1,10 @@
# Configuration file for jupyterhub (postgres example). # Configuration file for jupyterhub (postgres example).
c = get_config() c = get_config() # noqa
# Add some users. # Add some users.
c.JupyterHub.admin_users = {'rhea'} c.JupyterHub.admin_users = {'rhea'}
c.Authenticator.whitelist = {'ganymede', 'io', 'rhea'} c.Authenticator.allowed_users = {'ganymede', 'io', 'rhea'}
# These environment variables are automatically supplied by the linked postgres # These environment variables are automatically supplied by the linked postgres
# container. # container.

View File

@@ -6,15 +6,17 @@ that appear when JupyterHub renders pages.
To run the service as a hub-managed service simply include in your JupyterHub To run the service as a hub-managed service simply include in your JupyterHub
configuration file something like: configuration file something like:
```python
c.JupyterHub.services = [ c.JupyterHub.services = [
{ {
'name': 'announcement', 'name': 'announcement',
'url': 'http://127.0.0.1:8888', 'url': 'http://127.0.0.1:8888',
'command': [sys.executable, "-m", "announcement"], 'command': [sys.executable, "-m", "announcement", "--port", "8888"],
} }
] ]
```
This starts the announcements service up at `/services/announcement` when This starts the announcements service up at `/services/announcement/` when
JupyterHub launches. By default the announcement text is empty. JupyterHub launches. By default the announcement text is empty.
The `announcement` module has a configurable port (default 8888) and an API The `announcement` module has a configurable port (default 8888) and an API
@@ -23,15 +25,28 @@ that environment variable is set or `/` if it is not.
## Managing the Announcement ## Managing the Announcement
Admin users can set the announcement text with an API token: Users with permission can set the announcement text with an API token:
$ curl -X POST -H "Authorization: token <token>" \ $ curl -X POST -H "Authorization: token <token>" \
-d '{"announcement":"JupyterHub will be upgraded on August 14!"}' \ -d '{"announcement":"JupyterHub will be upgraded on August 14!"}' \
https://.../services/announcement https://.../services/announcement/
To grant permission, add a role (JupyterHub 2.0) with access to the announcement service:
```python
# grant the 'announcer' permission to access the announcement service
c.JupyterHub.load_roles = [
{
"name": "announcers",
"users": ["announcer"], # or groups
"scopes": ["access:services!service=announcement"],
}
]
```
Anyone can read the announcement: Anyone can read the announcement:
$ curl https://.../services/announcement | python -m json.tool $ curl https://.../services/announcement/ | python -m json.tool
{ {
announcement: "JupyterHub will be upgraded on August 14!", announcement: "JupyterHub will be upgraded on August 14!",
timestamp: "...", timestamp: "...",
@@ -41,10 +56,11 @@ Anyone can read the announcement:
The time the announcement was posted is recorded in the `timestamp` field and The time the announcement was posted is recorded in the `timestamp` field and
the user who posted the announcement is recorded in the `user` field. the user who posted the announcement is recorded in the `user` field.
To clear the announcement text, just DELETE. Only admin users can do this. To clear the announcement text, send a DELETE request.
This has the same permissionOnly admin users can do this.
$ curl -X POST -H "Authorization: token <token>" \ $ curl -X DELETE -H "Authorization: token <token>" \
https://.../services/announcement https://.../services/announcement/
## Seeing the Announcement in JupyterHub ## Seeing the Announcement in JupyterHub

View File

@@ -13,9 +13,6 @@ from jupyterhub.services.auth import HubAuthenticated
class AnnouncementRequestHandler(HubAuthenticated, web.RequestHandler): class AnnouncementRequestHandler(HubAuthenticated, web.RequestHandler):
"""Dynamically manage page announcements""" """Dynamically manage page announcements"""
hub_users = []
allow_admin = True
def initialize(self, storage): def initialize(self, storage):
"""Create storage for announcement text""" """Create storage for announcement text"""
self.storage = storage self.storage = storage

View File

@@ -2,11 +2,18 @@ import sys
# To run the announcement service managed by the hub, add this. # To run the announcement service managed by the hub, add this.
port = 9999
c.JupyterHub.services = [ c.JupyterHub.services = [
{ {
'name': 'announcement', 'name': 'announcement',
'url': 'http://127.0.0.1:8888', 'url': f'http://127.0.0.1:{port}',
'command': [sys.executable, "-m", "announcement"], 'command': [
sys.executable,
"-m",
"announcement",
'--port',
str(port),
],
} }
] ]
@@ -14,3 +21,19 @@ c.JupyterHub.services = [
# for an example of how to do this. # for an example of how to do this.
c.JupyterHub.template_paths = ["templates"] c.JupyterHub.template_paths = ["templates"]
c.Authenticator.allowed_users = {"announcer", "otheruser"}
# grant the 'announcer' permission to access the announcement service
c.JupyterHub.load_roles = [
{
"name": "announcers",
"users": ["announcer"],
"scopes": ["access:services!service=announcement"],
}
]
# dummy spawner and authenticator for testing, don't actually use these!
c.JupyterHub.authenticator_class = 'dummy'
c.JupyterHub.spawner_class = 'simple'
c.JupyterHub.ip = '127.0.0.1' # let's just run on localhost while dummy auth is enabled

View File

@@ -16,6 +16,7 @@ jupyterhub --ip=127.0.0.1
``` ```
2. Visit http://127.0.0.1:8000/services/fastapi or http://127.0.0.1:8000/services/fastapi/docs 2. Visit http://127.0.0.1:8000/services/fastapi or http://127.0.0.1:8000/services/fastapi/docs
Login with username 'test-user' and any password.
3. Try interacting programmatically. If you create a new token in your control panel or pull out the `JUPYTERHUB_API_TOKEN` in the single user environment, you can skip the third step here. 3. Try interacting programmatically. If you create a new token in your control panel or pull out the `JUPYTERHUB_API_TOKEN` in the single user environment, you can skip the third step here.
@@ -24,10 +25,10 @@ $ curl -X GET http://127.0.0.1:8000/services/fastapi/
{"Hello":"World"} {"Hello":"World"}
$ curl -X GET http://127.0.0.1:8000/services/fastapi/me $ curl -X GET http://127.0.0.1:8000/services/fastapi/me
{"detail":"Must login with token parameter, cookie, or header"} {"detail":"Must login with token parameter, or Authorization bearer header"}
$ curl -X POST http://127.0.0.1:8000/hub/api/authorizations/token \ $ curl -X POST http://127.0.0.1:8000/hub/api/users/test-user/tokens \
-d '{"username": "myname", "password": "mypasswd!"}' \ -d '{"auth": {"username": "test-user", "password": "mypasswd!"}}' \
| jq '.token' | jq '.token'
"3fee13ce6d2845da9bd5f2c2170d3428" "3fee13ce6d2845da9bd5f2c2170d3428"
@@ -35,13 +36,18 @@ $ curl -X GET http://127.0.0.1:8000/services/fastapi/me \
-H "Authorization: Bearer 3fee13ce6d2845da9bd5f2c2170d3428" \ -H "Authorization: Bearer 3fee13ce6d2845da9bd5f2c2170d3428" \
| jq . | jq .
{ {
"name": "myname", "name": "test-user",
"admin": false, "admin": false,
"groups": [], "groups": [],
"server": null, "server": null,
"pending": null, "pending": null,
"last_activity": "2021-04-07T18:05:11.587638+00:00", "last_activity": "2021-05-21T09:13:00.514309+00:00",
"servers": null "servers": null,
"scopes": [
"access:services",
"access:users:servers!user=test-user",
"...",
]
} }
``` ```

View File

@@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime
from typing import Any from typing import Any
from typing import Dict
from typing import List from typing import List
from typing import Optional from typing import Optional
@@ -22,11 +23,12 @@ class Server(BaseModel):
class User(BaseModel): class User(BaseModel):
name: str name: str
admin: bool admin: bool
groups: List[str] groups: Optional[List[str]]
server: Optional[str] server: Optional[str]
pending: Optional[str] pending: Optional[str]
last_activity: datetime last_activity: datetime
servers: Optional[List[Server]] servers: Optional[Dict[str, Server]]
scopes: List[str]
# https://stackoverflow.com/questions/64501193/fastapi-how-to-use-httpexception-in-responses # https://stackoverflow.com/questions/64501193/fastapi-how-to-use-httpexception-in-responses

View File

@@ -1,3 +1,4 @@
import json
import os import os
from fastapi import HTTPException from fastapi import HTTPException
@@ -27,6 +28,12 @@ auth_by_header = OAuth2AuthorizationCodeBearer(
### our client_secret (JUPYTERHUB_API_TOKEN) and that code to get an ### our client_secret (JUPYTERHUB_API_TOKEN) and that code to get an
### access_token, which it returns to browser, which places in Authorization header. ### access_token, which it returns to browser, which places in Authorization header.
if os.environ.get("JUPYTERHUB_OAUTH_SCOPES"):
# typically ["access:services", "access:services!service=$service_name"]
access_scopes = json.loads(os.environ["JUPYTERHUB_OAUTH_SCOPES"])
else:
access_scopes = ["access:services"]
### For consideration: optimize performance with a cache instead of ### For consideration: optimize performance with a cache instead of
### always hitting the Hub api? ### always hitting the Hub api?
async def get_current_user( async def get_current_user(
@@ -58,4 +65,15 @@ async def get_current_user(
}, },
) )
user = User(**resp.json()) user = User(**resp.json())
if any(scope in user.scopes for scope in access_scopes):
return user return user
else:
raise HTTPException(
status.HTTP_403_FORBIDDEN,
detail={
"msg": f"User not authorized: {user.name}",
"request_url": str(resp.request.url),
"token": token,
"user": resp.json(),
},
)

View File

@@ -24,8 +24,21 @@ c.JupyterHub.services = [
"name": service_name, "name": service_name,
"url": "http://127.0.0.1:10202", "url": "http://127.0.0.1:10202",
"command": ["uvicorn", "app:app", "--port", "10202"], "command": ["uvicorn", "app:app", "--port", "10202"],
"admin": True,
"oauth_redirect_uri": oauth_redirect_uri, "oauth_redirect_uri": oauth_redirect_uri,
"environment": {"PUBLIC_HOST": public_host}, "environment": {"PUBLIC_HOST": public_host},
} }
] ]
c.JupyterHub.load_roles = [
{
"name": "user",
# grant all users access to services
"scopes": ["self", "access:services"],
},
]
# dummy for testing, create test-user
c.Authenticator.allowed_users = {"test-user"}
c.JupyterHub.authenticator_class = "dummy"
c.JupyterHub.spawner_class = "simple"

View File

@@ -1,33 +1,35 @@
# our user list # our user list
c.Authenticator.allowed_users = ['minrk', 'ellisonbg', 'willingc'] c.Authenticator.allowed_users = ['minrk', 'ellisonbg', 'willingc']
# ellisonbg and willingc have access to a shared server: service_name = 'shared-notebook'
service_port = 9999
group_name = 'shared'
c.JupyterHub.load_groups = {'shared-notebook-grp': ['ellisonbg', 'willingc']} # ellisonbg and willingc are in a group that will access the shared server:
c.JupyterHub.load_roles = [ c.JupyterHub.load_groups = {group_name: ['ellisonbg', 'willingc']}
{
"name": "shared-notebook",
"groups": ["shared-notebook-grp"],
"scopes": ["access:services!service=shared-notebook"],
},
# by default, the user role has access to all services
# we want to limit that, so give users only access to 'self'
{
"name": "user",
"scopes": ["self"],
},
]
# start the notebook server as a service # start the notebook server as a service
c.JupyterHub.services = [ c.JupyterHub.services = [
{ {
'name': 'shared-notebook', 'name': service_name,
'url': 'http://127.0.0.1:9999', 'url': 'http://127.0.0.1:{}'.format(service_port),
'api_token': 'c3a29e5d386fd7c9aa1e8fe9d41c282ec8b', 'api_token': 'c3a29e5d386fd7c9aa1e8fe9d41c282ec8b',
} }
] ]
# This "role assignment" is what grants members of the group
# access to the service
c.JupyterHub.load_roles = [
{
"name": "shared-notebook",
"groups": [group_name],
"scopes": [f"access:services!service={service_name}"],
},
]
# dummy spawner and authenticator for testing, don't actually use these! # dummy spawner and authenticator for testing, don't actually use these!
c.JupyterHub.authenticator_class = 'dummy' c.JupyterHub.authenticator_class = 'dummy'
c.JupyterHub.spawner_class = 'simple' c.JupyterHub.spawner_class = 'simple'
c.JupyterHub.ip = '127.0.0.1' # let's just run on localhost while dummy auth is enabled

View File

@@ -1,19 +1,35 @@
# our user list # our user list
c.Authenticator.whitelist = ['minrk', 'ellisonbg', 'willingc'] c.Authenticator.allowed_users = ['minrk', 'ellisonbg', 'willingc']
# ellisonbg and willingc have access to a shared server:
c.JupyterHub.load_groups = {'shared': ['ellisonbg', 'willingc']}
service_name = 'shared-notebook' service_name = 'shared-notebook'
service_port = 9999 service_port = 9999
group_name = 'shared' group_name = 'shared'
# ellisonbg and willingc have access to a shared server:
c.JupyterHub.load_groups = {group_name: ['ellisonbg', 'willingc']}
# start the notebook server as a service # start the notebook server as a service
c.JupyterHub.services = [ c.JupyterHub.services = [
{ {
'name': service_name, 'name': service_name,
'url': 'http://127.0.0.1:{}'.format(service_port), 'url': 'http://127.0.0.1:{}'.format(service_port),
'command': ['jupyterhub-singleuser', '--group=shared', '--debug'], 'command': ['jupyterhub-singleuser', '--debug'],
} }
] ]
# This "role assignment" is what grants members of the group
# access to the service
c.JupyterHub.load_roles = [
{
"name": "shared-notebook",
"groups": [group_name],
"scopes": [f"access:services!service={service_name}"],
},
]
# dummy spawner and authenticator for testing, don't actually use these!
c.JupyterHub.authenticator_class = 'dummy'
c.JupyterHub.spawner_class = 'simple'
c.JupyterHub.ip = '127.0.0.1' # let's just run on localhost while dummy auth is enabled

View File

@@ -2,15 +2,15 @@
Uses `jupyterhub.services.HubAuthenticated` to authenticate requests with the Hub. Uses `jupyterhub.services.HubAuthenticated` to authenticate requests with the Hub.
There is an implementation each of cookie-based `HubAuthenticated` and OAuth-based `HubOAuthenticated`. There is an implementation each of api-token-based `HubAuthenticated` and OAuth-based `HubOAuthenticated`.
## Run ## Run
1. Launch JupyterHub and the `whoami service` with 1. Launch JupyterHub and the `whoami` services with
jupyterhub --ip=127.0.0.1 jupyterhub --ip=127.0.0.1
2. Visit http://127.0.0.1:8000/services/whoami or http://127.0.0.1:8000/services/whoami-oauth 2. Visit http://127.0.0.1:8000/services/whoami-oauth
After logging in with your local-system credentials, you should see a JSON dump of your user info: After logging in with your local-system credentials, you should see a JSON dump of your user info:
@@ -24,15 +24,65 @@ After logging in with your local-system credentials, you should see a JSON dump
} }
``` ```
The `whoami-api` service powered by the base `HubAuthenticated` class only supports token-authenticated API requests,
not browser visits, because it does not implement OAuth. Visit it by requesting an api token from the tokens page,
and making a direct request:
```bash
$ curl -H "Authorization: token 8630bbd8ef064c48b22c7f122f0cd8ad" http://127.0.0.1:8000/services/whoami-api/ | jq .
{
"admin": false,
"created": "2021-05-21T09:47:41.299400Z",
"groups": [],
"kind": "user",
"last_activity": "2021-05-21T09:49:08.290745Z",
"name": "test",
"pending": null,
"roles": [
"user"
],
"scopes": [
"access:services",
"access:users:servers!user=test",
"read:users!user=test",
"read:users:activity!user=test",
"read:users:groups!user=test",
"read:users:name!user=test",
"read:users:servers!user=test",
"read:users:tokens!user=test",
"users!user=test",
"users:activity!user=test",
"users:groups!user=test",
"users:name!user=test",
"users:servers!user=test",
"users:tokens!user=test"
],
"server": null
}
```
This relies on the Hub starting the whoami services, via config (see [jupyterhub_config.py](./jupyterhub_config.py)). This relies on the Hub starting the whoami services, via config (see [jupyterhub_config.py](./jupyterhub_config.py)).
You may set the `hub_users` configuration in the service script To govern access to the services, create **roles** with the scope `access:services!service=$service-name`,
to restrict access to the service to a whitelist of allowed users. and assign users to the scope.
By default, any authenticated user is allowed.
The jupyterhub_config.py grants access for all users to all services via the default 'user' role, with:
```python
c.JupyterHub.load_roles = [
{
"name": "user",
# grant all users access to all services
"scopes": ["access:services", "self"],
}
]
```
A similar service could be run externally, by setting the JupyterHub service environment variables: A similar service could be run externally, by setting the JupyterHub service environment variables:
JUPYTERHUB_API_TOKEN JUPYTERHUB_API_TOKEN
JUPYTERHUB_SERVICE_PREFIX JUPYTERHUB_SERVICE_PREFIX
JUPYTERHUB_OAUTH_SCOPES
JUPYTERHUB_CLIENT_ID # for whoami-oauth only
or instantiating and configuring a HubAuth object yourself, and attaching it as `self.hub_auth` in your HubAuthenticated handlers. or instantiating and configuring a HubAuth object yourself, and attaching it as `self.hub_auth` in your HubAuthenticated handlers.

View File

@@ -2,7 +2,7 @@ import sys
c.JupyterHub.services = [ c.JupyterHub.services = [
{ {
'name': 'whoami', 'name': 'whoami-api',
'url': 'http://127.0.0.1:10101', 'url': 'http://127.0.0.1:10101',
'command': [sys.executable, './whoami.py'], 'command': [sys.executable, './whoami.py'],
}, },
@@ -12,3 +12,16 @@ c.JupyterHub.services = [
'command': [sys.executable, './whoami-oauth.py'], 'command': [sys.executable, './whoami-oauth.py'],
}, },
] ]
c.JupyterHub.load_roles = [
{
"name": "user",
# grant all users access to all services
"scopes": ["access:services", "self"],
}
]
# dummy spawner and authenticator for testing, don't actually use these!
c.JupyterHub.authenticator_class = 'dummy'
c.JupyterHub.spawner_class = 'simple'
c.JupyterHub.ip = '127.0.0.1' # let's just run on localhost while dummy auth is enabled

View File

@@ -1,6 +1,6 @@
"""An example service authenticating with the Hub. """An example service authenticating with the Hub.
This example service serves `/services/whoami/`, This example service serves `/services/whoami-oauth/`,
authenticated with the Hub, authenticated with the Hub,
showing the user their own info. showing the user their own info.
""" """
@@ -20,13 +20,6 @@ from jupyterhub.utils import url_path_join
class WhoAmIHandler(HubOAuthenticated, RequestHandler): class WhoAmIHandler(HubOAuthenticated, RequestHandler):
# hub_users can be a set of users who are allowed to access the service
# `getuser()` here would mean only the user who started the service
# can access the service:
# from getpass import getuser
# hub_users = {getuser()}
@authenticated @authenticated
def get(self): def get(self):
user_model = self.get_current_user() user_model = self.get_current_user()

View File

@@ -1,6 +1,8 @@
"""An example service authenticating with the Hub. """An example service authenticating with the Hub.
This serves `/services/whoami/`, authenticated with the Hub, showing the user their own info. This serves `/services/whoami-api/`, authenticated with the Hub, showing the user their own info.
HubAuthenticated only supports token-based access.
""" """
import json import json
import os import os
@@ -16,13 +18,6 @@ from jupyterhub.services.auth import HubAuthenticated
class WhoAmIHandler(HubAuthenticated, RequestHandler): class WhoAmIHandler(HubAuthenticated, RequestHandler):
# hub_users can be a set of users who are allowed to access the service
# `getuser()` here would mean only the user who started the service
# can access the service:
# from getpass import getuser
# hub_users = {getuser()}
@authenticated @authenticated
def get(self): def get(self):
user_model = self.get_current_user() user_model = self.get_current_user()

View File

@@ -824,13 +824,26 @@ class UserNotAllowed(Exception):
) )
class HubAuthenticated(object): class HubAuthenticated:
"""Mixin for tornado handlers that are authenticated with JupyterHub """Mixin for tornado handlers that are authenticated with JupyterHub
A handler that mixes this in must have the following attributes/properties: A handler that mixes this in must have the following attributes/properties:
- .hub_auth: A HubAuth instance - .hub_auth: A HubAuth instance
- .hub_scopes: A set of JupyterHub 2.0 OAuth scopes to allow. - .hub_scopes: A set of JupyterHub 2.0 OAuth scopes to allow.
Default comes from .hub_auth.oauth_scopes,
which in turn is set by $JUPYTERHUB_OAUTH_SCOPES
Default values include:
- 'access:services', 'access:services!service={service_name}' for services
- 'access:users:servers', 'access:users:servers!user={user}',
'access:users:servers!server={user}/{server_name}'
for single-user servers
If hub_scopes is not used (e.g. JupyterHub 1.x),
these additional properties can be used:
- .allow_admin: If True, allow any admin user.
Default: False.
- .hub_users: A set of usernames to allow. - .hub_users: A set of usernames to allow.
If left unspecified or None, username will not be checked. If left unspecified or None, username will not be checked.
- .hub_groups: A set of group names to allow. - .hub_groups: A set of group names to allow.
@@ -943,6 +956,8 @@ class HubAuthenticated(object):
# note: this means successful authentication, but insufficient permission # note: this means successful authentication, but insufficient permission
raise UserNotAllowed(model) raise UserNotAllowed(model)
# proceed with the pre-2.0 way if hub_scopes is not set
if self.allow_admin and model.get('admin', False): if self.allow_admin and model.get('admin', False):
app_log.debug("Allowing Hub admin %s", name) app_log.debug("Allowing Hub admin %s", name)
return model return model