diff --git a/application/api/user/conversations/routes.py b/application/api/user/conversations/routes.py index 5e474538..d690dcec 100644 --- a/application/api/user/conversations/routes.py +++ b/application/api/user/conversations/routes.py @@ -11,6 +11,7 @@ from application.api.answer.services.conversation_service import ( TERMINATED_RESPONSE_PLACEHOLDER, ) from application.storage.db.base_repository import looks_like_uuid, row_to_dict +from application.storage.db.repositories.agents import AgentsRepository from application.storage.db.repositories.attachments import AttachmentsRepository from application.storage.db.repositories.conversations import ConversationsRepository from application.storage.db.repositories.message_events import MessageEventsRepository @@ -357,10 +358,20 @@ class SubmitFeedback(Resource): description="Submit feedback for a conversation", ) def post(self): + data = request.get_json() or {} decoded_token = request.decoded_token + # api_key callers (widgets) carry no JWT; resolve the key to its owner. + scoped_api_key = None if not decoded_token: - return make_response(jsonify({"success": False}), 401) - data = request.get_json() + api_key = data.get("api_key") + agent = None + if api_key: + with db_readonly() as conn: + agent = AgentsRepository(conn).find_by_key(api_key) + if not agent: + return make_response(jsonify({"success": False}), 401) + decoded_token = {"sub": agent.get("user_id")} + scoped_api_key = api_key required_fields = ["feedback", "conversation_id", "question_index"] missing_fields = check_required_fields(data, required_fields) if missing_fields: @@ -387,7 +398,10 @@ class SubmitFeedback(Resource): with db_session() as conn: repo = ConversationsRepository(conn) conv = repo.get_any(data["conversation_id"], user_id) - if conv is None: + # A key may only rate conversations it created. + if conv is None or ( + scoped_api_key and conv.get("api_key") != scoped_api_key + ): return make_response( jsonify({"success": False, "message": "Not found"}), 404 ) diff --git a/tests/api/user/test_conversations.py b/tests/api/user/test_conversations.py index 8667f6eb..32506df4 100644 --- a/tests/api/user/test_conversations.py +++ b/tests/api/user/test_conversations.py @@ -769,3 +769,88 @@ class TestSubmitFeedbackHappy: response = SubmitFeedback().post() assert response.status_code == 400 + + +@pytest.mark.unit +class TestSubmitFeedbackWithApiKey: + """api_key callers carry no JWT.""" + + def _seed_agent_with_key(self, pg_conn, owner, key): + from application.storage.db.repositories.agents import AgentsRepository + + return AgentsRepository(pg_conn).create(owner, "widget", "published", key=key) + + def _post(self, app, pg_conn, payload): + from application.api.user.conversations.routes import SubmitFeedback + + with _patch_conversations_db(pg_conn), app.test_request_context("/api/feedback", method="POST", json=payload): + from flask import request + + request.decoded_token = None + return SubmitFeedback().post() + + def test_valid_key_rates_its_own_conversation(self, app, pg_conn): + from application.storage.db.repositories.conversations import ( + ConversationsRepository, + ) + + owner, key = "owner-fb-key", "agent-key-ok" + self._seed_agent_with_key(pg_conn, owner, key) + repo = ConversationsRepository(pg_conn) + conv_id = str(repo.create(owner, name="via widget", api_key=key)["id"]) + repo.append_message(conv_id, {"prompt": "p", "response": "r"}) + + response = self._post( + app, + pg_conn, + { + "feedback": "LIKE", + "question_index": 0, + "conversation_id": conv_id, + "api_key": key, + }, + ) + + assert response.status_code == 200 + fb = repo.get_messages(conv_id)[0].get("feedback") + assert fb and fb.get("text") == "like" + + def test_key_cannot_rate_owner_conversation_it_did_not_create(self, app, pg_conn): + from application.storage.db.repositories.conversations import ( + ConversationsRepository, + ) + + owner, key = "owner-fb-scope", "agent-key-scope" + self._seed_agent_with_key(pg_conn, owner, key) + # Owner's conversation, not created with the key. + conv_id = _seed_conversation(pg_conn, owner, name="owner private") + ConversationsRepository(pg_conn).append_message(conv_id, {"prompt": "p", "response": "r"}) + + response = self._post( + app, + pg_conn, + { + "feedback": "LIKE", + "question_index": 0, + "conversation_id": conv_id, + "api_key": key, + }, + ) + + assert response.status_code == 404 + + def test_unknown_key_is_unauthorized(self, app, pg_conn): + conv_id = _seed_conversation(pg_conn, "owner-fb-bad") + + response = self._post( + app, + pg_conn, + { + "feedback": "LIKE", + "question_index": 0, + "conversation_id": conv_id, + "api_key": "no-such-key", + }, + ) + + assert response.status_code == 401