Skip to content

Commit 8687bfb

Browse files
committed
refactor!: use origin instead of base_url in configuration
Streamlines configuration. Whereas previously we had `base_url=origin+root_path`, which forced us to check if the two matched, we now have only the two constituent pieces `origin` and `root_path`
1 parent 2bd9071 commit 8687bfb

File tree

11 files changed

+66
-65
lines changed

11 files changed

+66
-65
lines changed

packages/solara-enterprise/solara_enterprise/auth/flask.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,10 @@ def authorize():
5252
check_oauth()
5353
assert oauth is not None
5454
assert oauth.oauth1 is not None
55+
assert settings.main.origin is not None
5556

56-
org_url = session.pop("redirect_uri", settings.main.base_url + "/")
57+
base_url = settings.main.origin + settings.main.root_path + "/"
58+
org_url = session.pop("redirect_uri", base_url)
5759

5860
token = oauth.oauth1.authorize_access_token()
5961
# workaround: if token is set in the session in one piece, it is not saved, so we
@@ -82,6 +84,7 @@ def login(redirect_uri: Optional[str] = None):
8284
check_oauth()
8385
assert oauth is not None
8486
assert oauth.oauth1 is not None
87+
assert settings.main.origin is not None
8588
if "redirect_uri" in request.args:
8689
# we arrived here via the auth.get_login_url() call, which means the
8790
# redirect_uri is in the query params
@@ -91,7 +94,8 @@ def login(redirect_uri: Optional[str] = None):
9194
# where it detect we the OAuth.private=True setting, leading to a redirect
9295
session["redirect_uri"] = str(request.url)
9396
session["client_id"] = settings.oauth.client_id
94-
callback_url = str(settings.main.base_url) + "_solara/auth/authorize"
97+
base_url = settings.main.origin + settings.main.root_path
98+
callback_url = base_url + "/_solara/auth/authorize"
9599
result = oauth.oauth1.authorize_redirect(callback_url)
96100
return result
97101

packages/solara-enterprise/solara_enterprise/auth/starlette.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,9 @@ async def authorize(request: Request):
5252
check_oauth()
5353
assert oauth is not None
5454
assert oauth.oauth1 is not None
55+
assert settings.main.origin is not None
5556

56-
org_url = request.session.pop("redirect_uri", settings.main.base_url + "/")
57+
org_url = request.session.pop("redirect_uri", settings.main.origin + settings.main.root_path + "/")
5758

5859
token = await oauth.oauth1.authorize_access_token(request)
5960
# workaround: if token is set in the session in one piece, it is not saved, so we
@@ -83,6 +84,7 @@ async def login(request: Request, redirect_uri: Optional[str] = None):
8384
check_oauth()
8485
assert oauth is not None
8586
assert oauth.oauth1 is not None
87+
assert settings.main.origin is not None
8688
if "redirect_uri" in request.query_params:
8789
# we arrived here via the auth.get_login_url() call, which means the
8890
# redirect_uri is in the query params
@@ -92,7 +94,8 @@ async def login(request: Request, redirect_uri: Optional[str] = None):
9294
# where it detect we the OAuth.required=True setting, leading to a redirect
9395
request.session["redirect_uri"] = str(request.url.path)
9496
request.session["client_id"] = settings.oauth.client_id
95-
result = await oauth.oauth1.authorize_redirect(request, str(settings.main.base_url) + "_solara/auth/authorize")
97+
url = settings.main.origin + settings.main.root_path + "/_solara/auth/authorize"
98+
result = await oauth.oauth1.authorize_redirect(request, url)
9699
return result
97100

98101

