mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 23:13:00 +00:00
Update service examples and documentation with access scopes and roles
This commit is contained in:
@@ -86,10 +86,18 @@ Hub-Managed Service would include:
|
||||
This example would be configured as follows in `jupyterhub_config.py`:
|
||||
|
||||
```python
|
||||
c.JupyterHub.load_roles = [
|
||||
{
|
||||
"name": "idle-culler",
|
||||
"scopes": [
|
||||
"users:servers",
|
||||
# also 'admin:users' if culling idle users as well
|
||||
]
|
||||
}
|
||||
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'idle-culler',
|
||||
'admin': True,
|
||||
'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_URL: Local URL where the service is expected to be listening.
|
||||
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
|
||||
@@ -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)
|
||||
for more details.
|
||||
|
||||
```python
|
||||
from functools import wraps
|
||||
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',
|
||||
)
|
||||
```{literalinclude} ../../../examples/service-whoami-flask/whoami-flask.py
|
||||
:language: python
|
||||
```
|
||||
|
||||
### 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
|
||||
(e.g. you find the implementation a poor fit for your Flask app),
|
||||
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:
|
||||
|
||||
1. retrieve the cookie `jupyterhub-services` from the request.
|
||||
2. Make an API request `GET /hub/api/authorizations/cookie/jupyterhub-services/cookie-value`,
|
||||
where cookie-value is the url-encoded value of the `jupyterhub-services` cookie.
|
||||
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).
|
||||
1. retrieve the token from the request.
|
||||
2. Make an API request `GET /hub/api/user`,
|
||||
with the token in the `Authorization` header.
|
||||
|
||||
For example, with [requests][]:
|
||||
|
||||
```python
|
||||
r = requests.get(
|
||||
'/'.join(["http://127.0.0.1:8081/hub/api",
|
||||
"authorizations/cookie/jupyterhub-services",
|
||||
quote(encrypted_cookie, safe=''),
|
||||
]),
|
||||
"http://127.0.0.1:8081/hub/api/user",
|
||||
headers = {
|
||||
'Authorization' : 'token %s' % api_token,
|
||||
'Authorization' : f'token {api_token}',
|
||||
},
|
||||
)
|
||||
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:
|
||||
|
||||
```json
|
||||
```python
|
||||
{
|
||||
"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
|
||||
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).
|
||||
@@ -357,9 +351,9 @@ section on securing the notebook viewer.
|
||||
|
||||
[requests]: http://docs.python-requests.org/en/master/
|
||||
[services_auth]: ../api/services.auth.html
|
||||
[hubauth]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth
|
||||
[hubauth.user_for_cookie]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_cookie
|
||||
[huboauth]: ../api/services.auth.html#jupyterhub.services.auth.HubOAuth
|
||||
[hubauth.user_for_token]: ../api/services.auth.html#jupyterhub.services.auth.HubAuth.user_for_token
|
||||
[hubauthenticated]: ../api/services.auth.html#jupyterhub.services.auth.HubAuthenticated
|
||||
[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
|
||||
|
@@ -1,10 +1,10 @@
|
||||
# Configuration file for jupyterhub (postgres example).
|
||||
|
||||
c = get_config()
|
||||
c = get_config() # noqa
|
||||
|
||||
# Add some users.
|
||||
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
|
||||
# container.
|
||||
|
@@ -6,15 +6,17 @@ that appear when JupyterHub renders pages.
|
||||
To run the service as a hub-managed service simply include in your JupyterHub
|
||||
configuration file something like:
|
||||
|
||||
```python
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'announcement',
|
||||
'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.
|
||||
|
||||
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
|
||||
|
||||
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>" \
|
||||
-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:
|
||||
|
||||
$ curl https://.../services/announcement | python -m json.tool
|
||||
$ curl https://.../services/announcement/ | python -m json.tool
|
||||
{
|
||||
announcement: "JupyterHub will be upgraded on August 14!",
|
||||
timestamp: "...",
|
||||
@@ -41,10 +56,11 @@ Anyone can read the announcement:
|
||||
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.
|
||||
|
||||
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>" \
|
||||
https://.../services/announcement
|
||||
$ curl -X DELETE -H "Authorization: token <token>" \
|
||||
https://.../services/announcement/
|
||||
|
||||
## Seeing the Announcement in JupyterHub
|
||||
|
||||
|
@@ -13,9 +13,6 @@ from jupyterhub.services.auth import HubAuthenticated
|
||||
class AnnouncementRequestHandler(HubAuthenticated, web.RequestHandler):
|
||||
"""Dynamically manage page announcements"""
|
||||
|
||||
hub_users = []
|
||||
allow_admin = True
|
||||
|
||||
def initialize(self, storage):
|
||||
"""Create storage for announcement text"""
|
||||
self.storage = storage
|
||||
|
@@ -2,11 +2,18 @@ import sys
|
||||
|
||||
# To run the announcement service managed by the hub, add this.
|
||||
|
||||
port = 9999
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'announcement',
|
||||
'url': 'http://127.0.0.1:8888',
|
||||
'command': [sys.executable, "-m", "announcement"],
|
||||
'url': f'http://127.0.0.1:{port}',
|
||||
'command': [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"announcement",
|
||||
'--port',
|
||||
str(port),
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
@@ -14,3 +21,19 @@ c.JupyterHub.services = [
|
||||
# for an example of how to do this.
|
||||
|
||||
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
|
||||
|
@@ -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
|
||||
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.
|
||||
|
||||
@@ -24,10 +25,10 @@ $ curl -X GET http://127.0.0.1:8000/services/fastapi/
|
||||
{"Hello":"World"}
|
||||
|
||||
$ 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 \
|
||||
-d '{"username": "myname", "password": "mypasswd!"}' \
|
||||
$ curl -X POST http://127.0.0.1:8000/hub/api/users/test-user/tokens \
|
||||
-d '{"auth": {"username": "test-user", "password": "mypasswd!"}}' \
|
||||
| jq '.token'
|
||||
"3fee13ce6d2845da9bd5f2c2170d3428"
|
||||
|
||||
@@ -35,13 +36,18 @@ $ curl -X GET http://127.0.0.1:8000/services/fastapi/me \
|
||||
-H "Authorization: Bearer 3fee13ce6d2845da9bd5f2c2170d3428" \
|
||||
| jq .
|
||||
{
|
||||
"name": "myname",
|
||||
"name": "test-user",
|
||||
"admin": false,
|
||||
"groups": [],
|
||||
"server": null,
|
||||
"pending": null,
|
||||
"last_activity": "2021-04-07T18:05:11.587638+00:00",
|
||||
"servers": null
|
||||
"last_activity": "2021-05-21T09:13:00.514309+00:00",
|
||||
"servers": null,
|
||||
"scopes": [
|
||||
"access:services",
|
||||
"access:users:servers!user=test-user",
|
||||
"...",
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from typing import Dict
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
|
||||
@@ -22,11 +23,12 @@ class Server(BaseModel):
|
||||
class User(BaseModel):
|
||||
name: str
|
||||
admin: bool
|
||||
groups: List[str]
|
||||
groups: Optional[List[str]]
|
||||
server: Optional[str]
|
||||
pending: Optional[str]
|
||||
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
|
||||
|
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from fastapi import HTTPException
|
||||
@@ -27,6 +28,12 @@ auth_by_header = OAuth2AuthorizationCodeBearer(
|
||||
### our client_secret (JUPYTERHUB_API_TOKEN) and that code to get an
|
||||
### 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
|
||||
### always hitting the Hub api?
|
||||
async def get_current_user(
|
||||
@@ -58,4 +65,15 @@ async def get_current_user(
|
||||
},
|
||||
)
|
||||
user = User(**resp.json())
|
||||
if any(scope in user.scopes for scope in access_scopes):
|
||||
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(),
|
||||
},
|
||||
)
|
||||
|
@@ -24,8 +24,21 @@ c.JupyterHub.services = [
|
||||
"name": service_name,
|
||||
"url": "http://127.0.0.1:10202",
|
||||
"command": ["uvicorn", "app:app", "--port", "10202"],
|
||||
"admin": True,
|
||||
"oauth_redirect_uri": oauth_redirect_uri,
|
||||
"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"
|
||||
|
@@ -1,33 +1,35 @@
|
||||
# our user list
|
||||
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 = [
|
||||
{
|
||||
"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"],
|
||||
},
|
||||
]
|
||||
c.JupyterHub.load_groups = {group_name: ['ellisonbg', 'willingc']}
|
||||
|
||||
# start the notebook server as a service
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'shared-notebook',
|
||||
'url': 'http://127.0.0.1:9999',
|
||||
'name': service_name,
|
||||
'url': 'http://127.0.0.1:{}'.format(service_port),
|
||||
'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!
|
||||
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
|
||||
|
@@ -1,19 +1,35 @@
|
||||
# our user list
|
||||
c.Authenticator.whitelist = ['minrk', 'ellisonbg', 'willingc']
|
||||
|
||||
# ellisonbg and willingc have access to a shared server:
|
||||
|
||||
c.JupyterHub.load_groups = {'shared': ['ellisonbg', 'willingc']}
|
||||
c.Authenticator.allowed_users = ['minrk', 'ellisonbg', 'willingc']
|
||||
|
||||
service_name = 'shared-notebook'
|
||||
service_port = 9999
|
||||
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
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': service_name,
|
||||
'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
|
||||
|
@@ -2,15 +2,15 @@
|
||||
|
||||
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
|
||||
|
||||
1. Launch JupyterHub and the `whoami service` with
|
||||
1. Launch JupyterHub and the `whoami` services with
|
||||
|
||||
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:
|
||||
|
||||
@@ -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)).
|
||||
|
||||
You may set the `hub_users` configuration in the service script
|
||||
to restrict access to the service to a whitelist of allowed users.
|
||||
By default, any authenticated user is allowed.
|
||||
To govern access to the services, create **roles** with the scope `access:services!service=$service-name`,
|
||||
and assign users to the scope.
|
||||
|
||||
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:
|
||||
|
||||
JUPYTERHUB_API_TOKEN
|
||||
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.
|
||||
|
@@ -2,7 +2,7 @@ import sys
|
||||
|
||||
c.JupyterHub.services = [
|
||||
{
|
||||
'name': 'whoami',
|
||||
'name': 'whoami-api',
|
||||
'url': 'http://127.0.0.1:10101',
|
||||
'command': [sys.executable, './whoami.py'],
|
||||
},
|
||||
@@ -12,3 +12,16 @@ c.JupyterHub.services = [
|
||||
'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
|
||||
|
@@ -1,6 +1,6 @@
|
||||
"""An example service authenticating with the Hub.
|
||||
|
||||
This example service serves `/services/whoami/`,
|
||||
This example service serves `/services/whoami-oauth/`,
|
||||
authenticated with the Hub,
|
||||
showing the user their own info.
|
||||
"""
|
||||
@@ -20,13 +20,6 @@ from jupyterhub.utils import url_path_join
|
||||
|
||||
|
||||
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
|
||||
def get(self):
|
||||
user_model = self.get_current_user()
|
||||
|
@@ -1,6 +1,8 @@
|
||||
"""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 os
|
||||
@@ -16,13 +18,6 @@ from jupyterhub.services.auth import HubAuthenticated
|
||||
|
||||
|
||||
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
|
||||
def get(self):
|
||||
user_model = self.get_current_user()
|
||||
|
@@ -824,13 +824,26 @@ class UserNotAllowed(Exception):
|
||||
)
|
||||
|
||||
|
||||
class HubAuthenticated(object):
|
||||
class HubAuthenticated:
|
||||
"""Mixin for tornado handlers that are authenticated with JupyterHub
|
||||
|
||||
A handler that mixes this in must have the following attributes/properties:
|
||||
|
||||
- .hub_auth: A HubAuth instance
|
||||
- .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.
|
||||
If left unspecified or None, username will not be checked.
|
||||
- .hub_groups: A set of group names to allow.
|
||||
@@ -943,6 +956,8 @@ class HubAuthenticated(object):
|
||||
# note: this means successful authentication, but insufficient permission
|
||||
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):
|
||||
app_log.debug("Allowing Hub admin %s", name)
|
||||
return model
|
||||
|
Reference in New Issue
Block a user