55to minimize write amplification when ACLs change.
66"""
77
8+ import asyncio
89import hashlib
910import json
10- import asyncio
11- from typing import Dict , List , Tuple , Optional
11+
1212from src .connectors .base import DocumentACL
1313from utils .logging_config import get_logger
1414
@@ -30,15 +30,32 @@ def compute_acl_hash(acl: DocumentACL) -> str:
3030 "allowed_users" : sorted (acl .allowed_users ),
3131 "allowed_groups" : sorted (acl .allowed_groups ),
3232 }
33- return hashlib .sha256 (
34- json .dumps (acl_data , sort_keys = True ).encode ()
35- ).hexdigest ()
33+ return hashlib .sha256 (json .dumps (acl_data , sort_keys = True ).encode ()).hexdigest ()
34+
35+
36+ def _build_id_query (document_id : str , id_fields : tuple [str , ...]) -> dict :
37+ """Build an OpenSearch query matching ``document_id`` against one or more fields.
38+
39+ Non-Langflow connector chunks store the connector source id in
40+ ``connector_file_id`` while ``document_id`` holds the content hash, whereas
41+ Langflow chunks and local uploads store the id in ``document_id``. Matching
42+ multiple fields lets a single id reliably target chunks from either pipeline.
43+ """
44+ if len (id_fields ) == 1 :
45+ return {"term" : {id_fields [0 ]: document_id }}
46+ return {
47+ "bool" : {
48+ "should" : [{"term" : {field : document_id }} for field in id_fields ],
49+ "minimum_should_match" : 1 ,
50+ }
51+ }
3652
3753
3854async def should_update_acl (
3955 document_id : str ,
4056 new_acl : DocumentACL ,
41- opensearch_client
57+ opensearch_client ,
58+ id_fields : tuple [str , ...] = ("document_id" ,),
4259) -> bool :
4360 """
4461 Check if ACL has changed by querying one chunk and comparing hashes.
@@ -59,14 +76,14 @@ async def should_update_acl(
5976 response = await opensearch_client .search (
6077 index = "documents" ,
6178 body = {
62- "query" : { "term" : { " document_id" : document_id }} ,
79+ "query" : _build_id_query ( document_id , id_fields ) ,
6380 "size" : 1 ,
6481 "_source" : [
6582 "owner" ,
6683 "allowed_users" ,
6784 "allowed_groups" ,
68- ]
69- }
85+ ],
86+ },
7087 )
7188
7289 if not response ["hits" ]["hits" ]:
@@ -96,24 +113,34 @@ async def should_update_acl(
96113async def update_document_acl (
97114 document_id : str ,
98115 acl : DocumentACL ,
99- opensearch_client
100- ) -> Dict [str , any ]:
116+ opensearch_client ,
117+ id_fields : tuple [str , ...] = ("document_id" ,),
118+ ) -> dict [str , any ]:
101119 """
102120 Update ACL for all chunks of a document.
103121
104122 Uses hash-based skip optimization: queries one chunk to check if ACL changed,
105123 only updates if changed. When updating, uses bulk update_by_query for efficiency.
106124
125+ Only the access lists (``allowed_users``/``allowed_groups``) are updated — the
126+ ``owner`` field is intentionally left untouched so an ACL re-sync never
127+ reassigns document ownership (owner is set once at ingest to the syncing user).
128+
107129 Args:
108130 document_id: Document identifier
109131 acl: New ACL to apply
110132 opensearch_client: OpenSearch client instance
133+ id_fields: Chunk fields to match ``document_id`` against. Defaults to
134+ ``("document_id",)``; pass ``("document_id", "connector_file_id")`` for
135+ connector chunks where the connector id lives in ``connector_file_id``.
111136
112137 Returns:
113138 Dict with status ("unchanged" or "updated") and chunks_updated count
114139 """
115140 # Check if ACL changed (queries one chunk)
116- should_update = await should_update_acl (document_id , acl , opensearch_client )
141+ should_update = await should_update_acl (
142+ document_id , acl , opensearch_client , id_fields = id_fields
143+ )
117144
118145 if not should_update :
119146 return {"status" : "unchanged" , "chunks_updated" : 0 }
@@ -123,36 +150,32 @@ async def update_document_acl(
123150 response = await opensearch_client .update_by_query (
124151 index = "documents" ,
125152 body = {
126- "query" : { "term" : { " document_id" : document_id }} ,
153+ "query" : _build_id_query ( document_id , id_fields ) ,
127154 "script" : {
128155 "source" : """
129- ctx._source.owner = params.owner;
130156 ctx._source.allowed_users = params.allowed_users;
131157 ctx._source.allowed_groups = params.allowed_groups;
132158 """ ,
133159 "params" : {
134- "owner" : acl .owner ,
135160 "allowed_users" : acl .allowed_users ,
136161 "allowed_groups" : acl .allowed_groups ,
137- }
138- }
139- }
162+ },
163+ },
164+ },
140165 )
141166
142- return {
143- "status" : "updated" ,
144- "chunks_updated" : response .get ("updated" , 0 )
145- }
167+ return {"status" : "updated" , "chunks_updated" : response .get ("updated" , 0 )}
146168
147169 except Exception as e :
148170 logger .error ("[OPENSEARCH] ACL update failed" , document_id = document_id , error = str (e ))
149171 return {"status" : "error" , "chunks_updated" : 0 , "error" : str (e )}
150172
151173
152174async def batch_update_acls (
153- acl_updates : List [Tuple [str , DocumentACL ]],
154- opensearch_client
155- ) -> Dict [str , any ]:
175+ acl_updates : list [tuple [str , DocumentACL ]],
176+ opensearch_client ,
177+ id_fields : tuple [str , ...] = ("document_id" ,),
178+ ) -> dict [str , any ]:
156179 """
157180 Batch update ACLs for multiple documents.
158181
@@ -161,9 +184,14 @@ async def batch_update_acls(
161184 - Skip unchanged ACLs (95%+ of webhook notifications)
162185 - Parallel bulk updates for changed ACLs
163186
187+ Only ``allowed_users``/``allowed_groups`` are updated; ``owner`` is left
188+ untouched (see :func:`update_document_acl`).
189+
164190 Args:
165191 acl_updates: List of (document_id, acl) tuples
166192 opensearch_client: OpenSearch client instance
193+ id_fields: Chunk fields to match the id against (see
194+ :func:`update_document_acl`).
167195
168196 Returns:
169197 Dict with status, documents_updated count, and chunks_updated count
@@ -173,15 +201,15 @@ async def batch_update_acls(
173201
174202 # Filter to only changed ACLs (parallel chunk queries)
175203 check_tasks = [
176- should_update_acl (doc_id , acl , opensearch_client )
204+ should_update_acl (doc_id , acl , opensearch_client , id_fields = id_fields )
177205 for doc_id , acl in acl_updates
178206 ]
179207 should_update_flags = await asyncio .gather (* check_tasks )
180208
181209 # Filter to documents with changed ACLs
182210 changed = [
183211 (doc_id , acl )
184- for (doc_id , acl ), should_update in zip (acl_updates , should_update_flags )
212+ for (doc_id , acl ), should_update in zip (acl_updates , should_update_flags , strict = True )
185213 if should_update
186214 ]
187215
@@ -190,28 +218,26 @@ async def batch_update_acls(
190218 "status" : "no_changes" ,
191219 "documents_updated" : 0 ,
192220 "chunks_updated" : 0 ,
193- "skipped" : len (acl_updates )
221+ "skipped" : len (acl_updates ),
194222 }
195223
196224 # Bulk update chunks for each document (parallelized)
197225 update_tasks = [
198226 opensearch_client .update_by_query (
199227 index = "documents" ,
200228 body = {
201- "query" : { "term" : { "document_id" : doc_id }} ,
229+ "query" : _build_id_query ( doc_id , id_fields ) ,
202230 "script" : {
203231 "source" : """
204- ctx._source.owner = params.owner;
205232 ctx._source.allowed_users = params.allowed_users;
206233 ctx._source.allowed_groups = params.allowed_groups;
207234 """ ,
208235 "params" : {
209- "owner" : acl .owner ,
210236 "allowed_users" : acl .allowed_users ,
211237 "allowed_groups" : acl .allowed_groups ,
212- }
213- }
214- }
238+ },
239+ },
240+ },
215241 )
216242 for doc_id , acl in changed
217243 ]
@@ -222,7 +248,7 @@ async def batch_update_acls(
222248 # Count successful updates
223249 total_chunks_updated = 0
224250 errors = []
225- for i , result in enumerate ( results ) :
251+ for result in results :
226252 if isinstance (result , Exception ):
227253 errors .append (str (result ))
228254 else :
@@ -233,13 +259,8 @@ async def batch_update_acls(
233259 "documents_updated" : len (changed ) - len (errors ),
234260 "chunks_updated" : total_chunks_updated ,
235261 "skipped" : len (acl_updates ) - len (changed ),
236- "errors" : errors if errors else None
262+ "errors" : errors if errors else None ,
237263 }
238264
239265 except Exception as e :
240- return {
241- "status" : "error" ,
242- "documents_updated" : 0 ,
243- "chunks_updated" : 0 ,
244- "error" : str (e )
245- }
266+ return {"status" : "error" , "documents_updated" : 0 , "chunks_updated" : 0 , "error" : str (e )}
0 commit comments