mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-15 00:16:46 +00:00
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:
1 parent
33a73bc442
commit
3b44b6851d
13 files changed
+560
-6
No files matched your search
@@ -27,6 +27,7 @@ from docsgpt.core.model_utils import (
|
||||
get_provider_from_model_id,
|
||||
validate_model_id,
|
||||
)
|
||||
from docsgpt.agents.tools.wiki import outside_edits_allowed
|
||||
from docsgpt.core.settings import settings
|
||||
from docsgpt.guardrails.config import AgentConfig
|
||||
from sqlalchemy import text as sql_text
|
||||
@@ -1259,10 +1260,22 @@ class StreamProcessor:
|
||||
writable wiki source; the first match wins and the scan stops there so
|
||||
this runs at most one owner+source lookup per chat on the hot path.
|
||||
Returns None when no writable wiki source is present.
|
||||
|
||||
An API-key, widget or public-link run (``outside_caller``) gets the
|
||||
edit actions only when the wiki's owner turned on
|
||||
``wiki_outside_edits``; otherwise ``writes_allowed`` is False and the
|
||||
tool offers only ``wiki_view``.
|
||||
"""
|
||||
caller = self.decoded_token.get("sub") if self.decoded_token else None
|
||||
if not caller:
|
||||
return None
|
||||
# Processors built without __init__ (tests, resume helpers) lack these.
|
||||
run_config = getattr(self, "agent_config", None) or {}
|
||||
outside_caller = bool(
|
||||
run_config.get("external_api_caller")
|
||||
or run_config.get("public_link_caller")
|
||||
or getattr(self, "external_caller", False)
|
||||
)
|
||||
|
||||
wiki_config: Optional[Dict[str, Any]] = None
|
||||
try:
|
||||
@@ -1286,6 +1299,8 @@ class StreamProcessor:
|
||||
"source_owner_id": owner,
|
||||
"decoded_token": self.decoded_token,
|
||||
"user": caller,
|
||||
"outside_caller": outside_caller,
|
||||
"writes_allowed": not outside_caller or outside_edits_allowed(source_doc),
|
||||
}
|
||||
break
|
||||
except Exception:
|
||||
|
||||
@@ -224,6 +224,7 @@ RULES: dict[tuple[str, str], Rule] = {
|
||||
("/api/get_chunks", "GET"): _rule("sources:read", (QUERY, "id")),
|
||||
("/api/sources/<string:source_id>/wiki/pages", "GET"): _rule("sources:read", (VIEW, "source_id")),
|
||||
("/api/sources/<string:source_id>/wiki/page", "GET"): _rule("sources:read", (VIEW, "source_id")),
|
||||
("/api/sources/<string:source_id>/wiki/settings", "GET"): _rule("sources:read", (VIEW, "source_id")),
|
||||
("/api/sources/<string:source_id>/graph", "GET"): _rule("sources:read", (VIEW, "source_id")),
|
||||
("/api/sources/<string:source_id>/graph/node/<string:node_id>", "GET"): _rule(
|
||||
"sources:read", (VIEW, "source_id")
|
||||
@@ -357,6 +358,8 @@ DENIED: dict[str, tuple[str, ...]] = {
|
||||
"/api/teams/<string:team_id>/grants": ("POST", "DELETE"),
|
||||
"/api/teams/<string:team_id>/transfer_owner": ("*",),
|
||||
"/api/resource_settings": ("PUT",),
|
||||
# Who may edit a wiki from outside the app is the owner's call in a session.
|
||||
"/api/sources/<string:source_id>/wiki/settings": ("PUT",),
|
||||
"/swagger.json": ("*",),
|
||||
}
|
||||
DENIED_PREFIXES = (
|
||||
|
||||
@@ -1056,6 +1056,87 @@ class WikiPage(Resource):
|
||||
)
|
||||
|
||||
|
||||
def _wiki_settings_body(doc: dict, ra) -> dict:
|
||||
"""The wiki settings response: the stored switch plus the caller's access."""
|
||||
return {
|
||||
"success": True,
|
||||
"allow_outside_edits": bool(doc.get("wiki_outside_edits")),
|
||||
**ra.payload(),
|
||||
}
|
||||
|
||||
|
||||
@sources_ns.route("/sources/<string:source_id>/wiki/settings")
|
||||
class WikiSettings(Resource):
|
||||
@api.doc(
|
||||
description="A wiki's settings. Anyone who can see the wiki may read "
|
||||
"them; returns allow_outside_edits plus the caller's access."
|
||||
)
|
||||
def get(self, source_id):
|
||||
decoded_token = request.decoded_token
|
||||
if not decoded_token:
|
||||
return make_response(jsonify({"success": False}), 401)
|
||||
user = decoded_token.get("sub")
|
||||
try:
|
||||
with db_readonly() as conn:
|
||||
try:
|
||||
doc, ra = load_source(conn, source_id, user, "use")
|
||||
except AccessDenied as err:
|
||||
return denied_response(err)
|
||||
except Exception as err:
|
||||
current_app.logger.error(
|
||||
f"Error reading wiki settings for {source_id}: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify(_wiki_settings_body(doc, ra)), 200)
|
||||
|
||||
@api.doc(
|
||||
description="Change a wiki's settings (owner only, manage_settings). "
|
||||
"Body: {\"allow_outside_edits\": bool}: whether runs from the agent's "
|
||||
"API key, widget or public link may edit the wiki."
|
||||
)
|
||||
def put(self, source_id):
|
||||
decoded_token = request.decoded_token
|
||||
if not decoded_token:
|
||||
return make_response(jsonify({"success": False}), 401)
|
||||
user = decoded_token.get("sub")
|
||||
data = request.get_json(silent=True) or {}
|
||||
allowed = data.get("allow_outside_edits")
|
||||
if not isinstance(allowed, bool):
|
||||
return make_response(
|
||||
jsonify(
|
||||
{"success": False, "message": "allow_outside_edits must be true or false"}
|
||||
),
|
||||
400,
|
||||
)
|
||||
try:
|
||||
with db_session() as conn:
|
||||
try:
|
||||
doc, ra = load_source(conn, source_id, user, "manage_settings")
|
||||
except AccessDenied as err:
|
||||
return denied_response(err)
|
||||
if SourceConfig.parse(doc.get("config")).kind != "wiki":
|
||||
return make_response(
|
||||
jsonify({"success": False, "message": "Source is not a wiki"}), 400
|
||||
)
|
||||
SourcesRepository(conn).set_wiki_outside_edits(
|
||||
str(doc["id"]), ra.owner_id, allowed
|
||||
)
|
||||
record_event(
|
||||
conn,
|
||||
"source.wiki_settings_updated",
|
||||
actor=user,
|
||||
source_id=str(doc["id"]),
|
||||
allow_outside_edits=allowed,
|
||||
)
|
||||
doc["wiki_outside_edits"] = allowed
|
||||
except Exception as err:
|
||||
current_app.logger.error(
|
||||
f"Error updating wiki settings for {source_id}: {err}", exc_info=True
|
||||
)
|
||||
return make_response(jsonify({"success": False}), 400)
|
||||
return make_response(jsonify(_wiki_settings_body(doc, ra)), 200)
|
||||
|
||||
|
||||
def _source_is_blank(doc):
|
||||
"""True when a source has no ingested files to convert into pages."""
|
||||
structure = doc.get("directory_structure") or {}
|
||||
|
||||
Reference in new issue
Block a user