14
14
# along with this program. If not, see <http://www.gnu.org/licenses/>.
15
15
16
16
from asyncio import Queue
17
+ from functools import wraps
17
18
import json
18
19
import getpass
19
20
import socket
21
+ from typing import Callable , Union
20
22
21
23
from graphene_tornado .tornado_graphql_handler import TornadoGraphQLHandler
22
24
from graphql import get_default_backend
31
33
ME = getpass .getuser ()
32
34
33
35
36
+ def authorised (fun : Callable ) -> Callable :
37
+ """Provides Cylc authorisation.
38
+
39
+ When the UIS is run standalone (token-authenticated) application,
40
+ authorisation is deactivated, the bearer of the token has full privileges.
41
+
42
+ When the UIS is spawned by Jupyter Hub (hub authenticated), multi-user
43
+ access is permitted. Users are authorised by _authorise.
44
+ """
45
+
46
+ @wraps (fun )
47
+ def _inner (
48
+ handler : 'CylcAppHandler' ,
49
+ * args ,
50
+ ** kwargs ,
51
+ ):
52
+ nonlocal fun
53
+ user : Union [
54
+ None , # unauthenticated
55
+ bytes , # token auth (bug in jupyter_server)
56
+ dict , # hub auth
57
+ str , # token auth
58
+
59
+ ] = handler .get_current_user ()
60
+ if user is None or user == 'anonymous' :
61
+ # user is not authenticated - additional protection incase the
62
+ # endpoint is not authenticate protected by mistake
63
+ # NOTE: Auth tests will hit this line unless mocked authentication
64
+ # is provided.
65
+ raise web .HTTPError (403 , reason = 'Forbidden' )
66
+ handler .log .critical (
67
+ f'authorise { user } ' # type: ignore
68
+ f'for { handler .__class__ .__name__ } '
69
+ )
70
+ if not (
71
+ is_token_authenticated (handler , user )
72
+ or (
73
+ isinstance (user , dict )
74
+ and _authorise (handler , user ['name' ], '' )
75
+ )
76
+ ):
77
+ raise web .HTTPError (403 , reason = 'authorisation insufficient' )
78
+ return fun (handler , * args , ** kwargs )
79
+ return _inner
80
+
81
+
82
+ def is_token_authenticated (
83
+ handler : JupyterHandler ,
84
+ user : Union [bytes , dict , str ],
85
+ ) -> bool :
86
+ """Returns True if the UIS is running "standalone".
87
+
88
+ At present we cannot use handler.is_token_authenticated because it
89
+ returns False when the token is cached in a cookie.
90
+
91
+ https://github.com/jupyter-server/jupyter_server/pull/562
92
+ """
93
+ if isinstance (user , bytes ):
94
+ # Cookie authentication:
95
+ # * The URL token is added to a secure cookie, it can then be
96
+ # removed rrom the URL for subsequent requests, the cookie is
97
+ # used in its place.
98
+ # * If the token was used token_authenticated is True.
99
+ # * If the cookie was used it is False (despite the cookie auth
100
+ # being derived from token auth).
101
+ # * Due to a bug in jupyter_server the user is returned as bytes
102
+ # when cookie auth is used so at present we can use this to
103
+ # tell.
104
+ # https://github.com/jupyter-server/jupyter_server/pull/562
105
+ # TODO: this hack is obviously not suitable for production!
106
+ return True
107
+ elif handler .token_authenticated :
108
+ # standalone UIS, the bearer of the token is the owner
109
+ # (no multi-user functionality so no futher auth required)
110
+ return True
111
+ return False
112
+
113
+
114
+ def _authorise (
115
+ handler : 'CylcAppHandler' ,
116
+ username : str ,
117
+ action : str = 'READ'
118
+ ) -> bool :
119
+ """Authorises a user to perform an action.
120
+
121
+ Currently this returns False unless the authenticated user is the same
122
+ as the user this server is running under.
123
+ """
124
+ if username != ME :
125
+ # auth provided by the hub, check the user name
126
+ handler .log .warning (f'Authorisation failed for { username } ' )
127
+ return False
128
+ return True
129
+
130
+
34
131
class CylcAppHandler (JupyterHandler ):
35
132
"""Base handler for Cylc endpoints.
36
133
@@ -42,46 +139,21 @@ class CylcAppHandler(JupyterHandler):
42
139
this handler to insert the HubOAuthenticated bases class high up
43
140
in the inheritance order.
44
141
45
- https://github.com/jupyterhub/jupyterhub/blob/
46
- 2c8b29b6bbd7197f34f553668365dbe16d001f03/
47
- jupyterhub/singleuser/mixins.py#L716
48
-
49
- TODO:
50
- * Implement authorisation!!!
51
- * Make authorisation configurabe via this class.
52
- * Make properties traitlets.
53
-
142
+ https://github.com/jupyterhub/jupyterhub/blob/3800ceaf9edf33a0171922b93ea3d94f87aa8d91/jupyterhub/singleuser/mixins.py#L826
54
143
"""
55
144
56
145
auth_level = None
57
146
58
147
@property
59
148
def hub_users (self ):
60
- # allow all users (handled by authorisation)
149
+ # allow all users (handled by Cylc authorisation decorator )
61
150
return None
62
151
63
152
@property
64
153
def hub_groups (self ):
65
- # allow all groups (handled by authorisation)
154
+ # allow all groups (handled by Cylc authorisation decorator )
66
155
return None
67
156
68
- def get_current_user (self ):
69
- user = CylcAppHandler .__bases__ [0 ].get_current_user (self )
70
- self .authorise (user )
71
- return user
72
-
73
- def authorise (self , user ):
74
- if not self ._authorise (user ):
75
- raise web .HTTPError (403 , reason = 'authorisation insufficient' )
76
-
77
- def _authorise (self , user ):
78
- self .log .debug (f'authorise { user } for { self .__class__ } ' )
79
- return True
80
- # if user != ME:
81
- # self.log.warning(f'Authorisation failed for {user}')
82
- # return False
83
- # return True
84
-
85
157
86
158
class CylcStaticHandler (CylcAppHandler , web .StaticFileHandler ):
87
159
pass
@@ -94,6 +166,7 @@ def set_default_headers(self) -> None:
94
166
self .set_header ("Content-Type" , 'application/json' )
95
167
96
168
@web .authenticated
169
+ @authorised
97
170
def get (self ):
98
171
user_info = self .get_current_user ()
99
172
@@ -158,10 +231,12 @@ def context(self):
158
231
return wider_context
159
232
160
233
@web .authenticated
234
+ @authorised
161
235
def prepare (self ):
162
236
super ().prepare ()
163
237
164
238
@web .authenticated
239
+ @authorised
165
240
async def execute (self , * args , ** kwargs ):
166
241
# Use own backend, and TornadoGraphQLHandler already does validation.
167
242
return await self .schema .execute (
@@ -173,6 +248,7 @@ async def execute(self, *args, **kwargs):
173
248
)
174
249
175
250
@web .authenticated
251
+ @authorised
176
252
async def run (self , * args , ** kwargs ):
177
253
await TornadoGraphQLHandler .run (self , * args , ** kwargs )
178
254
@@ -188,11 +264,13 @@ def select_subprotocol(self, subprotocols):
188
264
return GRAPHQL_WS
189
265
190
266
@websockets_authenticated
267
+ @authorised
191
268
def get (self , * args , ** kwargs ):
192
269
# forward this call so we can authenticate/authorise it
193
270
return websocket .WebSocketHandler .get (self , * args , ** kwargs )
194
271
195
272
@websockets_authenticated
273
+ @authorised
196
274
def open (self , * args , ** kwargs ):
197
275
IOLoop .current ().spawn_callback (self .subscription_server .handle , self ,
198
276
self .context )
0 commit comments