feat(PLU-511): ACL digest (permissions_version) for OneDrive/SharePoint - #778
feat(PLU-511): ACL digest (permissions_version) for OneDrive/SharePoint#778danielle-unstructured-io wants to merge 10 commits into
Conversation
…ve/SharePoint - Add permissions_version to FileDataSourceMetadata. - Add utils/acl.py: canonical, order-independent sha256 over permissions_data (+ denied). Returns None only when there is no ACL signal; an empty list yields a real digest so full revocation is detected. - Emit the digest in the OneDrive base indexer, covering SharePoint + OneDrive. - Unit tests: determinism (reorder), real change, revocation, deny-side, none-vs-empty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…oint indexer Log indexed items, digests computed, and Graph $batch calls at the end of the index run to show the permissions_version digest issues no Graph calls of its own (computed over already-fetched permissions). Supports the marginal-cost finding for the POC. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
73eb1eb to
212cc8a
Compare
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 6 files
Shadow auto-approve: would not auto-approve because issues were found.
Re-trigger cubic
…tion Empty raw_permissions was ambiguous: both a failed/forbidden Graph sub-response and a genuinely permission-less (revoked) item degraded to []. The truthiness guard then skipped the digest for both, so full access revocation went undetected (contradicting compute_permissions_version's empty-is-revoked contract and acceptance scenario S9). _parse_batch_response / _fetch_permissions_raw now map an unavailable fetch to None and a genuine 200-empty to []. The connector computes the digest when raw_permissions is not None, so revocation ([]) yields a stable digest while a fetch error (None) leaves the ACL fields unset (self-heals next run). Updates the affected batch-fetch tests, adds a deny-only digest test, and adds a 200-empty-vs-error test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 2 unresolved issues from previous reviews.
Re-trigger cubic
|
A few things from a read-through, mostly around the new None-vs-empty contract plus some cleanup I'd want before this merges.
Relaxing the guard to
Returning the normalized all-empty triple from the
Version bump This adds a public field to Changelog Three entries, where two of them describe fixes made to the first during review. From outside the PR that reads as churn rather than as anything a consumer acts on — a single entry describing the digest and its None-vs-empty semantics covers what changed. The telemetry helper
Internal references in the code There's a fair amount of internal ticket and scenario shorthand sitting in comments, docstrings, and test names here, including inside a log message that ships to production. This repo is public, so none of it means anything to a reader who doesn't have the internal context open. Worth a pass to either drop them or restate the reasoning inline so the code stands on its own. Small one
|
…review) Address awalker4's review on #778: - _parse_batch_response: a null/malformed 200 body no longer crashes the index run and a missing 'value' key no longer fabricates a revocation -- (sub.get('body') or {}).get('value') degrades both to None (unavailable), while a present-but-empty value stays [] (genuine revocation). - extract_permissions([]) now returns the normalized all-empty read/update/delete triple instead of [{}], so a revoked record and a no-op-role record share one digest (no spurious reprocess from representation drift). Tests pin the shape. - Version bump minor (1.7.17 -> 1.8.0): new public field + new utils/acl module. - Collapse the three intra-PR changelog entries into one. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
2 issues found across 4 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="unstructured_ingest/processes/connectors/onedrive.py">
<violation number="1" location="unstructured_ingest/processes/connectors/onedrive.py:528">
P2: A malformed 200 sub-response can still crash indexing because `_parse_batch_response` accepts any `body/value` shape and passes it as `raw_permissions`. Consider validating `body` is a dict and `value` is a list of permission dicts, otherwise degrading to `None` so this path skips digesting instead of failing the run.</violation>
</file>
<file name="test/unit/connectors/test_onedrive.py">
<violation number="1" location="test/unit/connectors/test_onedrive.py:491">
P2: A malformed successful Graph sub-response with a non-object `body` can still abort the index run, despite these tests asserting malformed bodies should degrade to `None`. Extending this coverage with cases such as `body=[]` and `body="invalid"` would expose the remaining parser gap and support guarding the body with a mapping check.</violation>
</file>
Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
| # (revoked) state and stays []. A null/absent body or a missing | ||
| # value key is malformed/unavailable and degrades to None (skip | ||
| # the digest) rather than a fabricated revocation. | ||
| by_id[di.id] = (sub.get("body") or {}).get("value") |
There was a problem hiding this comment.
P2: A malformed 200 sub-response can still crash indexing because _parse_batch_response accepts any body/value shape and passes it as raw_permissions. Consider validating body is a dict and value is a list of permission dicts, otherwise degrading to None so this path skips digesting instead of failing the run.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At unstructured_ingest/processes/connectors/onedrive.py, line 528:
<comment>A malformed 200 sub-response can still crash indexing because `_parse_batch_response` accepts any `body/value` shape and passes it as `raw_permissions`. Consider validating `body` is a dict and `value` is a list of permission dicts, otherwise degrading to `None` so this path skips digesting instead of failing the run.</comment>
<file context>
@@ -518,7 +521,11 @@ def _parse_batch_response(
+ # (revoked) state and stays []. A null/absent body or a missing
+ # value key is malformed/unavailable and degrades to None (skip
+ # the digest) rather than a fabricated revocation.
+ by_id[di.id] = (sub.get("body") or {}).get("value")
elif status in (401, 403):
logger.error(f"forbidden fetching permissions for {di.name} (status {status})")
</file context>
| # None (unavailable), not [] (which would be a fabricated revocation). | ||
| indexer = _make_indexer() | ||
| items = [_make_drive_item("f.docx")] | ||
| body = _batch_response(responses=[{"id": "0", "status": 200, "body": None}]) |
There was a problem hiding this comment.
P2: A malformed successful Graph sub-response with a non-object body can still abort the index run, despite these tests asserting malformed bodies should degrade to None. Extending this coverage with cases such as body=[] and body="invalid" would expose the remaining parser gap and support guarding the body with a mapping check.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At test/unit/connectors/test_onedrive.py, line 491:
<comment>A malformed successful Graph sub-response with a non-object `body` can still abort the index run, despite these tests asserting malformed bodies should degrade to `None`. Extending this coverage with cases such as `body=[]` and `body="invalid"` would expose the remaining parser gap and support guarding the body with a mapping check.</comment>
<file context>
@@ -471,6 +483,26 @@ def test_per_item_200_empty_value_is_empty_list(self):
+ # None (unavailable), not [] (which would be a fabricated revocation).
+ indexer = _make_indexer()
+ items = [_make_drive_item("f.docx")]
+ body = _batch_response(responses=[{"id": "0", "status": 200, "body": None}])
+ with patch("requests.post", return_value=body):
+ result = indexer._fetch_permissions_raw(items, access_token="tok")
</file context>
Per review (awalker4): the digest's marginal API cost is zero by construction (it hashes permissions already fetched for ACL metadata), so a runtime counter measured nothing derivable from the code while adding a per-run log line and a private cross-module helper. Remove _AclDigestTelemetry and its OneDrive/ SharePoint call sites; the cost is documented once in the analysis (microbenchmark + PLU-511 write-up) instead. Also removes the last internal ticket/scenario reference that shipped in a production log string. The None-vs-[] fetch-vs-revocation contract is unchanged; both run_async paths still pass perms_by_id.get(id) (defaulting to None) to drive_item_to_file_data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks Austin, this was a great pass. Rundown of what landed:
Version bump — agreed, bumped to a minor ( Changelog — collapsed to a single entry describing the digest and its None-vs-empty semantics. Telemetry helper — dropped entirely in
One remaining item from your public-repo note: the shipped log line is gone with the telemetry, but there are still |
There was a problem hiding this comment.
0 issues found across 3 files (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 2 unresolved issues from previous reviews.
Re-trigger cubic
…urce Per review (awalker4): strip internal shorthand (ticket + acceptance-scenario numbers) from comments, docstrings, and test docstrings so the code stands on its own in this public repo. Reasoning is restated inline where a ref carried it; the code pointer to _extract_identity_ids_from_raw is kept. No behavior change. (Changelog keeps its ticket ref per repo convention; the internal platform repos are unaffected.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Did the internal-reference sweep in That should cover everything from your pass — thanks again. |
There was a problem hiding this comment.
0 issues found across 4 files (changes from recent commits).
Shadow auto-approve: would not auto-approve. Auto-approval blocked by 2 unresolved issues from previous reviews.
Re-trigger cubic
What
Adds a per-file ACL digest (
permissions_version) so an ACL-only change can trigger reprocessing under incremental (reprocess_all = false), following the mechanism recommended in the spike. This is the connector-layer slice of PLU-511; it pairs with the invoker changes in platform-plugins and the compare/write-back in platform-etl-orchestration (#1250).utils/acl.py—compute_permissions_version(permissions_data, denied_permissions_data=None): canonicalizes (recursive sort) thensha256. ReturnsNoneonly when there is no ACL signal at all; an empty list is a real state (all access revoked) and yields a stable digest.data_types/file_data.py— newpermissions_versionfield onFileDataSourceMetadata.processes/connectors/onedrive.py— the indexer computes the digest over the permissions it already batch-fetches (SharePoint inherits this base class). Adds call-count telemetry proving the digest issues 0 additional Graph calls.test/unit/utils/test_acl.py— determinism / order-independence / empty-vs-None behavior.Validated
Proven end to end on an SND against a real SharePoint site: an ACL-only change (content unchanged) reprocesses exactly the changed file (spike scenarios S2/S8). See PLU-511 for evidence.
Notes
mainand needs a rebase before merge.siteGroupACL changes are not captured (pre-existing PLU-370_extract_identity_ids_from_rawexclusion), so a site-group-only change won't move the digest. Documented in the spike; follow-up TBD.PLU-511
🤖 Generated with Claude Code