diff --git a/nodes/src/nodes/store_pinecone/IGlobal.py b/nodes/src/nodes/store_pinecone/IGlobal.py index c6401ffc7..c849e9005 100644 --- a/nodes/src/nodes/store_pinecone/IGlobal.py +++ b/nodes/src/nodes/store_pinecone/IGlobal.py @@ -25,6 +25,7 @@ # This class controls the data shared between all threads for the task # ------------------------------------------------------------------------------ +import hashlib import os import re from typing import Any, Dict @@ -45,8 +46,9 @@ def _open_store(self, logical_type: str, conn_config: Dict[str, Any], bag: Dict[ return Store(logical_type, conn_config, bag) def _sub_key(self) -> str: - """Return the transform sub-key: host/port/collection.""" - return f'{self.store.host}/{self.store.port}/{self.store.collection}' + """Return the transform sub-key without leaking the Pinecone API key.""" + account = hashlib.sha256((self.store.apikey or '').encode()).hexdigest()[:12] + return f'{account}/{self.store.profile}/{self.store.collection}' def _probe_connection(self, config: Dict[str, Any]) -> None: """ diff --git a/nodes/src/nodes/store_pinecone/README.md b/nodes/src/nodes/store_pinecone/README.md index e9b78e9d9..274f94d36 100644 --- a/nodes/src/nodes/store_pinecone/README.md +++ b/nodes/src/nodes/store_pinecone/README.md @@ -99,6 +99,8 @@ The tool path runs on the control plane and does not pass through the pipeline's Set `apikey` to your Pinecone API key. The key is used during both config validation (HTTP client) and runtime data operations (gRPC client). There are no additional auth modes - Pinecone uses API-key-only authentication. +Changing the API key, profile, or index changes the transform key used to track ingested objects. Existing Pinecone pipelines may re-ingest once after upgrading from versions that keyed only by index name. + --- diff --git a/nodes/test/store_pinecone/test_transform_sub_key.py b/nodes/test/store_pinecone/test_transform_sub_key.py new file mode 100644 index 000000000..7325628e0 --- /dev/null +++ b/nodes/test/store_pinecone/test_transform_sub_key.py @@ -0,0 +1,59 @@ +import importlib.util +import sys +from pathlib import Path +from types import ModuleType, SimpleNamespace + + +class StoreGlobalBase: + pass + + +ai = ModuleType('ai') +ai_common = ModuleType('ai.common') +ai_common_store = ModuleType('ai.common.store') +ai_common_store.StoreGlobalBase = StoreGlobalBase +rocketlib = ModuleType('rocketlib') +rocketlib.warning = lambda message: None + +path = Path(__file__).parents[2] / 'src/nodes/store_pinecone/IGlobal.py' +spec = importlib.util.spec_from_file_location('store_pinecone_IGlobal', path) +assert spec is not None +module = importlib.util.module_from_spec(spec) +assert spec.loader is not None +stubs = { + 'ai': ai, + 'ai.common': ai_common, + 'ai.common.store': ai_common_store, + 'rocketlib': rocketlib, +} +originals = {name: sys.modules.get(name) for name in stubs} +sys.modules.update(stubs) +try: + spec.loader.exec_module(module) +finally: + for name, original in originals.items(): + if original is None: + del sys.modules[name] + else: + sys.modules[name] = original +IGlobal = module.IGlobal + + +def _sub_key(apikey: str, profile: str = 'serverless-dense', collection: str = 'shared-index') -> str: + glb = IGlobal.__new__(IGlobal) + glb.store = SimpleNamespace(apikey=apikey, profile=profile, collection=collection) + return glb._sub_key() + + +def test_sub_key_distinguishes_accounts_without_leaking_api_key() -> None: + first = _sub_key('pcsk_first_secret') + second = _sub_key('pcsk_second_secret') + + assert first != second + assert first.endswith('/serverless-dense/shared-index') + assert 'pcsk_first_secret' not in first + assert 'pcsk_second_secret' not in second + + +def test_sub_key_includes_profile_and_collection() -> None: + assert _sub_key('pcsk_secret', 'pod-based', 'other-index').endswith('/pod-based/other-index')