Connections own their credentials

Migration 0038 moves every stored secret (OAuth tokens, MCP OAuth
tokens and client registrations, API keys) into the connection's
encrypted envelope, links API-key tools to one connection per distinct
credential, allows several accounts per provider, and adds
credential_mode to sources and tools. OAuth MCP tools keep resolving
each member's own token, as they did before.

docsgpt.connectors.service is now the only reader of OAuth tokens:
get_valid_token_info refreshes under a row lock and persists rotated
refresh tokens, and a revoked grant flags the connection, pauses its
sources and notifies the owner. Loaders build from a connection
(BaseConnectorLoader.from_connection), so scheduled sync covers Drive,
SharePoint and Confluence sources with no browser. S3 and Reddit keys
stay on the connection instead of in remote_data.

New endpoints: POST /api/connections, /setup, /reconnect,
/picker-token, /claim, DELETE /api/connections/<id>, per-action
permissions and MCP refresh-tools. Upload, file listing, sync and
validate-session take a connection_id; session tokens keep working for
this release. The tool executor reads credentials from the resolved
connection (owner or member mode) and pauses on a Connect card when a
connection needs signing in. docsgpt connectors reencrypt rewrites
stored credentials after a key rotation.
This commit is contained in:
arc53-machine committed 2026-09-28 17:03:09 +01:00
1 parent 667d4bbab0
commit 3511004f50
54 files changed
+4007 -754

No files matched your search

+2 -1
View File
@@ -24,6 +24,7 @@ def _mock_connector_pipeline(monkeypatch):
from docsgpt import worker
fake_connector = MagicMock(name="connector")
fake_connector.connection_id = None
fake_connector.download_to_directory.return_value = {
"files_downloaded": 1,
"empty_result": False,
@@ -34,7 +35,7 @@ def _mock_connector_pipeline(monkeypatch):
monkeypatch.setattr(
worker.ConnectorCreator,
"create_connector",
staticmethod(lambda source_type, session_token: fake_connector),
staticmethod(lambda source_type, session_token=None, connection_id=None: fake_connector),
)
fake_reader = MagicMock(name="reader")
+2 -1
View File
@@ -125,7 +125,8 @@ class TestSyncWorker:
captured: list[dict] = []
def _fake_sync(self, source_data, name_job, user, loader,
sync_frequency, retriever, doc_id=None, directory="temp"):
sync_frequency, retriever, doc_id=None, directory="temp",
connection_id=None):
captured.append({
"name_job": name_job,
"user": user,
+2 -1
View File
@@ -470,6 +470,7 @@ def _stub_connector_pipeline(
from docsgpt import worker
fake_connector = MagicMock(name="connector")
fake_connector.connection_id = None
fake_connector.download_to_directory.return_value = {
"files_downloaded": files_downloaded,
"empty_result": empty_result,
@@ -482,7 +483,7 @@ def _stub_connector_pipeline(
monkeypatch.setattr(
worker.ConnectorCreator,
"create_connector",
staticmethod(lambda source_type, session_token: fake_connector),
staticmethod(lambda source_type, session_token=None, connection_id=None: fake_connector),
)
fake_reader = MagicMock(name="reader")