Skip to content

refactor!: use origin instead of base_url in configuration #979

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

Open
wants to merge 1 commit into
base: 12-23-feat_change_mutation_detection_and_allow_reactive_boolean_defaults
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 6 additions & 2 deletions packages/solara-enterprise/solara_enterprise/auth/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ def authorize():
check_oauth()
assert oauth is not None
assert oauth.oauth1 is not None
assert settings.main.origin is not None

org_url = session.pop("redirect_uri", settings.main.base_url + "/")
base_url = settings.main.origin + settings.main.root_path + "/"
org_url = session.pop("redirect_uri", base_url)

token = oauth.oauth1.authorize_access_token()
# workaround: if token is set in the session in one piece, it is not saved, so we
Expand Down Expand Up @@ -82,6 +84,7 @@ def login(redirect_uri: Optional[str] = None):
check_oauth()
assert oauth is not None
assert oauth.oauth1 is not None
assert settings.main.origin is not None
if "redirect_uri" in request.args:
# we arrived here via the auth.get_login_url() call, which means the
# redirect_uri is in the query params
Expand All @@ -91,7 +94,8 @@ def login(redirect_uri: Optional[str] = None):
# where it detect we the OAuth.private=True setting, leading to a redirect
session["redirect_uri"] = str(request.url)
session["client_id"] = settings.oauth.client_id
callback_url = str(settings.main.base_url) + "_solara/auth/authorize"
base_url = settings.main.origin + settings.main.root_path
callback_url = base_url + "/_solara/auth/authorize"
result = oauth.oauth1.authorize_redirect(callback_url)
return result

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ async def authorize(request: Request):
check_oauth()
assert oauth is not None
assert oauth.oauth1 is not None
assert settings.main.origin is not None

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

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


