mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-10-21 05:05:36 +00:00
fix(sunset): gate SUNSET_MODE on DEPLOYMENT_MODE=cloud so self-hosted instances can never be sunset-blocked
This commit is contained in:
parent
73202b1966
commit
aae178d294
5 changed files with 50 additions and 8 deletions
|
|
@ -106,7 +106,8 @@ NEXT_FRONTEND_URL=http://localhost:3000
|
|||
# clients read once at startup to load the live /sunset page instead of their
|
||||
# bundled frontend (plans/community-local/contracts/04-sunset-flag.md).
|
||||
# Read per request, so the flip needs no redeploy. Accepts 1 / true / yes / on.
|
||||
# Self-hosters leave this unset -- unset means "not sunset".
|
||||
# Only takes effect when DEPLOYMENT_MODE=cloud -- self-hosted instances ignore
|
||||
# this flag entirely, so leaving it unset (or set) makes no difference there.
|
||||
SUNSET_MODE=
|
||||
# Where those clients are sent. Defaults to https://surfsense.com/sunset.
|
||||
SUNSET_URL=
|
||||
|
|
|
|||
|
|
@ -473,7 +473,7 @@ class Config:
|
|||
# Deployment Mode (self-hosted or cloud)
|
||||
# self-hosted: Full access to local file system connectors (Obsidian, etc.)
|
||||
# cloud: Only cloud-based connectors available
|
||||
DEPLOYMENT_MODE = os.getenv("SURFSENSE_DEPLOYMENT_MODE", "self-hosted")
|
||||
DEPLOYMENT_MODE = os.getenv("DEPLOYMENT_MODE", "self-hosted")
|
||||
ENABLE_DESKTOP_LOCAL_FILESYSTEM = (
|
||||
os.getenv("ENABLE_DESKTOP_LOCAL_FILESYSTEM", "FALSE").upper() == "TRUE"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,9 +6,10 @@ flag that says so and the middleware that enforces it.
|
|||
|
||||
Two properties matter more than anything else here:
|
||||
|
||||
* **Self-hosters run this same code forever with the flag unset.** Every path
|
||||
through this module has to be a no-op in that case, which is why the flag is
|
||||
checked before anything else and defaults to off.
|
||||
* **Self-hosters can never hit this, even by accident.** ``SUNSET_MODE`` alone
|
||||
is not enough -- the flag only takes effect when ``DEPLOYMENT_MODE=cloud``,
|
||||
so a stray ``SUNSET_MODE=1`` in a self-hosted ``.env`` (copied from a hosted
|
||||
template, say) is a no-op rather than an outage.
|
||||
* **Signing in has to keep working.** Export is behind a session, so blocking
|
||||
authentication would lock people out of the one action still available and
|
||||
make the 30-day window meaningless.
|
||||
|
|
@ -58,10 +59,14 @@ _DETAIL = (
|
|||
def is_sunset_mode() -> bool:
|
||||
"""Whether the hosted service is winding down.
|
||||
|
||||
Read from the environment on every call rather than through ``config``,
|
||||
which resolves at import: contract 4 requires that flipping the flag take
|
||||
effect without a deploy.
|
||||
Only ever true for a cloud deployment. ``SUNSET_MODE`` is read from the
|
||||
environment on every call rather than cached, so flipping it takes effect
|
||||
without a deploy; ``DEPLOYMENT_MODE`` is the safety net that keeps a
|
||||
self-hosted instance immune to that flag regardless of what its own
|
||||
``.env`` sets.
|
||||
"""
|
||||
if os.getenv("DEPLOYMENT_MODE", "self-hosted") != "cloud":
|
||||
return False
|
||||
return os.getenv("SUNSET_MODE", "").strip().lower() in _TRUTHY
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ def test_every_documented_spelling_turns_the_flag_on(client, monkeypatch, value)
|
|||
Both have to work. The cost of a spelling that silently reads as false is
|
||||
a sunset day where nothing happens and nothing says why.
|
||||
"""
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.setenv("SUNSET_MODE", value)
|
||||
|
||||
assert _health(client)["sunset"] is True
|
||||
|
|
@ -51,13 +52,23 @@ def test_every_documented_spelling_turns_the_flag_on(client, monkeypatch, value)
|
|||
|
||||
@pytest.mark.parametrize("value", ["", "0", "false", "no", "off", " "])
|
||||
def test_anything_else_leaves_the_flag_off(client, monkeypatch, value):
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.setenv("SUNSET_MODE", value)
|
||||
|
||||
assert _health(client)["sunset"] is False
|
||||
|
||||
|
||||
def test_self_hosted_stays_off_even_if_sunset_mode_is_set(client, monkeypatch):
|
||||
"""A stray ``SUNSET_MODE=1`` in a self-hosted ``.env`` must be a no-op."""
|
||||
monkeypatch.delenv("DEPLOYMENT_MODE", raising=False)
|
||||
monkeypatch.setenv("SUNSET_MODE", "1")
|
||||
|
||||
assert _health(client)["sunset"] is False
|
||||
|
||||
|
||||
def test_the_flag_is_read_per_request(client, monkeypatch):
|
||||
"""No restart beyond the flag change, so it cannot be cached at import."""
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.delenv("SUNSET_MODE", raising=False)
|
||||
assert _health(client)["sunset"] is False
|
||||
|
||||
|
|
@ -71,6 +82,7 @@ def test_sunset_is_a_json_boolean_not_a_string(client, monkeypatch):
|
|||
``is True`` rather than ``== True`` on purpose: ``1 == True`` in Python, so
|
||||
equality would pass on the raw environment value this once returned.
|
||||
"""
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.setenv("SUNSET_MODE", "1")
|
||||
|
||||
assert isinstance(_health(client)["sunset"], bool)
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@ def client() -> TestClient:
|
|||
|
||||
@pytest.fixture
|
||||
def sunset_on(monkeypatch):
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.setenv("SUNSET_MODE", "1")
|
||||
|
||||
|
||||
|
|
@ -92,13 +93,27 @@ def test_nothing_is_blocked_while_the_flag_is_unset(client, sunset_off, path, me
|
|||
@pytest.mark.parametrize("value", ["", "0", "false", "no", "off", " "])
|
||||
def test_a_flag_that_does_not_mean_yes_blocks_nothing(client, monkeypatch, value):
|
||||
"""Anything short of an explicit yes leaves the service fully writable."""
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.setenv("SUNSET_MODE", value)
|
||||
|
||||
assert client.post("/api/v1/documents").status_code == 200
|
||||
|
||||
|
||||
def test_self_hosted_ignores_a_stray_sunset_flag(client, monkeypatch):
|
||||
"""``DEPLOYMENT_MODE`` defaults to self-hosted, which must win over SUNSET_MODE.
|
||||
|
||||
A ``.env`` copied from a hosted template can carry ``SUNSET_MODE=1`` by
|
||||
accident; a self-hosted instance must stay fully writable regardless.
|
||||
"""
|
||||
monkeypatch.delenv("DEPLOYMENT_MODE", raising=False)
|
||||
monkeypatch.setenv("SUNSET_MODE", "1")
|
||||
|
||||
assert client.post("/api/v1/documents").status_code == 200
|
||||
|
||||
|
||||
def test_the_flag_is_read_per_request(client, monkeypatch):
|
||||
"""Sunset is thrown by changing the environment, not by redeploying."""
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.delenv("SUNSET_MODE", raising=False)
|
||||
assert client.post("/api/v1/documents").status_code == 200
|
||||
|
||||
|
|
@ -182,6 +197,7 @@ def test_the_scraper_api_keeps_running(client, sunset_on):
|
|||
|
||||
@pytest.mark.parametrize("value", ["1", "true", "TRUE", "True", "yes", "on"])
|
||||
def test_every_documented_spelling_means_yes(monkeypatch, value):
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.setenv("SUNSET_MODE", value)
|
||||
|
||||
assert is_sunset_mode() is True
|
||||
|
|
@ -189,6 +205,14 @@ def test_every_documented_spelling_means_yes(monkeypatch, value):
|
|||
|
||||
@pytest.mark.parametrize("value", ["", "0", "false", "no", "off", " ", "1 "])
|
||||
def test_anything_else_means_no(monkeypatch, value):
|
||||
monkeypatch.setenv("DEPLOYMENT_MODE", "cloud")
|
||||
monkeypatch.setenv("SUNSET_MODE", value)
|
||||
|
||||
assert is_sunset_mode() is (value.strip() == "1")
|
||||
|
||||
|
||||
def test_self_hosted_means_no_regardless_of_sunset_mode(monkeypatch):
|
||||
monkeypatch.delenv("DEPLOYMENT_MODE", raising=False)
|
||||
monkeypatch.setenv("SUNSET_MODE", "1")
|
||||
|
||||
assert is_sunset_mode() is False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue