diff --git a/cylc/uiserver/authorise.py b/cylc/uiserver/authorise.py
index 415913b7..57aa81b4 100644
--- a/cylc/uiserver/authorise.py
+++ b/cylc/uiserver/authorise.py
@@ -28,6 +28,7 @@
from traitlets.config.loader import LazyConfigValue
from cylc.uiserver.schema import UISMutations
+from cylc.uiserver.utils import is_bearer_token_authenticated
class CylcAuthorizer(Authorizer):
@@ -82,6 +83,11 @@ def is_authorized(self, handler, user, action, resource) -> bool:
Note that Cylc uses its own authorization system (which is locked-down
by default) and is not affected by this policy.
"""
+ if is_bearer_token_authenticated(handler):
+ # this session is authenticated by a token or password NOT by
+ # Jupyter Hub -> the bearer of the token has full permissions
+ return True
+
# the username of the user running this server
# (used for authorzation purposes)
me = getuser()
diff --git a/cylc/uiserver/handlers.py b/cylc/uiserver/handlers.py
index 1ec87421..9b5c4b00 100644
--- a/cylc/uiserver/handlers.py
+++ b/cylc/uiserver/handlers.py
@@ -24,11 +24,6 @@
from graphql import get_default_backend
from graphql_ws.constants import GRAPHQL_WS
from jupyter_server.base.handlers import JupyterHandler
-from jupyter_server.auth.identity import (
- User as JPSUser,
- IdentityProvider as JPSIdentityProvider,
- PasswordIdentityProvider,
-)
from tornado import web, websocket
from tornado.ioloop import IOLoop
@@ -38,12 +33,14 @@
)
from cylc.uiserver.authorise import Authorization, AuthorizationMiddleware
+from cylc.uiserver.utils import is_bearer_token_authenticated
from cylc.uiserver.websockets import authenticated as websockets_authenticated
if TYPE_CHECKING:
from cylc.uiserver.resolvers import Resolvers
from cylc.uiserver.websockets.tornado import TornadoSubscriptionServer
from graphql.execution import ExecutionResult
+ from jupyter_server.auth.identity import User as JPSUser
ME = getpass.getuser()
@@ -66,7 +63,7 @@ def _inner(
**kwargs,
):
nonlocal fun
- user: JPSUser = handler.current_user
+ user: 'JPSUser' = handler.current_user
if not user or not user.username:
# the user is only truthy if they have authenticated successfully
@@ -76,7 +73,7 @@ def _inner(
# if authentication is turned off we don't want to work with this
raise web.HTTPError(403, reason='authorization insufficient')
- if is_token_authenticated(handler):
+ if is_bearer_token_authenticated(handler):
# token or password authenticated, the bearer of the token or
# password has full control
pass
@@ -90,20 +87,6 @@ def _inner(
return _inner
-def is_token_authenticated(handler: 'CylcAppHandler') -> bool:
- """Returns True if this request is bearer token authenticated.
-
- E.G. The default single-user token-based authenticated.
-
- In these cases the bearer of the token is awarded full privileges.
- """
- identity_provider: JPSIdentityProvider = (
- handler.serverapp.identity_provider # type: ignore[union-attr]
- )
- return identity_provider.__class__ == PasswordIdentityProvider
- # NOTE: not using isinstance to narrow this down to just the one class
-
-
def _authorise(
handler: 'CylcAppHandler',
username: str
@@ -139,7 +122,7 @@ def get_user_info(handler: 'CylcAppHandler'):
If the handler is token authenticated, then we return the username of the
account that this server instance is running under.
"""
- if is_token_authenticated(handler):
+ if is_bearer_token_authenticated(handler):
# the bearer of the token has full privileges
return {'name': ME, 'initials': get_initials(ME), 'username': ME}
else:
diff --git a/cylc/uiserver/tests/conftest.py b/cylc/uiserver/tests/conftest.py
index bb1d512a..ee3451d2 100644
--- a/cylc/uiserver/tests/conftest.py
+++ b/cylc/uiserver/tests/conftest.py
@@ -211,7 +211,7 @@ def mock_authentication_yossarian(monkeypatch):
user,
)
monkeypatch.setattr(
- 'cylc.uiserver.handlers.is_token_authenticated',
+ 'cylc.uiserver.handlers.is_bearer_token_authenticated',
lambda x: True,
)
diff --git a/cylc/uiserver/utils.py b/cylc/uiserver/utils.py
index 41a90521..98f9cb66 100644
--- a/cylc/uiserver/utils.py
+++ b/cylc/uiserver/utils.py
@@ -14,6 +14,35 @@
# along with this program. If not, see .
+from typing import TYPE_CHECKING
+
+from jupyter_server.auth.identity import PasswordIdentityProvider
+
+if TYPE_CHECKING:
+ from cylc.uiserver.handlers import CylcAppHandler
+ from jupyter_server.auth.identity import (
+ IdentityProvider as JPSIdentityProvider,
+ )
+
+
+def is_bearer_token_authenticated(handler: 'CylcAppHandler') -> bool:
+ """Returns True if this request is bearer token authenticated.
+
+ Bearer tokens, e.g. tokens (?token=1234) and passwords, are short pieces of
+ text that are used for authentication. These can be used in single-user
+ mode (i.e. "cylc gui"). In these cases the bearer of the token is awarded
+ full privileges.
+
+ In multi-user mode, we have more advanced authentication based on an
+ external service which allows us to implement fine-grained authorisation.
+ """
+ identity_provider: 'JPSIdentityProvider' = (
+ handler.serverapp.identity_provider # type: ignore[union-attr]
+ )
+ return identity_provider.__class__ == PasswordIdentityProvider
+ # NOTE: not using isinstance to narrow this down to just the one class
+
+
def _repr(value):
if isinstance(value, dict):
return ''