Skip to content

Commit

Permalink
feat: implement support for single custom prefix
Browse files Browse the repository at this point in the history
This allows for an administrator to set a prefix that will
be used for all authenticators configured.

It can be useful when there's a need to use an alternate login
method while keeping the same username.

**WARNING: This can have security implications !!!**

If two different users, using each a different login service
have the same username returned (e.g. User1 on GitLab gets UserXYZ
and User2 on GitHub also get UserXYZ, then they will have access
to the same account on the JupyterHub deployment.
  • Loading branch information
sgaist committed Oct 18, 2024
1 parent cad5c35 commit 83ad5ed
Show file tree
Hide file tree
Showing 3 changed files with 32 additions and 3 deletions.
2 changes: 1 addition & 1 deletion .bandit
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
# SPDX-License-Identifier: BSD-3-Clause

assert_used:
skips: ['multiauthenticator/tests/test_*.py']
skips: ['*_test.py', '*test_*.py']
15 changes: 13 additions & 2 deletions multiauthenticator/multiauthenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from jupyterhub.auth import Authenticator
from jupyterhub.utils import url_path_join
from traitlets import List
from traitlets import Unicode

PREFIX_SEPARATOR = ":"

Expand Down Expand Up @@ -66,6 +67,12 @@ class MultiAuthenticator(Authenticator):
for JupyterHub"""

authenticators = List(help="The subauthenticators to use", config=True)
username_prefix = Unicode(
help="Prefix to prepend to username",
config=True,
allow_none=True,
default_value=None,
)

def __init__(self, *arg, **kwargs):
super().__init__(*arg, **kwargs)
Expand All @@ -81,7 +88,9 @@ class WrapperAuthenticator(URLScopeMixin, authenticator_klass):

@property
def username_prefix(self):
prefix = f"{getattr(self, 'service_name', self.login_service)}{PREFIX_SEPARATOR}"
prefix = getattr(self, "prefix", None)
if prefix is None:
prefix = f"{getattr(self, 'service_name', self.login_service)}{PREFIX_SEPARATOR}"
return self.normalize_username(prefix)

async def authenticate(self, handler, data=None, **kwargs):
Expand Down Expand Up @@ -116,7 +125,9 @@ def check_blocked_users(self, username, authentication=None):
parent=self, **authenticator_configuration
)

if service_name is not None:
if self.username_prefix is not None:
authenticator.prefix = self.username_prefix
elif service_name is not None:
self.log.warning(
"service_name is deprecated, please create a subclass and set the login_service class variable"
)
Expand Down
18 changes: 18 additions & 0 deletions multiauthenticator/tests/test_multiauthenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,21 @@ def test_next_handling():

with_empty_next = template.render({"next": ""})
assert "href='dummy/login'" in with_empty_next


@pytest.mark.parametrize(
"prefix,expected", [(None, "DUMMY:TEST"), ("", "TEST"), ("prefix", "PREFIXTEST")]
)
@pytest.mark.asyncio
async def test_prefix(prefix, expected):
MultiAuthenticator.username_prefix = prefix
MultiAuthenticator.authenticators = [
(CustomDummyAuthenticator, "/dummy", {}),
]

multi_authenticator = MultiAuthenticator()
assert len(multi_authenticator._authenticators) == 1
user = await multi_authenticator._authenticators[0].get_authenticated_user(
None, {"username": "test"}
)
assert user["name"] == expected

0 comments on commit 83ad5ed

Please sign in to comment.