Let a wiki's owner decide whether API, widget and public-link runs edit it

A run from an agent's API key or widget acts as the agent's owner, so it
could rewrite any wiki the owner can edit. A new per-wiki setting,
wiki_outside_edits (off by default), decides whether such runs, and runs
from the agent's public link, get the wiki's edit actions. While it is off
they are offered only wiki_view, and the tool refuses writes itself after
reading the live setting. The owner changes it through the owner-only
/api/sources/<id>/wiki/settings route; tokens can read it but not change it.
This commit is contained in:
arc53-machine committed 2026-09-29 16:07:57 +01:00
1 parent 33a73bc442
commit 3b44b6851d
13 files changed
+560 -6

No files matched your search

@@ -606,3 +606,102 @@ class TestWikiPageEdit:
assert response.status_code == 403
mock_reembed.assert_not_called()
def _settings_call(app, pg_conn, sid, user, method="GET", body=None):
from docsgpt.api.user.sources.routes import WikiSettings
with _patch_db(pg_conn), app.test_request_context(
f"/api/sources/{sid}/wiki/settings", method=method, json=body
):
from flask import request
request.decoded_token = {"sub": user} if user else None
resource = WikiSettings()
return resource.get(sid) if method == "GET" else resource.put(sid)
class TestWikiSettings:
def _wiki(self, pg_conn, owner, kind="wiki"):
from docsgpt.storage.db.repositories.sources import SourcesRepository
src = SourcesRepository(pg_conn).create(
"wiki", user_id=owner, type=kind, config={"kind": kind}
)
return str(src["id"])
def _stored(self, pg_conn, sid):
from docsgpt.storage.db.repositories.sources import SourcesRepository
return SourcesRepository(pg_conn).get_by_id(sid)["wiki_outside_edits"]
def test_returns_401_unauthenticated(self, app, pg_conn):
assert _settings_call(app, pg_conn, str(uuid.uuid4()), None).status_code == 401
assert _settings_call(app, pg_conn, str(uuid.uuid4()), None, "PUT", {}).status_code == 401
def test_defaults_to_off(self, app, pg_conn):
sid = self._wiki(pg_conn, "alice-ws")
response = _settings_call(app, pg_conn, sid, "alice-ws")
assert response.status_code == 200
assert response.json["allow_outside_edits"] is False
assert "manage_settings" in response.json["allowed_actions"]
def test_owner_turns_it_on_and_off(self, app, pg_conn):
sid = self._wiki(pg_conn, "alice-ws-on")
response = _settings_call(
app, pg_conn, sid, "alice-ws-on", "PUT", {"allow_outside_edits": True}
)
assert response.status_code == 200
assert response.json["allow_outside_edits"] is True
assert self._stored(pg_conn, sid) is True
assert _settings_call(app, pg_conn, sid, "alice-ws-on").json["allow_outside_edits"] is True
_settings_call(app, pg_conn, sid, "alice-ws-on", "PUT", {"allow_outside_edits": False})
assert self._stored(pg_conn, sid) is False
def test_change_is_audited(self, app, pg_conn):
from sqlalchemy import text
sid = self._wiki(pg_conn, "alice-ws-audit")
_settings_call(app, pg_conn, sid, "alice-ws-audit", "PUT", {"allow_outside_edits": True})
row = pg_conn.execute(
text("SELECT metadata FROM auth_events WHERE event = 'source.wiki_settings_updated' AND actor_id = :a"),
{"a": "alice-ws-audit"},
).fetchone()
assert row is not None
assert row[0]["source_id"] == sid and row[0]["allow_outside_edits"] is True
@pytest.mark.parametrize("level", ["editor", "viewer"])
def test_team_member_reads_but_cannot_change(self, app, pg_conn, level):
owner, member = f"alice-ws-{level}", f"bob-ws-{level}"
sid = self._wiki(pg_conn, owner)
_grant_team_access(pg_conn, owner, member, sid, level)
read = _settings_call(app, pg_conn, sid, member)
assert read.status_code == 200
assert read.json["allow_outside_edits"] is False
assert "manage_settings" not in read.json["allowed_actions"]
response = _settings_call(
app, pg_conn, sid, member, "PUT", {"allow_outside_edits": True}
)
assert response.status_code == 403
assert self._stored(pg_conn, sid) is False
def test_stranger_gets_404(self, app, pg_conn):
sid = self._wiki(pg_conn, "alice-ws-404")
assert _settings_call(app, pg_conn, sid, "eve-ws").status_code == 404
response = _settings_call(app, pg_conn, sid, "eve-ws", "PUT", {"allow_outside_edits": True})
assert response.status_code == 404
assert self._stored(pg_conn, sid) is False
@pytest.mark.parametrize("body", [{}, {"allow_outside_edits": "yes"}, {"allow_outside_edits": 1}])
def test_bad_body_is_400(self, app, pg_conn, body):
sid = self._wiki(pg_conn, "alice-ws-400")
response = _settings_call(app, pg_conn, sid, "alice-ws-400", "PUT", body)
assert response.status_code == 400
assert self._stored(pg_conn, sid) is False
def test_not_a_wiki_is_400(self, app, pg_conn):
sid = self._wiki(pg_conn, "alice-ws-classic", kind="classic")
response = _settings_call(
app, pg_conn, sid, "alice-ws-classic", "PUT", {"allow_outside_edits": True}
)
assert response.status_code == 400
assert self._stored(pg_conn, sid) is False