packages/solara-enterprise/solara_enterprise/auth/utils.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ def get_logout_url(return_to_path: Optional[str] = None):
1212
if return_to_path is None:
1313
router = router_context.get()
1414
return_to_path = router.path
15-
if return_to_path.startswith("/"):
16-
return_to_path = return_to_path[1:]
17-
assert settings.main.base_url is not None
18-
return_to_app = urllib.parse.quote(settings.main.base_url + return_to_path)
19-
return_to = urllib.parse.quote(settings.main.base_url + f"_solara/auth/logout?redirect_uri={return_to_app}")
15+
if not return_to_path.startswith("/"):
16+
return_to_path = "/" + return_to_path
17+
assert settings.main.origin is not None
18+
url = settings.main.origin + settings.main.root_path
19+
return_to_app = urllib.parse.quote(url + return_to_path)
20+
return_to = urllib.parse.quote(url + f"/_solara/auth/logout?redirect_uri={return_to_app}")
2021
client_id = settings.oauth.client_id
2122
url = f"https://{settings.oauth.api_base_url}/{settings.oauth.logout_path}"
2223
if settings.oauth.logout_path.startswith("http"):
@@ -28,9 +29,10 @@ def get_login_url(return_to_path: Optional[str] = None):
2829
if return_to_path is None:
2930
router = router_context.get()
3031
return_to_path = router.path
31-
if return_to_path.startswith("/"):
32-
return_to_path = return_to_path[1:]
33-
assert settings.main.base_url is not None
34-
redirect_uri = urllib.parse.quote(settings.main.base_url + return_to_path)
35-
root = settings.main.root_path or ""
32+
if not return_to_path.startswith("/"):
33+
return_to_path = "/" + return_to_path
34+
assert settings.main.origin is not None
35+
url = settings.main.origin + settings.main.root_path
36+
redirect_uri = urllib.parse.quote(url + return_to_path)
37+
root = settings.main.root_path
3638
return f"{root}/_solara/auth/login?redirect_uri={redirect_uri}"

solara/__main__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,7 @@ def open_browser():
413413
kwargs["app"] = "solara.server.starlette:app"
414414
kwargs["log_config"] = LOGGING_CONFIG if log_config is None else log_config
415415
kwargs["loop"] = loop
416+
settings.main.root_path = root_path
416417
settings.main.use_pdb = use_pdb
417418
settings.theme.loader = theme_loader
418419
if dark:

