Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

authorisation: allow jupyter lab to work in standalone mode #558

Merged
merged 2 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -82,6 +83,11 @@
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

Check warning on line 89 in cylc/uiserver/authorise.py

View check run for this annotation

Codecov / codecov/patch

cylc/uiserver/authorise.py#L89

Added line #L89 was not covered by tests

# 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 @@
**kwargs,
):
nonlocal fun
user: JPSUser = handler.current_user
user: 'JPSUser' = handler.current_user

Check warning on line 66 in cylc/uiserver/handlers.py

View check run for this annotation

Codecov / codecov/patch

cylc/uiserver/handlers.py#L66

Added line #L66 was not covered by tests

if not user or not user.username:
# the user is only truthy if they have authenticated successfully
Expand All @@ -76,7 +73,7 @@
# 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 @@
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 @@
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 @@ -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,
)

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
oliver-sanders marked this conversation as resolved.
Show resolved Hide resolved
# NOTE: not using isinstance to narrow this down to just the one class


def _repr(value):
if isinstance(value, dict):
return '<dict>'
Expand Down
Loading