diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dd551181..98b0fe4b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -103,6 +103,9 @@ jobs: subset: singleuser - python: "3.11" browser: browser + - python: "3.11" + subdomain: subdomain + browser: browser - python: "3.12" main_dependencies: main_dependencies diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 9c426ce3..65bf4e90 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -38,7 +38,6 @@ from ..metrics import ( ServerStopStatus, ) from ..objects import Server -from ..scopes import needs_scope from ..spawner import LocalProcessSpawner from ..user import User from ..utils import ( @@ -1557,10 +1556,28 @@ class UserUrlHandler(BaseHandler): delete = non_get @web.authenticated - @needs_scope("access:servers") async def get(self, user_name, user_path): if not user_path: user_path = '/' + path_parts = user_path.split("/", 2) + server_names = [""] + if len(path_parts) >= 3: + # second part _may_ be a server name + server_names.append(path_parts[1]) + + access_scopes = [ + f"access:servers!server={user_name}/{server_name}" + for server_name in server_names + ] + if not any(self.has_scope(scope) for scope in access_scopes): + self.log.warning( + "Not authorizing access to %s. Requires any of [%s], not derived from scopes [%s]", + self.request.path, + ", ".join(access_scopes), + ", ".join(self.expanded_scopes), + ) + raise web.HTTPError(404, "No access to resources or resources not found") + current_user = self.current_user if user_name != current_user.name: user = self.find_user(user_name) diff --git a/jupyterhub/tests/browser/test_browser.py b/jupyterhub/tests/browser/test_browser.py index 9054138a..5e698c66 100644 --- a/jupyterhub/tests/browser/test_browser.py +++ b/jupyterhub/tests/browser/test_browser.py @@ -44,7 +44,7 @@ async def test_submit_login_form(app, browser, user_special_chars): login_url = url_path_join(public_host(app), app.hub.base_url, "login") await browser.goto(login_url) await login(browser, user.name, password=user.name) - expected_url = ujoin(public_url(app), f"/user/{user_special_chars.urlname}/") + expected_url = public_url(app, user) await expect(browser).to_have_url(expected_url) @@ -56,7 +56,7 @@ async def test_submit_login_form(app, browser, user_special_chars): # will encode given parameters for an unauthenticated URL in the next url # the next parameter will contain the app base URL (replaces BASE_URL in tests) 'spawn', - [('param', 'value')], + {'param': 'value'}, '/hub/login?next={{BASE_URL}}hub%2Fspawn%3Fparam%3Dvalue', '/hub/login?next={{BASE_URL}}hub%2Fspawn%3Fparam%3Dvalue', ), @@ -64,15 +64,15 @@ async def test_submit_login_form(app, browser, user_special_chars): # login?param=fromlogin&next=encoded(/hub/spawn?param=value) # will drop parameters given to the login page, passing only the next url 'login', - [('param', 'fromlogin'), ('next', '/hub/spawn?param=value')], - '/hub/login?param=fromlogin&next=%2Fhub%2Fspawn%3Fparam%3Dvalue', - '/hub/login?next=%2Fhub%2Fspawn%3Fparam%3Dvalue', + {'param': 'fromlogin', 'next': '/hub/spawn?param=value'}, + '/hub/login?param=fromlogin&next={{BASE_URL}}hub%2Fspawn%3Fparam%3Dvalue', + '/hub/login?next={{BASE_URL}}hub%2Fspawn%3Fparam%3Dvalue', ), ( # login?param=value&anotherparam=anothervalue # will drop parameters given to the login page, and use an empty next url 'login', - [('param', 'value'), ('anotherparam', 'anothervalue')], + {'param': 'value', 'anotherparam': 'anothervalue'}, '/hub/login?param=value&anotherparam=anothervalue', '/hub/login?next=', ), @@ -80,7 +80,7 @@ async def test_submit_login_form(app, browser, user_special_chars): # login # simplest case, accessing the login URL, gives an empty next url 'login', - [], + {}, '/hub/login', '/hub/login?next=', ), @@ -98,6 +98,8 @@ async def test_open_url_login( user = user_special_chars.user login_url = url_path_join(public_host(app), app.hub.base_url, url) await browser.goto(login_url) + if params.get("next"): + params["next"] = url_path_join(app.base_url, params["next"]) url_new = url_path_join(public_host(app), app.hub.base_url, url_concat(url, params)) print(url_new) await browser.goto(url_new) @@ -853,12 +855,15 @@ async def test_oauth_page( oauth_client.allowed_scopes = sorted(roles.roles_to_scopes([service_role])) app.db.commit() # open the service url in the browser - service_url = url_path_join(public_url(app, service) + 'owhoami/?arg=x') + service_url = url_path_join(public_url(app, service), 'owhoami/?arg=x') await browser.goto(service_url) - expected_redirect_url = url_path_join( - app.base_url + f"services/{service.name}/oauth_callback" - ) + if app.subdomain_host: + expected_redirect_url = url_path_join( + public_url(app, service), "oauth_callback" + ) + else: + expected_redirect_url = url_path_join(service.prefix, "oauth_callback") expected_client_id = f"service-{service.name}" # decode the URL