Expand Down
22 changes: 12 additions & 10 deletions packages/solara-enterprise/solara_enterprise/auth/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ def get_logout_url(return_to_path: Optional[str] = None):
if return_to_path is None:
router = router_context.get()
return_to_path = router.path
if return_to_path.startswith("/"):
return_to_path = return_to_path[1:]
assert settings.main.base_url is not None
return_to_app = urllib.parse.quote(settings.main.base_url + return_to_path)
return_to = urllib.parse.quote(settings.main.base_url + f"_solara/auth/logout?redirect_uri={return_to_app}")
if not return_to_path.startswith("/"):
return_to_path = "/" + return_to_path
assert settings.main.origin is not None
url = settings.main.origin + settings.main.root_path
return_to_app = urllib.parse.quote(url + return_to_path)
return_to = urllib.parse.quote(url + f"/_solara/auth/logout?redirect_uri={return_to_app}")
client_id = settings.oauth.client_id
url = f"https://{settings.oauth.api_base_url}/{settings.oauth.logout_path}"
if settings.oauth.logout_path.startswith("http"):
Expand All @@ -28,9 +29,10 @@ def get_login_url(return_to_path: Optional[str] = None):
if return_to_path is None:
router = router_context.get()
return_to_path = router.path
if return_to_path.startswith("/"):
return_to_path = return_to_path[1:]
assert settings.main.base_url is not None
redirect_uri = urllib.parse.quote(settings.main.base_url + return_to_path)
root = settings.main.root_path or ""
if not return_to_path.startswith("/"):
return_to_path = "/" + return_to_path
assert settings.main.origin is not None
url = settings.main.origin + settings.main.root_path
redirect_uri = urllib.parse.quote(url + return_to_path)
root = settings.main.root_path
return f"{root}/_solara/auth/login?redirect_uri={redirect_uri}"
1 change: 1 addition & 0 deletions solara/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ def open_browser():
kwargs["app"] = "solara.server.starlette:app"
kwargs["log_config"] = LOGGING_CONFIG if log_config is None else log_config
kwargs["loop"] = loop
settings.main.root_path = root_path
settings.main.use_pdb = use_pdb
settings.theme.loader = theme_loader
if dark:
Expand Down
2 changes: 1 addition & 1 deletion solara/routing.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def __init__(self, path: str, routes: List[solara.Route], set_path: Callable[[st
if _using_solara_server():
import solara.server.settings

self.root_path = solara.server.settings.main.root_path or ""
self.root_path = solara.server.settings.main.root_path
# each route in this list corresponds to a part in self.parts
self.path_routes: List["solara.Route"] = []
self.path_routes_siblings: List[List["solara.Route"]] = [] # siblings including itself
Expand Down
13 changes: 9 additions & 4 deletions solara/server/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,9 @@ def kernels(id):

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

if not settings.main.base_url:
settings.main.base_url = url_for("blueprint-solara.read_root", _external=True)
if not settings.main.origin:
url_str = url_for("blueprint-solara.read_root", _external=True)
url = urlparse(url_str)

settings.main.origin = url.scheme + "://" + url.netloc
settings.main.root_path = url.path.rstrip("/")

session_id = request.cookies.get(server.COOKIE_KEY_SESSION_ID) or str(uuid4())
if root_path:
Expand Down
20 changes: 18 additions & 2 deletions solara/server/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
import uuid
import warnings
import logging
from enum import Enum
from pathlib import Path
from typing import Optional, List
Expand Down Expand Up @@ -178,8 +179,8 @@ class MainSettings(BaseSettings):
mode: str = "production"
tracer: bool = False
timing: bool = False
root_path: Optional[str] = None # e.g. /myapp (without trailing slash)
base_url: str = "" # e.g. https://myapp.solara.run/myapp/
root_path: str = "" # e.g. /myapp (without trailing slash)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We used the None option to signal it's not configured, which then in starlette.py will recognize it (put a comment there where we do that)

origin: Optional[str] = None # e.g. https://myapp.solara.run (without trailing slash)
platform: str = sys.platform
host: str = HOST_DEFAULT
experimental_performance: bool = False
Expand Down Expand Up @@ -245,5 +246,20 @@ class Config:
session.https_only = False


# Make sure origin and root_path are correctly configured
if "SOLARA_BASE_URL" in os.environ:
from urllib.parse import urlparse

base_url = urlparse(os.environ["SOLARA_BASE_URL"])

if not main.origin:
main.origin = base_url.scheme + "://" + base_url.netloc
main.root_path = base_url.path.rstrip("/")
logging.warning(
"SOLARA_BASE_URL is deprecated, please use SOLARA_ORIGIN and SOLARA_ROOT_PATH instead. "
"SOLARA_BASE_URL will be removed in a future major version of Solara.",
Comment on lines +259 to +260
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

)


# call early so a misconfiguration fails early
assets.extra_paths()
42 changes: 6 additions & 36 deletions solara/server/starlette.py
Original file line number Diff line number Diff line change
Expand Up @@ -399,19 +399,17 @@ async def root(request: Request, fullpath: str = ""):
""")
if settings.oauth.private and not has_auth_support:
raise RuntimeError("SOLARA_OAUTH_PRIVATE requires solara-enterprise")
root_path = settings.main.root_path or ""
if not settings.main.base_url:
root_path = settings.main.root_path
if not settings.main.origin:
# Note:
# starlette does not respect x-forwarded-host, and therefore
# base_url and expected_origin below could be different
# x-forwarded-host should only be considered if the same criteria in
# uvicorn's ProxyHeadersMiddleware accepts x-forwarded-proto
settings.main.base_url = str(request.base_url)
# if not explicltly set,
configured_root_path = settings.main.root_path
settings.main.origin = request.base_url.scheme + "://" + request.base_url.netloc
scope = request.scope
root_path_asgi = scope.get("route_root_path", scope.get("root_path", ""))
if settings.main.root_path is None:
if settings.main.root_path == "":
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to keep this to None, to make sure we only do this when it is not explicitly configured.

# use the default root path from the app, which seems to also include the path
# if we are mounted under a path
root_path = root_path_asgi
Expand All @@ -425,44 +423,16 @@ async def root(request: Request, fullpath: str = ""):
if script_name:
logger.debug("override root_path using x-script-name header from %s to %s", root_path, script_name)
root_path = script_name
root_path = root_path.rstrip("/")
settings.main.root_path = root_path

# lets be flexible about the trailing slash
# TODO: maybe we should be more strict about the trailing slash
naked_root_path = settings.main.root_path.rstrip("/")
naked_base_url = settings.main.base_url.rstrip("/")
if not naked_base_url.endswith(naked_root_path):
msg = f"""base url {naked_base_url!r} does not end with root path {naked_root_path!r}

This could be a configuration mismatch behind a reverse proxy and can cause issues with redirect urls, and auth.

See also https://solara.dev/documentation/getting_started/deploying/self-hosted
"""
if "script-name" in request.headers:
msg += f"""It looks like the reverse proxy sets the script-name header to {request.headers["script-name"]!r}
"""
if "x-script-name" in request.headers:
msg += f"""It looks like the reverse proxy sets the x-script-name header to {request.headers["x-script-name"]!r}
"""
if configured_root_path:
msg += f"""It looks like the root path was configured to {configured_root_path!r} in the settings
"""
if root_path_asgi:
msg += f"""It looks like the root path set by the asgi framework was configured to {root_path_asgi!r}
"""
warnings.warn(msg)
if host and forwarded_host and forwarded_proto:
port = request.base_url.port
ports = {"http": 80, "https": 443}
expected_origin = f"{forwarded_proto}://{forwarded_host}"
if port and port != ports[forwarded_proto]:
expected_origin += f":{port}"
starlette_origin = settings.main.base_url
# strip off trailing / because we compare to the naked root path
starlette_origin = starlette_origin.rstrip("/")
if naked_root_path:
# take off the root path
starlette_origin = starlette_origin[: -len(naked_root_path)]
starlette_origin = settings.main.origin
if starlette_origin != expected_origin:
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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ Please note that Python 3.6 is not supported for Solara OAuth.

### Wrong redirection

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`.
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`.


### Wrong schema detected for redirect URL
Expand Down
6 changes: 3 additions & 3 deletions tests/integration/enterprise/oauth_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
@pytest.mark.skipif(not bool(os.environ.get("AUTH0_PASSWORD")), reason="AUTH0_PASSWORD not set")
def test_oauth_from_app_auth0(page_session: playwright.sync_api.Page, solara_server, solara_app):
with solara_app("solara.website.pages"):
settings.main.base_url = ""
settings.main.origin = None
settings.oauth.client_id = settings.AUTH0_TEST_CLIENT_ID
settings.oauth.client_secret = settings.AUTH0_TEST_CLIENT_SECRET
settings.oauth.api_base_url = settings.AUTH0_TEST_API_BASE_URL
Expand All @@ -41,7 +41,7 @@ def test_oauth_from_app_auth0(page_session: playwright.sync_api.Page, solara_ser
@pytest.mark.skip(reason="Fief support is deprecated for now")
def test_oauth_from_app_fief(page_session: playwright.sync_api.Page, solara_server, solara_app):
with solara_app("solara.website.pages"):
settings.main.base_url = ""
settings.main.origin = None
settings.oauth.client_id = settings.FIEF_TEST_CLIENT_ID
settings.oauth.client_secret = settings.FIEF_TEST_CLIENT_SECRET
settings.oauth.api_base_url = settings.FIEF_TEST_API_BASE_URL
Expand All @@ -60,7 +60,7 @@ def test_oauth_private(page_session: playwright.sync_api.Page, solara_server, so
settings.oauth.private = True
try:
with solara_app("solara.website.pages"):
settings.main.base_url = ""
settings.main.origin = None
settings.oauth.client_id = settings.AUTH0_TEST_CLIENT_ID
settings.oauth.client_secret = settings.AUTH0_TEST_CLIENT_SECRET
settings.oauth.api_base_url = settings.AUTH0_TEST_API_BASE_URL
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/starlette_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ def myroot(request: Request):


def test_starlette_mount(page_session: playwright.sync_api.Page, solara_app, extra_include_path):
settings.main.root_path = None
settings.main.base_url = ""
settings.main.root_path = ""
settings.main.origin = None
try:
port = conftest.TEST_PORT
conftest.TEST_PORT += 1
Expand All @@ -49,5 +49,5 @@ def test_starlette_mount(page_session: playwright.sync_api.Page, solara_app, ext
page_session.goto(f"{server.base_url}/solara_mount/")
page_session.locator("text=Mounted in starlette").wait_for()
finally:
settings.main.root_path = None
settings.main.base_url = ""
settings.main.root_path = ""
settings.main.origin = None
Loading