handlers.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. """Tornado handlers for api specifications."""
  2. # Copyright (c) Jupyter Development Team.
  3. # Distributed under the terms of the Modified BSD License.
  4. import json
  5. import os
  6. from typing import Any, cast
  7. from jupyter_core.utils import ensure_async
  8. from tornado import web
  9. from jupyter_server._tz import isoformat, utcfromtimestamp
  10. from jupyter_server.auth.decorator import authorized
  11. from jupyter_server.auth.identity import IdentityProvider, UpdatableField
  12. from ...base.handlers import APIHandler, JupyterHandler
  13. AUTH_RESOURCE = "api"
  14. class APISpecHandler(web.StaticFileHandler, JupyterHandler):
  15. """A spec handler for the REST API."""
  16. auth_resource = AUTH_RESOURCE
  17. def initialize(self):
  18. """Initialize the API spec handler."""
  19. web.StaticFileHandler.initialize(self, path=os.path.dirname(__file__))
  20. @web.authenticated
  21. @authorized
  22. def head(self):
  23. return self.get("api.yaml", include_body=False)
  24. @web.authenticated
  25. @authorized
  26. def get(self):
  27. """Get the API spec."""
  28. self.log.warning("Serving api spec (experimental, incomplete)")
  29. return web.StaticFileHandler.get(self, "api.yaml")
  30. def get_content_type(self):
  31. """Get the content type."""
  32. return "text/x-yaml"
  33. class APIStatusHandler(APIHandler):
  34. """An API status handler."""
  35. auth_resource = AUTH_RESOURCE
  36. _track_activity = False
  37. @web.authenticated
  38. @authorized
  39. async def get(self):
  40. """Get the API status."""
  41. # if started was missing, use unix epoch
  42. started = self.settings.get("started", utcfromtimestamp(0))
  43. started = isoformat(started)
  44. kernels = await ensure_async(self.kernel_manager.list_kernels())
  45. total_connections = sum(k["connections"] for k in kernels)
  46. last_activity = isoformat(self.application.last_activity()) # type:ignore[attr-defined]
  47. model = {
  48. "started": started,
  49. "last_activity": last_activity,
  50. "kernels": len(kernels),
  51. "connections": total_connections,
  52. }
  53. self.finish(json.dumps(model, sort_keys=True))
  54. class IdentityHandler(APIHandler):
  55. """Get or patch the current user's identity model"""
  56. @web.authenticated
  57. async def get(self):
  58. """Get the identity model."""
  59. permissions_json: str = self.get_argument("permissions", "")
  60. bad_permissions_msg = f'permissions should be a JSON dict of {{"resource": ["action",]}}, got {permissions_json!r}'
  61. if permissions_json:
  62. try:
  63. permissions_to_check = json.loads(permissions_json)
  64. except ValueError as e:
  65. raise web.HTTPError(400, bad_permissions_msg) from e
  66. if not isinstance(permissions_to_check, dict):
  67. raise web.HTTPError(400, bad_permissions_msg)
  68. else:
  69. permissions_to_check = {}
  70. permissions: dict[str, list[str]] = {}
  71. user = self.current_user
  72. for resource, actions in permissions_to_check.items():
  73. if (
  74. not isinstance(resource, str)
  75. or not isinstance(actions, list)
  76. or not all(isinstance(action, str) for action in actions)
  77. ):
  78. raise web.HTTPError(400, bad_permissions_msg)
  79. allowed = permissions[resource] = []
  80. for action in actions:
  81. authorized = await ensure_async(
  82. self.authorizer.is_authorized(self, user, action, resource)
  83. )
  84. if authorized:
  85. allowed.append(action)
  86. # Add permission to user to update their own identity
  87. permissions["updatable_fields"] = self.identity_provider.updatable_fields
  88. identity: dict[str, Any] = self.identity_provider.identity_model(user)
  89. model = {
  90. "identity": identity,
  91. "permissions": permissions,
  92. }
  93. self.write(json.dumps(model))
  94. @web.authenticated
  95. async def patch(self):
  96. """Update user information."""
  97. user_data = cast(dict[UpdatableField, str], self.get_json_body())
  98. if not user_data:
  99. raise web.HTTPError(400, "Invalid or missing JSON body")
  100. # Update user information
  101. identity_provider = self.settings["identity_provider"]
  102. if not isinstance(identity_provider, IdentityProvider):
  103. raise web.HTTPError(500, "Identity provider not configured properly")
  104. try:
  105. updated_user = identity_provider.update_user(self, user_data)
  106. self.write(
  107. {"status": "success", "identity": identity_provider.identity_model(updated_user)}
  108. )
  109. except ValueError as e:
  110. raise web.HTTPError(400, str(e)) from e
  111. except NotImplementedError as e:
  112. raise web.HTTPError(501, str(e)) from e
  113. default_handlers = [
  114. (r"/api/spec.yaml", APISpecHandler),
  115. (r"/api/status", APIStatusHandler),
  116. (r"/api/me", IdentityHandler),
  117. ]