mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-07 10:04:07 +00:00
205 lines
6.9 KiB
Python
205 lines
6.9 KiB
Python
"""Service handlers
|
|
|
|
Currently GET-only, no actions can be taken to modify services.
|
|
"""
|
|
|
|
# Copyright (c) Jupyter Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
import json
|
|
from typing import Optional, Tuple
|
|
|
|
from tornado import web
|
|
|
|
from .. import orm
|
|
from ..roles import get_default_roles
|
|
from ..scopes import Scope, _check_token_scopes, needs_scope
|
|
from ..services.service import Service
|
|
from .base import APIHandler
|
|
|
|
|
|
class ServiceListAPIHandler(APIHandler):
|
|
@needs_scope('list:services')
|
|
def get(self):
|
|
data = {}
|
|
service_scope = self.parsed_scopes['list:services']
|
|
for name, service in self.services.items():
|
|
if service_scope == Scope.ALL or name in service_scope.get("service", {}):
|
|
model = self.service_model(service)
|
|
data[name] = model
|
|
self.write(json.dumps(data))
|
|
|
|
|
|
class ServiceAPIHandler(APIHandler):
|
|
@needs_scope('read:services', 'read:services:name', 'read:roles:services')
|
|
def get(self, service_name):
|
|
if service_name not in self.services:
|
|
raise web.HTTPError(404, f"No such service: {service_name}")
|
|
service = self.services[service_name]
|
|
self.write(json.dumps(self.service_model(service)))
|
|
|
|
def _check_service_scopes(self, spec: dict):
|
|
user = self.current_user
|
|
requested_scopes = []
|
|
if spec.get('admin'):
|
|
default_roles = get_default_roles()
|
|
admin_scopes = [
|
|
role['scopes'] for role in default_roles if role['name'] == 'admin'
|
|
]
|
|
requested_scopes.extend(admin_scopes[0])
|
|
|
|
requested_client_scope = spec.get('oauth_client_allowed_scopes')
|
|
if requested_client_scope is not None:
|
|
requested_scopes.extend(requested_client_scope)
|
|
if len(requested_scopes) > 0:
|
|
try:
|
|
_check_token_scopes(requested_scopes, user, None)
|
|
except ValueError as e:
|
|
raise web.HTTPError(400, str(e))
|
|
|
|
async def add_service(self, spec: dict) -> Service:
|
|
"""Add a new service and related objects to the database
|
|
|
|
Args:
|
|
spec (dict): The service specification
|
|
|
|
Raises:
|
|
web.HTTPError: Raise if the service is not created
|
|
|
|
Returns:
|
|
Service: Returns the service instance.
|
|
|
|
"""
|
|
|
|
self._check_service_model(spec)
|
|
self._check_service_scopes(spec)
|
|
|
|
service_name = spec["name"]
|
|
managed = bool(spec.get('command'))
|
|
if managed:
|
|
msg = f"Can not create managed service {service_name} at runtime"
|
|
self.log.error(msg, exc_info=True)
|
|
raise web.HTTPError(400, msg)
|
|
try:
|
|
new_service = self.service_from_spec(spec)
|
|
except Exception:
|
|
msg = f"Failed to create service {service_name}"
|
|
self.log.error(msg, exc_info=True)
|
|
raise web.HTTPError(400, msg)
|
|
|
|
if new_service is None:
|
|
raise web.HTTPError(400, f"Failed to create service {service_name}")
|
|
|
|
if new_service.api_token:
|
|
# Add api token to database
|
|
await self.app._add_tokens(
|
|
{new_service.api_token: new_service.name}, kind='service'
|
|
)
|
|
if new_service.url:
|
|
# Start polling for external service
|
|
service_status = await self.app.start_service(service_name, new_service)
|
|
if not service_status:
|
|
self.log.error(
|
|
'Failed to start service %s',
|
|
service_name,
|
|
exc_info=True,
|
|
)
|
|
|
|
if new_service.oauth_no_confirm:
|
|
oauth_no_confirm_list = self.settings.get('oauth_no_confirm_list')
|
|
msg = f"Allowing service {new_service.name} to complete OAuth without confirmation on an authorization web page"
|
|
self.log.warning(msg)
|
|
oauth_no_confirm_list.add(new_service.oauth_client_id)
|
|
|
|
return new_service
|
|
|
|
@needs_scope('admin:services')
|
|
async def post(self, service_name: str):
|
|
data = self.get_json_body()
|
|
service, _ = self.find_service(service_name)
|
|
|
|
if service is not None:
|
|
raise web.HTTPError(409, f"Service {service_name} already exists")
|
|
|
|
if not data or not isinstance(data, dict):
|
|
raise web.HTTPError(400, "Invalid service data")
|
|
|
|
data['name'] = service_name
|
|
new_service = await self.add_service(data)
|
|
self.write(json.dumps(self.service_model(new_service)))
|
|
self.set_status(201)
|
|
|
|
@needs_scope('admin:services')
|
|
async def delete(self, service_name: str):
|
|
service, orm_service = self.find_service(service_name)
|
|
|
|
if service is None:
|
|
raise web.HTTPError(404, f"Service {service_name} does not exist")
|
|
|
|
if service.from_config:
|
|
raise web.HTTPError(
|
|
405, f"Service {service_name} is not modifiable at runtime"
|
|
)
|
|
try:
|
|
await self.remove_service(service, orm_service)
|
|
self.services.pop(service_name)
|
|
except Exception:
|
|
msg = f"Failed to remove service {service_name}"
|
|
self.log.error(msg, exc_info=True)
|
|
raise web.HTTPError(400, msg)
|
|
|
|
self.set_status(200)
|
|
|
|
async def remove_service(self, service: Service, orm_service: orm.Service) -> None:
|
|
"""Remove a service and all related objects from the database.
|
|
|
|
Args:
|
|
service (Service): the service object to be removed
|
|
|
|
orm_service (orm.Service): The `orm.Service` object linked
|
|
with `service`
|
|
"""
|
|
if service.managed:
|
|
await service.stop()
|
|
|
|
if service.oauth_client:
|
|
self.oauth_provider.remove_client(service.oauth_client_id)
|
|
|
|
if orm_service._server_id is not None:
|
|
orm_server = (
|
|
self.db.query(orm.Server).filter_by(id=orm_service._server_id).first()
|
|
)
|
|
if orm_server is not None:
|
|
self.db.delete(orm_server)
|
|
|
|
if service.oauth_no_confirm:
|
|
oauth_no_confirm_list = self.settings.get('oauth_no_confirm_list')
|
|
oauth_no_confirm_list.discard(service.oauth_client_id)
|
|
|
|
self.db.delete(orm_service)
|
|
self.db.commit()
|
|
|
|
def service_from_spec(self, spec) -> Optional[Service]:
|
|
"""Create service from api request"""
|
|
service = self.app.service_from_spec(spec, from_config=False)
|
|
self.db.commit()
|
|
return service
|
|
|
|
def find_service(
|
|
self, name: str
|
|
) -> Tuple[Optional[Service], Optional[orm.Service]]:
|
|
"""Get a service by name
|
|
return None if no such service
|
|
"""
|
|
orm_service = orm.Service.find(db=self.db, name=name)
|
|
if orm_service is not None:
|
|
service = self.services.get(name)
|
|
return service, orm_service
|
|
|
|
return (None, None)
|
|
|
|
|
|
default_handlers = [
|
|
(r"/api/services", ServiceListAPIHandler),
|
|
(r"/api/services/([^/]+)", ServiceAPIHandler),
|
|
]
|