solara/routing.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def __init__(self, path: str, routes: List[solara.Route], set_path: Callable[[st
5656
if _using_solara_server():
5757
import solara.server.settings
5858

59-
self.root_path = solara.server.settings.main.root_path or ""
59+
self.root_path = solara.server.settings.main.root_path
6060
# each route in this list corresponds to a part in self.parts
6161
self.path_routes: List["solara.Route"] = []
6262
self.path_routes_siblings: List[List["solara.Route"]] = [] # siblings including itself

solara/server/flask.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,9 @@ def kernels(id):
122122

123123
@websocket_extension.route("/jupyter/api/kernels/<kernel_id>/<name>")
124124
def kernels_connection(ws: simple_websocket.Server, kernel_id: str, name: str):
125-
if not settings.main.base_url:
126-
settings.main.base_url = url_for("blueprint-solara.read_root", _external=True)
125+
if not settings.main.origin:
126+
url = url_for("blueprint-solara.read_root", _external=True)
127+
settings.main.origin = url.rstrip("/")
127128
if settings.oauth.private and not has_solara_enterprise:
128129
raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
129130
if has_solara_enterprise:
@@ -238,8 +239,12 @@ def read_root(path):
238239
if root_path.endswith("/"):
239240
root_path = root_path[:-1]
240241

241-
if not settings.main.base_url:
242-
settings.main.base_url = url_for("blueprint-solara.read_root", _external=True)
242+
if not settings.main.origin:
243+
url_str = url_for("blueprint-solara.read_root", _external=True)
244+
url = urlparse(url_str)
245+
246+
settings.main.origin = url.scheme + "://" + url.netloc
247+
settings.main.root_path = url.path.rstrip("/")
243248

244249
session_id = request.cookies.get(server.COOKIE_KEY_SESSION_ID) or str(uuid4())
245250
if root_path:

solara/server/settings.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import uuid
77
import warnings
8+
import logging
89
from enum import Enum
910
from pathlib import Path
1011
from typing import Optional, List
@@ -178,8 +179,8 @@ class MainSettings(BaseSettings):
178179
mode: str = "production"
179180
tracer: bool = False
180181
timing: bool = False
181-
root_path: Optional[str] = None # e.g. /myapp (without trailing slash)
182-
base_url: str = "" # e.g. https://myapp.solara.run/myapp/
182+
root_path: str = "" # e.g. /myapp (without trailing slash)
183+
origin: Optional[str] = None # e.g. https://myapp.solara.run (without trailing slash)
183184
platform: str = sys.platform
184185
host: str = HOST_DEFAULT
185186
experimental_performance: bool = False
@@ -245,5 +246,20 @@ class Config:
245246
session.https_only = False
246247

247248

249+
# Make sure origin and root_path are correctly configured
250+
if "SOLARA_BASE_URL" in os.environ:
251+
from urllib.parse import urlparse
252+
253+
base_url = urlparse(os.environ["SOLARA_BASE_URL"])
254+
255+
if not main.origin:
256+
main.origin = base_url.scheme + "://" + base_url.netloc
257+
main.root_path = base_url.path.rstrip("/")
258+
logging.warning(
259+
"SOLARA_BASE_URL is deprecated, please use SOLARA_ORIGIN and SOLARA_ROOT_PATH instead. "
260+
"SOLARA_BASE_URL will be removed in a future major version of Solara.",
261+
)
262+
263+
248264
# call early so a misconfiguration fails early
249265
assets.extra_paths()

solara/server/starlette.py

Lines changed: 6 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -399,19 +399,17 @@ async def root(request: Request, fullpath: str = ""):
399399
""")
400400
if settings.oauth.private and not has_auth_support:
401401
raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
402-
root_path = settings.main.root_path or ""
403-
if not settings.main.base_url:
402+
root_path = settings.main.root_path
403+
if not settings.main.origin:
404404
# Note:
405405
# starlette does not respect x-forwarded-host, and therefore
406406
# base_url and expected_origin below could be different
407407
# x-forwarded-host should only be considered if the same criteria in
408408
# uvicorn's ProxyHeadersMiddleware accepts x-forwarded-proto
409-
settings.main.base_url = str(request.base_url)
410-
# if not explicltly set,
411-
configured_root_path = settings.main.root_path
409+
settings.main.origin = request.base_url.scheme + "://" + request.base_url.netloc
412410
scope = request.scope
413411
root_path_asgi = scope.get("route_root_path", scope.get("root_path", ""))
414-
if settings.main.root_path is None:
412+
if settings.main.root_path == "":
415413
# use the default root path from the app, which seems to also include the path
416414
# if we are mounted under a path
417415
root_path = root_path_asgi
@@ -425,44 +423,16 @@ async def root(request: Request, fullpath: str = ""):
425423
if script_name:
426424
logger.debug("override root_path using x-script-name header from %s to %s", root_path, script_name)
427425
root_path = script_name
426+
root_path = root_path.rstrip("/")
428427
settings.main.root_path = root_path
429428

430-
# lets be flexible about the trailing slash
431-
# TODO: maybe we should be more strict about the trailing slash
432-
naked_root_path = settings.main.root_path.rstrip("/")
433-
naked_base_url = settings.main.base_url.rstrip("/")
434-
if not naked_base_url.endswith(naked_root_path):
435-
msg = f"""base url {naked_base_url!r} does not end with root path {naked_root_path!r}
436-
437-
This could be a configuration mismatch behind a reverse proxy and can cause issues with redirect urls, and auth.
438-
439-
See also https://solara.dev/documentation/getting_started/deploying/self-hosted
440-
"""
441-
if "script-name" in request.headers:
442-
msg += f"""It looks like the reverse proxy sets the script-name header to {request.headers["script-name"]!r}
443-
"""
444-
if "x-script-name" in request.headers:
445-
msg += f"""It looks like the reverse proxy sets the x-script-name header to {request.headers["x-script-name"]!r}
446-
"""
447-
if configured_root_path:
448-
msg += f"""It looks like the root path was configured to {configured_root_path!r} in the settings
449-
"""
450-
if root_path_asgi:
451-
msg += f"""It looks like the root path set by the asgi framework was configured to {root_path_asgi!r}
452-
"""
453-
warnings.warn(msg)
454429
if host and forwarded_host and forwarded_proto:
455430
port = request.base_url.port
456431
ports = {"http": 80, "https": 443}
457432
expected_origin = f"{forwarded_proto}://{forwarded_host}"
458433
if port and port != ports[forwarded_proto]:
459434
expected_origin += f":{port}"
460-
starlette_origin = settings.main.base_url
461-
# strip off trailing / because we compare to the naked root path
462-
starlette_origin = starlette_origin.rstrip("/")
463-
if naked_root_path:
464-
# take off the root path
465-
starlette_origin = starlette_origin[: -len(naked_root_path)]
435+
starlette_origin = settings.main.origin
466436
if starlette_origin != expected_origin:
467437
warnings.warn(f"""Origin as determined by starlette ({starlette_origin!r}) does not match expected origin ({expected_origin!r}) based on x-forwarded-proto ({forwarded_proto!r}) and x-forwarded-host ({forwarded_host!r}) headers.
468438

solara/website/pages/documentation/advanced/content/30-enterprise/10-oauth.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ Please note that Python 3.6 is not supported for Solara OAuth.
175175
176176
### Wrong redirection
177177
178-
If the redirection back to solara return to the wrong address, it might be due to solara not choosing the right default for `SOLARA_BASE_URL`. This can happen in a situation where you have multiple reverse proxies that communicate via https and therefore need to set the Host header. For instance this variable could be set to `SOLARA_BASE_URL=https://solara.dev` for the solara.dev server. If you application runs behind a subpath, e.g. `/myapp`, you might have to set `SOLARA_ROOT_PATH=/myapp`.
178+
If the redirection back to solara return to the wrong address, it might be due to solara not choosing the right default for `SOLARA_ORIGIN`. This can happen in a situation where you have multiple reverse proxies that communicate via https and therefore need to set the Host header. For instance this variable could be set to `SOLARA_ORIGIN=https://solara.dev` for the solara.dev server. If you application runs behind a subpath, e.g. `/myapp`, you might have to set `SOLARA_ROOT_PATH=/myapp`.
179179
180180
181181
### Wrong schema detected for redirect URL

tests/integration/enterprise/oauth_test.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
@pytest.mark.skipif(not bool(os.environ.get("AUTH0_PASSWORD")), reason="AUTH0_PASSWORD not set")
1818
def test_oauth_from_app_auth0(page_session: playwright.sync_api.Page, solara_server, solara_app):
1919
with solara_app("solara.website.pages"):
20-
settings.main.base_url = ""
20+
settings.main.origin = None
2121
settings.oauth.client_id = settings.AUTH0_TEST_CLIENT_ID
2222
settings.oauth.client_secret = settings.AUTH0_TEST_CLIENT_SECRET
2323
settings.oauth.api_base_url = settings.AUTH0_TEST_API_BASE_URL
@@ -41,7 +41,7 @@ def test_oauth_from_app_auth0(page_session: playwright.sync_api.Page, solara_ser
4141
@pytest.mark.skip(reason="Fief support is deprecated for now")
4242
def test_oauth_from_app_fief(page_session: playwright.sync_api.Page, solara_server, solara_app):
4343
with solara_app("solara.website.pages"):
44-
settings.main.base_url = ""
44+
settings.main.origin = None
4545
settings.oauth.client_id = settings.FIEF_TEST_CLIENT_ID
4646
settings.oauth.client_secret = settings.FIEF_TEST_CLIENT_SECRET
4747
settings.oauth.api_base_url = settings.FIEF_TEST_API_BASE_URL
@@ -60,7 +60,7 @@ def test_oauth_private(page_session: playwright.sync_api.Page, solara_server, so
6060
settings.oauth.private = True
6161
try:
6262
with solara_app("solara.website.pages"):
63-
settings.main.base_url = ""
63+
settings.main.origin = None
6464
settings.oauth.client_id = settings.AUTH0_TEST_CLIENT_ID
6565
settings.oauth.client_secret = settings.AUTH0_TEST_CLIENT_SECRET
6666
settings.oauth.api_base_url = settings.AUTH0_TEST_API_BASE_URL

tests/integration/starlette_test.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ def myroot(request: Request):
3737

3838

3939
def test_starlette_mount(page_session: playwright.sync_api.Page, solara_app, extra_include_path):
40-
settings.main.root_path = None
41-
settings.main.base_url = ""
40+
settings.main.root_path = ""
41+
settings.main.origin = None
4242
try:
4343
port = conftest.TEST_PORT
4444
conftest.TEST_PORT += 1
@@ -49,5 +49,5 @@ def test_starlette_mount(page_session: playwright.sync_api.Page, solara_app, ext
4949
page_session.goto(f"{server.base_url}/solara_mount/")
5050
page_session.locator("text=Mounted in starlette").wait_for()
5151
finally:
52-
settings.main.root_path = None
53-
settings.main.base_url = ""
52+
settings.main.root_path = ""
53+
settings.main.origin = None

0 commit comments

Comments
 (0)