13
13
import os
14
14
import re
15
15
import sys
16
+ import typing as t
16
17
import uuid
17
18
from dataclasses import asdict , dataclass
18
19
from http .cookies import Morsel
19
- from typing import TYPE_CHECKING , Any , Awaitable
20
20
21
21
from tornado import escape , httputil , web
22
22
from traitlets import Bool , Dict , Type , Unicode , default
27
27
from .security import passwd_check , set_password
28
28
from .utils import get_anonymous_username
29
29
30
- # circular imports for type checking
31
- if TYPE_CHECKING :
32
- from jupyter_server .base .handlers import AuthenticatedHandler , JupyterHandler
33
- from jupyter_server .serverapp import ServerApp
34
-
35
30
_non_alphanum = re .compile (r"[^A-Za-z0-9]" )
36
31
37
32
@@ -82,7 +77,7 @@ def fill_defaults(self):
82
77
self .display_name = self .name
83
78
84
79
85
- def _backward_compat_user (got_user : Any ) -> User :
80
+ def _backward_compat_user (got_user : t . Any ) -> User :
86
81
"""Backward-compatibility for LoginHandler.get_user
87
82
88
83
Prior to 2.0, LoginHandler.get_user could return anything truthy.
@@ -128,7 +123,7 @@ class IdentityProvider(LoggingConfigurable):
128
123
.. versionadded:: 2.0
129
124
"""
130
125
131
- cookie_name : str | Unicode = Unicode (
126
+ cookie_name : str | Unicode [ str , str | bytes ] = Unicode (
132
127
"" ,
133
128
config = True ,
134
129
help = _i18n ("Name of the cookie to set for persisting login. Default: username-${Host}." ),
@@ -142,7 +137,7 @@ class IdentityProvider(LoggingConfigurable):
142
137
),
143
138
)
144
139
145
- secure_cookie : bool | Bool = Bool (
140
+ secure_cookie : bool | Bool [ bool | None , bool | int | None ] = Bool (
146
141
None ,
147
142
allow_none = True ,
148
143
config = True ,
@@ -160,7 +155,7 @@ class IdentityProvider(LoggingConfigurable):
160
155
),
161
156
)
162
157
163
- token : str | Unicode = Unicode (
158
+ token : str | Unicode [ str , str | bytes ] = Unicode (
164
159
"<generated>" ,
165
160
help = _i18n (
166
161
"""Token used for authenticating first-time connections to the server.
@@ -211,9 +206,9 @@ def _token_default(self):
211
206
self .token_generated = True
212
207
return binascii .hexlify (os .urandom (24 )).decode ("ascii" )
213
208
214
- need_token : bool | Bool = Bool (True )
209
+ need_token : bool | Bool [ bool , t . Union [ bool , int ]] = Bool (True )
215
210
216
- def get_user (self , handler : JupyterHandler ) -> User | None | Awaitable [User | None ]:
211
+ def get_user (self , handler : web . RequestHandler ) -> User | None | t . Awaitable [User | None ]:
217
212
"""Get the authenticated user for a request
218
213
219
214
Must return a :class:`jupyter_server.auth.User`,
@@ -228,17 +223,17 @@ def get_user(self, handler: JupyterHandler) -> User | None | Awaitable[User | No
228
223
# not sure how to have optional-async type signature
229
224
# on base class with `async def` without splitting it into two methods
230
225
231
- async def _get_user (self , handler : JupyterHandler ) -> User | None :
226
+ async def _get_user (self , handler : web . RequestHandler ) -> User | None :
232
227
"""Get the user."""
233
228
if getattr (handler , "_jupyter_current_user" , None ):
234
229
# already authenticated
235
- return handler ._jupyter_current_user
236
- _token_user : User | None | Awaitable [User | None ] = self .get_user_token (handler )
237
- if isinstance (_token_user , Awaitable ):
230
+ return t . cast ( User , handler ._jupyter_current_user ) # type:ignore[attr-defined]
231
+ _token_user : User | None | t . Awaitable [User | None ] = self .get_user_token (handler )
232
+ if isinstance (_token_user , t . Awaitable ):
238
233
_token_user = await _token_user
239
234
token_user : User | None = _token_user # need second variable name to collapse type
240
235
_cookie_user = self .get_user_cookie (handler )
241
- if isinstance (_cookie_user , Awaitable ):
236
+ if isinstance (_cookie_user , t . Awaitable ):
242
237
_cookie_user = await _cookie_user
243
238
cookie_user : User | None = _cookie_user
244
239
# prefer token to cookie if both given,
@@ -273,12 +268,12 @@ async def _get_user(self, handler: JupyterHandler) -> User | None:
273
268
274
269
return user
275
270
276
- def identity_model (self , user : User ) -> dict :
271
+ def identity_model (self , user : User ) -> dict [ str , t . Any ] :
277
272
"""Return a User as an Identity model"""
278
273
# TODO: validate?
279
274
return asdict (user )
280
275
281
- def get_handlers (self ) -> list :
276
+ def get_handlers (self ) -> list [ tuple [ str , object ]] :
282
277
"""Return list of additional handlers for this identity provider
283
278
284
279
For example, an OAuth callback handler.
@@ -321,7 +316,7 @@ def user_from_cookie(self, cookie_value: str) -> User | None:
321
316
user ["color" ],
322
317
)
323
318
324
- def get_cookie_name (self , handler : AuthenticatedHandler ) -> str :
319
+ def get_cookie_name (self , handler : web . RequestHandler ) -> str :
325
320
"""Return the login cookie name
326
321
327
322
Uses IdentityProvider.cookie_name, if defined.
@@ -333,7 +328,7 @@ def get_cookie_name(self, handler: AuthenticatedHandler) -> str:
333
328
else :
334
329
return _non_alphanum .sub ("-" , f"username-{ handler .request .host } " )
335
330
336
- def set_login_cookie (self , handler : AuthenticatedHandler , user : User ) -> None :
331
+ def set_login_cookie (self , handler : web . RequestHandler , user : User ) -> None :
337
332
"""Call this on handlers to set the login cookie for success"""
338
333
cookie_options = {}
339
334
cookie_options .update (self .cookie_options )
@@ -345,12 +340,12 @@ def set_login_cookie(self, handler: AuthenticatedHandler, user: User) -> None:
345
340
secure_cookie = handler .request .protocol == "https"
346
341
if secure_cookie :
347
342
cookie_options .setdefault ("secure" , True )
348
- cookie_options .setdefault ("path" , handler .base_url )
343
+ cookie_options .setdefault ("path" , handler .base_url ) # type:ignore[attr-defined]
349
344
cookie_name = self .get_cookie_name (handler )
350
345
handler .set_secure_cookie (cookie_name , self .user_to_cookie (user ), ** cookie_options )
351
346
352
347
def _force_clear_cookie (
353
- self , handler : AuthenticatedHandler , name : str , path : str = "/" , domain : str | None = None
348
+ self , handler : web . RequestHandler , name : str , path : str = "/" , domain : str | None = None
354
349
) -> None :
355
350
"""Deletes the cookie with the given name.
356
351
@@ -368,19 +363,19 @@ def _force_clear_cookie(
368
363
name = escape .native_str (name )
369
364
expires = datetime .datetime .now (tz = datetime .timezone .utc ) - datetime .timedelta (days = 365 )
370
365
371
- morsel : Morsel = Morsel ()
366
+ morsel : Morsel [ t . Any ] = Morsel ()
372
367
morsel .set (name , "" , '""' )
373
368
morsel ["expires" ] = httputil .format_timestamp (expires )
374
369
morsel ["path" ] = path
375
370
if domain :
376
371
morsel ["domain" ] = domain
377
372
handler .add_header ("Set-Cookie" , morsel .OutputString ())
378
373
379
- def clear_login_cookie (self , handler : AuthenticatedHandler ) -> None :
374
+ def clear_login_cookie (self , handler : web . RequestHandler ) -> None :
380
375
"""Clear the login cookie, effectively logging out the session."""
381
376
cookie_options = {}
382
377
cookie_options .update (self .cookie_options )
383
- path = cookie_options .setdefault ("path" , handler .base_url )
378
+ path = cookie_options .setdefault ("path" , handler .base_url ) # type:ignore[attr-defined]
384
379
cookie_name = self .get_cookie_name (handler )
385
380
handler .clear_cookie (cookie_name , path = path )
386
381
if path and path != "/" :
@@ -390,7 +385,9 @@ def clear_login_cookie(self, handler: AuthenticatedHandler) -> None:
390
385
# two cookies with the same name. See the method above.
391
386
self ._force_clear_cookie (handler , cookie_name )
392
387
393
- def get_user_cookie (self , handler : JupyterHandler ) -> User | None | Awaitable [User | None ]:
388
+ def get_user_cookie (
389
+ self , handler : web .RequestHandler
390
+ ) -> User | None | t .Awaitable [User | None ]:
394
391
"""Get user from a cookie
395
392
396
393
Calls user_from_cookie to deserialize cookie value
@@ -413,7 +410,7 @@ def get_user_cookie(self, handler: JupyterHandler) -> User | None | Awaitable[Us
413
410
414
411
auth_header_pat = re .compile (r"(token|bearer)\s+(.+)" , re .IGNORECASE )
415
412
416
- def get_token (self , handler : JupyterHandler ) -> str | None :
413
+ def get_token (self , handler : web . RequestHandler ) -> str | None :
417
414
"""Get the user token from a request
418
415
419
416
Default:
@@ -429,14 +426,14 @@ def get_token(self, handler: JupyterHandler) -> str | None:
429
426
user_token = m .group (2 )
430
427
return user_token
431
428
432
- async def get_user_token (self , handler : JupyterHandler ) -> User | None :
429
+ async def get_user_token (self , handler : web . RequestHandler ) -> User | None :
433
430
"""Identify the user based on a token in the URL or Authorization header
434
431
435
432
Returns:
436
433
- uuid if authenticated
437
434
- None if not
438
435
"""
439
- token = handler .token
436
+ token = t . cast ( "str | None" , handler .token ) # type:ignore[attr-defined]
440
437
if not token :
441
438
return None
442
439
# check login token from URL argument or Authorization header
@@ -455,7 +452,7 @@ async def get_user_token(self, handler: JupyterHandler) -> User | None:
455
452
# which is stored in a cookie.
456
453
# still check the cookie for the user id
457
454
_user = self .get_user_cookie (handler )
458
- if isinstance (_user , Awaitable ):
455
+ if isinstance (_user , t . Awaitable ):
459
456
_user = await _user
460
457
user : User | None = _user
461
458
if user is None :
@@ -464,7 +461,7 @@ async def get_user_token(self, handler: JupyterHandler) -> User | None:
464
461
else :
465
462
return None
466
463
467
- def generate_anonymous_user (self , handler : JupyterHandler ) -> User :
464
+ def generate_anonymous_user (self , handler : web . RequestHandler ) -> User :
468
465
"""Generate a random anonymous user.
469
466
470
467
For use when a single shared token is used,
@@ -475,10 +472,10 @@ def generate_anonymous_user(self, handler: JupyterHandler) -> User:
475
472
name = display_name = f"Anonymous { moon } "
476
473
initials = f"A{ moon [0 ]} "
477
474
color = None
478
- handler .log .debug (f"Generating new user for token-authenticated request: { user_id } " )
475
+ handler .log .debug (f"Generating new user for token-authenticated request: { user_id } " ) # type:ignore[attr-defined]
479
476
return User (user_id , name , display_name , initials , None , color )
480
477
481
- def should_check_origin (self , handler : AuthenticatedHandler ) -> bool :
478
+ def should_check_origin (self , handler : web . RequestHandler ) -> bool :
482
479
"""Should the Handler check for CORS origin validation?
483
480
484
481
Origin check should be skipped for token-authenticated requests.
@@ -489,7 +486,7 @@ def should_check_origin(self, handler: AuthenticatedHandler) -> bool:
489
486
"""
490
487
return not self .is_token_authenticated (handler )
491
488
492
- def is_token_authenticated (self , handler : AuthenticatedHandler ) -> bool :
489
+ def is_token_authenticated (self , handler : web . RequestHandler ) -> bool :
493
490
"""Returns True if handler has been token authenticated. Otherwise, False.
494
491
495
492
Login with a token is used to signal certain things, such as:
@@ -504,8 +501,8 @@ def is_token_authenticated(self, handler: AuthenticatedHandler) -> bool:
504
501
505
502
def validate_security (
506
503
self ,
507
- app : ServerApp ,
508
- ssl_options : dict | None = None ,
504
+ app : t . Any ,
505
+ ssl_options : dict [ str , t . Any ] | None = None ,
509
506
) -> None :
510
507
"""Check the application's security.
511
508
@@ -526,7 +523,7 @@ def validate_security(
526
523
" Anyone who can connect to this server will be able to run code."
527
524
)
528
525
529
- def process_login_form (self , handler : JupyterHandler ) -> User | None :
526
+ def process_login_form (self , handler : web . RequestHandler ) -> User | None :
530
527
"""Process login form data
531
528
532
529
Return authenticated User if successful, None if not.
@@ -538,7 +535,7 @@ def process_login_form(self, handler: JupyterHandler) -> User | None:
538
535
return self .generate_anonymous_user (handler )
539
536
540
537
if self .token and self .token == typed_password :
541
- return self .user_for_token (typed_password ) # type:ignore[attr-defined]
538
+ return t . cast ( User , self .user_for_token (typed_password ) ) # type:ignore[attr-defined]
542
539
543
540
return user
544
541
@@ -633,7 +630,7 @@ def passwd_check(self, password):
633
630
"""Check password against our stored hashed password"""
634
631
return passwd_check (self .hashed_password , password )
635
632
636
- def process_login_form (self , handler : JupyterHandler ) -> User | None :
633
+ def process_login_form (self , handler : web . RequestHandler ) -> User | None :
637
634
"""Process login form data
638
635
639
636
Return authenticated User if successful, None if not.
@@ -659,8 +656,8 @@ def process_login_form(self, handler: JupyterHandler) -> User | None:
659
656
660
657
def validate_security (
661
658
self ,
662
- app : ServerApp ,
663
- ssl_options : dict | None = None ,
659
+ app : t . Any ,
660
+ ssl_options : dict [ str , t . Any ] | None = None ,
664
661
) -> None :
665
662
"""Handle security validation."""
666
663
super ().validate_security (app , ssl_options )
@@ -700,31 +697,33 @@ def _default_login_handler_class(self):
700
697
def auth_enabled (self ):
701
698
return self .login_available
702
699
703
- def get_user (self , handler : JupyterHandler ) -> User | None :
700
+ def get_user (self , handler : web . RequestHandler ) -> User | None :
704
701
"""Get the user."""
705
702
user = self .login_handler_class .get_user (handler ) # type:ignore[attr-defined]
706
703
if user is None :
707
704
return None
708
705
return _backward_compat_user (user )
709
706
710
707
@property
711
- def login_available (self ):
712
- return self .login_handler_class .get_login_available ( # type:ignore[attr-defined]
713
- self .settings
708
+ def login_available (self ) -> bool :
709
+ return bool (
710
+ self .login_handler_class .get_login_available ( # type:ignore[attr-defined]
711
+ self .settings
712
+ )
714
713
)
715
714
716
- def should_check_origin (self , handler : AuthenticatedHandler ) -> bool :
715
+ def should_check_origin (self , handler : web . RequestHandler ) -> bool :
717
716
"""Whether we should check origin."""
718
- return self .login_handler_class .should_check_origin (handler ) # type:ignore[attr-defined]
717
+ return bool ( self .login_handler_class .should_check_origin (handler ) ) # type:ignore[attr-defined]
719
718
720
- def is_token_authenticated (self , handler : AuthenticatedHandler ) -> bool :
719
+ def is_token_authenticated (self , handler : web . RequestHandler ) -> bool :
721
720
"""Whether we are token authenticated."""
722
- return self .login_handler_class .is_token_authenticated (handler ) # type:ignore[attr-defined]
721
+ return bool ( self .login_handler_class .is_token_authenticated (handler ) ) # type:ignore[attr-defined]
723
722
724
723
def validate_security (
725
724
self ,
726
- app : ServerApp ,
727
- ssl_options : dict | None = None ,
725
+ app : t . Any ,
726
+ ssl_options : dict [ str , t . Any ] | None = None ,
728
727
) -> None :
729
728
"""Validate security."""
730
729
if self .password_required and (not self .hashed_password ):
@@ -734,6 +733,6 @@ def validate_security(
734
733
self .log .critical (_i18n ("Hint: run the following command to set a password" ))
735
734
self .log .critical (_i18n ("\t $ python -m jupyter_server.auth password" ))
736
735
sys .exit (1 )
737
- return self .login_handler_class .validate_security ( # type:ignore[attr-defined]
736
+ self .login_handler_class .validate_security ( # type:ignore[attr-defined]
738
737
app , ssl_options
739
738
)
0 commit comments