14
14
import warnings
15
15
from http .client import responses
16
16
from logging import Logger
17
- from typing import TYPE_CHECKING , Any , Awaitable , Sequence , cast
17
+ from typing import TYPE_CHECKING , Any , Awaitable , Coroutine , Sequence , cast
18
18
from urllib .parse import urlparse
19
19
20
20
import prometheus_client
29
29
from jupyter_server import CallContext
30
30
from jupyter_server ._sysinfo import get_sys_info
31
31
from jupyter_server ._tz import utcnow
32
- from jupyter_server .auth .decorator import authorized
32
+ from jupyter_server .auth .decorator import allow_unauthenticated , authorized
33
33
from jupyter_server .auth .identity import User
34
34
from jupyter_server .i18n import combine_translations
35
35
from jupyter_server .services .security import csp_report_uri
@@ -589,7 +589,7 @@ def check_host(self) -> bool:
589
589
)
590
590
return allow
591
591
592
- async def prepare (self ) -> Awaitable [None ] | None : # type:ignore[override]
592
+ async def prepare (self , * , _redirect_to_login = True ) -> Awaitable [None ] | None : # type:ignore[override]
593
593
"""Prepare a response."""
594
594
# Set the current Jupyter Handler context variable.
595
595
CallContext .set (CallContext .JUPYTER_HANDLER , self )
@@ -630,6 +630,25 @@ async def prepare(self) -> Awaitable[None] | None: # type:ignore[override]
630
630
self .set_cors_headers ()
631
631
if self .request .method not in {"GET" , "HEAD" , "OPTIONS" }:
632
632
self .check_xsrf_cookie ()
633
+
634
+ if not self .settings .get ("allow_unauthenticated_access" , False ):
635
+ if not self .request .method :
636
+ raise HTTPError (403 )
637
+ method = getattr (self , self .request .method .lower ())
638
+ if not getattr (method , "__allow_unauthenticated" , False ):
639
+ if _redirect_to_login :
640
+ # reuse `web.authenticated` logic, which redirects to the login
641
+ # page on GET and HEAD and otherwise raises 403
642
+ return web .authenticated (lambda _ : super ().prepare ())(self )
643
+ else :
644
+ # raise 403 if user is not known without redirecting to login page
645
+ user = self .current_user
646
+ if user is None :
647
+ self .log .warning (
648
+ f"Couldn't authenticate { self .__class__ .__name__ } connection"
649
+ )
650
+ raise web .HTTPError (403 )
651
+
633
652
return super ().prepare ()
634
653
635
654
# ---------------------------------------------------------------
@@ -726,7 +745,7 @@ def write_error(self, status_code: int, **kwargs: Any) -> None:
726
745
class APIHandler (JupyterHandler ):
727
746
"""Base class for API handlers"""
728
747
729
- async def prepare (self ) -> None :
748
+ async def prepare (self ) -> None : # type:ignore[override]
730
749
"""Prepare an API response."""
731
750
await super ().prepare ()
732
751
if not self .check_origin ():
@@ -794,6 +813,7 @@ def finish(self, *args: Any, **kwargs: Any) -> Future[Any]:
794
813
self .set_header ("Content-Type" , set_content_type )
795
814
return super ().finish (* args , ** kwargs )
796
815
816
+ @allow_unauthenticated
797
817
def options (self , * args : Any , ** kwargs : Any ) -> None :
798
818
"""Get the options."""
799
819
if "Access-Control-Allow-Headers" in self .settings .get ("headers" , {}):
@@ -837,7 +857,7 @@ def options(self, *args: Any, **kwargs: Any) -> None:
837
857
class Template404 (JupyterHandler ):
838
858
"""Render our 404 template"""
839
859
840
- async def prepare (self ) -> None :
860
+ async def prepare (self ) -> None : # type:ignore[override]
841
861
"""Prepare a 404 response."""
842
862
await super ().prepare ()
843
863
raise web .HTTPError (404 )
@@ -1002,6 +1022,18 @@ def compute_etag(self) -> str | None:
1002
1022
"""Compute the etag."""
1003
1023
return None
1004
1024
1025
+ # access is allowed as this class is used to serve static assets on login page
1026
+ # TODO: create an allow-list of files used on login page and remove this decorator
1027
+ @allow_unauthenticated
1028
+ def get (self , path : str , include_body : bool = True ) -> Coroutine [Any , Any , None ]:
1029
+ return super ().get (path , include_body )
1030
+
1031
+ # access is allowed as this class is used to serve static assets on login page
1032
+ # TODO: create an allow-list of files used on login page and remove this decorator
1033
+ @allow_unauthenticated
1034
+ def head (self , path : str ) -> Awaitable [None ]:
1035
+ return super ().head (path )
1036
+
1005
1037
@classmethod
1006
1038
def get_absolute_path (cls , roots : Sequence [str ], path : str ) -> str :
1007
1039
"""locate a file to serve on our static file search path"""
@@ -1036,6 +1068,7 @@ class APIVersionHandler(APIHandler):
1036
1068
1037
1069
_track_activity = False
1038
1070
1071
+ @allow_unauthenticated
1039
1072
def get (self ) -> None :
1040
1073
"""Get the server version info."""
1041
1074
# not authenticated, so give as few info as possible
@@ -1048,6 +1081,7 @@ class TrailingSlashHandler(web.RequestHandler):
1048
1081
This should be the first, highest priority handler.
1049
1082
"""
1050
1083
1084
+ @allow_unauthenticated
1051
1085
def get (self ) -> None :
1052
1086
"""Handle trailing slashes in a get."""
1053
1087
assert self .request .uri is not None
@@ -1064,6 +1098,7 @@ def get(self) -> None:
1064
1098
class MainHandler (JupyterHandler ):
1065
1099
"""Simple handler for base_url."""
1066
1100
1101
+ @allow_unauthenticated
1067
1102
def get (self ) -> None :
1068
1103
"""Get the main template."""
1069
1104
html = self .render_template ("main.html" )
@@ -1104,18 +1139,20 @@ async def redirect_to_files(self: Any, path: str) -> None:
1104
1139
self .log .debug ("Redirecting %s to %s" , self .request .path , url )
1105
1140
self .redirect (url )
1106
1141
1142
+ @allow_unauthenticated
1107
1143
async def get (self , path : str = "" ) -> None :
1108
1144
return await self .redirect_to_files (self , path )
1109
1145
1110
1146
1111
1147
class RedirectWithParams (web .RequestHandler ):
1112
- """Sam as web.RedirectHandler, but preserves URL parameters"""
1148
+ """Same as web.RedirectHandler, but preserves URL parameters"""
1113
1149
1114
1150
def initialize (self , url : str , permanent : bool = True ) -> None :
1115
1151
"""Initialize a redirect handler."""
1116
1152
self ._url = url
1117
1153
self ._permanent = permanent
1118
1154
1155
+ @allow_unauthenticated
1119
1156
def get (self ) -> None :
1120
1157
"""Get a redirect."""
1121
1158
sep = "&" if "?" in self ._url else "?"
@@ -1128,6 +1165,7 @@ class PrometheusMetricsHandler(JupyterHandler):
1128
1165
Return prometheus metrics for this server
1129
1166
"""
1130
1167
1168
+ @allow_unauthenticated
1131
1169
def get (self ) -> None :
1132
1170
"""Get prometheus metrics."""
1133
1171
if self .settings ["authenticate_prometheus" ] and not self .logged_in :
@@ -1137,6 +1175,18 @@ def get(self) -> None:
1137
1175
self .write (prometheus_client .generate_latest (prometheus_client .REGISTRY ))
1138
1176
1139
1177
1178
+ class PublicStaticFileHandler (web .StaticFileHandler ):
1179
+ """Same as web.StaticFileHandler, but decorated to acknowledge that auth is not required."""
1180
+
1181
+ @allow_unauthenticated
1182
+ def head (self , path : str ) -> Awaitable [None ]:
1183
+ return super ().head (path )
1184
+
1185
+ @allow_unauthenticated
1186
+ def get (self , path : str , include_body : bool = True ) -> Coroutine [Any , Any , None ]:
1187
+ return super ().get (path , include_body )
1188
+
1189
+
1140
1190
# -----------------------------------------------------------------------------
1141
1191
# URL pattern fragments for reuse
1142
1192
# -----------------------------------------------------------------------------
@@ -1152,6 +1202,6 @@ def get(self) -> None:
1152
1202
default_handlers = [
1153
1203
(r".*/" , TrailingSlashHandler ),
1154
1204
(r"api" , APIVersionHandler ),
1155
- (r"/(robots\.txt|favicon\.ico)" , web . StaticFileHandler ),
1205
+ (r"/(robots\.txt|favicon\.ico)" , PublicStaticFileHandler ),
1156
1206
(r"/metrics" , PrometheusMetricsHandler ),
1157
1207
]
0 commit comments