Skip to content

Commit

Permalink
Merge pull request #579 from cylc/1.4.x-sync
Browse files Browse the repository at this point in the history
🤖 Merge 1.4.x-sync into master
  • Loading branch information
oliver-sanders committed Apr 5, 2024
2 parents df7cba0 + db7a942 commit b007b5f
Show file tree
Hide file tree
Showing 6 changed files with 51 additions and 24 deletions.
10 changes: 10 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,16 @@ $ towncrier create <PR-number>.<break|feat|fix>.md --content "Short description"

<!-- towncrier release notes start -->

## cylc-uiserver-1.4.4 (Released 2024-04-05)

[Updated cylc-ui to 2.4.0](https://github.com/cylc/cylc-ui/blob/master/CHANGES.md)

### 🔧 Fixes

[#558](https://github.com/cylc/cylc-uiserver/pull/558) - Permit Jupyter Lab to be run in the same Jupyter Server instance as the Cylc UI Server in standalone mode (i.e. via `cylc gui`), note it was already possible to do this in multi-user mode (i.e. via `cylc hub`).

[#570](https://github.com/cylc/cylc-uiserver/pull/570) - Fix an issue that could impose a low limit on the number of active workflows the server is able to track.

## cylc-uiserver-1.4.3 (Released 2023-12-05)

### 🔧 Fixes
Expand Down
1 change: 0 additions & 1 deletion changes.d/+4e284398.ui-version.md

This file was deleted.

6 changes: 6 additions & 0 deletions cylc/uiserver/authorise.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -87,6 +88,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()
Expand Down
27 changes: 5 additions & 22 deletions cylc/uiserver/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion cylc/uiserver/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,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,
)

Expand Down
29 changes: 29 additions & 0 deletions cylc/uiserver/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,35 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.


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 '<dict>'
Expand Down

0 comments on commit b007b5f

Please sign in to comment.