From 76960c1db7707cfe402abd3d96409d3bf8baf0b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 03:47:31 +0900 Subject: [PATCH 1/8] test: expose organization tenant authority fail-open --- .../cloudReviewQueue.authorization.test.ts | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/lib/cloudReviewQueue.authorization.test.ts diff --git a/src/lib/cloudReviewQueue.authorization.test.ts b/src/lib/cloudReviewQueue.authorization.test.ts new file mode 100644 index 000000000..63787e7c2 --- /dev/null +++ b/src/lib/cloudReviewQueue.authorization.test.ts @@ -0,0 +1,118 @@ +import { describe, expect, it } from "vitest"; +import type { CloudCandidate, CloudReviewDecision } from "./api"; +import { + ORGANIZATION_TENANT_AUTHORITY_ATTESTATION, + cloudReviewQueueState, + organizationTenantAuthorityRequired, +} from "./cloudReviewQueue"; + +const ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON = + "organization-cloud-sensitive-context-needs-explicit-tenant-approval"; + +function candidate( + id: string, + overrides: Partial, +): CloudCandidate { + return { + metadata_fingerprint: id.repeat(64), + review_fingerprint: `${id}r`.repeat(32), + src: `/source/${id}.pdf`, + dst: `/cloud/${id}.pdf`, + provider: "icloud", + destination_account_scope: "unknown", + kind: "document", + bytes: 1_024, + age_days: 10, + created_ms: 100, + modified_ms: 200, + production_time_ms: 300, + production_time_source: "filesystem:created", + production_time_confidence: "low", + source_root: "/source", + relative_path: `${id}.pdf`, + source_context: ".", + requires_review: true, + review_reasons: ["destination-account-scope-unknown"], + content_title: null, + content_authors: [], + content_context: [], + duration_ms: null, + dataset_profile: null, + metadata_evidence: [], + blocked_reason: null, + ...overrides, + }; +} + +function approvedDecision( + item: CloudCandidate, + rationale = "metadata reviewed", +): CloudReviewDecision { + return { + version: 2, + decision_id: "d".repeat(64), + candidate_fingerprint: item.metadata_fingerprint, + review_fingerprint: item.review_fingerprint, + disposition: "approved", + reviewed_at_ms: 400, + reviewed_by: "human:local:test", + rationale, + }; +} + +describe("organization tenant authority fail-closed validation", () => { + it("requires tenant authority when either canonical organization signal is present", () => { + const organizationScopeOnly = candidate("a", { + destination_account_scope: "organization", + review_reasons: ["destination-account-scope-unknown"], + }); + const organizationReasonOnly = candidate("b", { + destination_account_scope: "personal", + review_reasons: [ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON], + }); + const ordinaryPersonal = candidate("c", { + destination_account_scope: "personal", + review_reasons: ["personal-cloud-sensitive-context-needs-explicit-approval"], + }); + + expect(organizationTenantAuthorityRequired(organizationScopeOnly)).toBe(true); + expect(organizationTenantAuthorityRequired(organizationReasonOnly)).toBe(true); + expect(organizationTenantAuthorityRequired(ordinaryPersonal)).toBe(false); + }); + + it("refuses approval when either organization signal is present without attestation", () => { + for (const item of [ + candidate("a", { + destination_account_scope: "organization", + review_reasons: ["destination-account-scope-unknown"], + }), + candidate("b", { + destination_account_scope: "personal", + review_reasons: [ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON], + }), + ]) { + expect(cloudReviewQueueState(item, [approvedDecision(item)])).toBe("unreviewed"); + expect(cloudReviewQueueState(item, [approvedDecision( + item, + `${ORGANIZATION_TENANT_AUTHORITY_ATTESTATION} Tenant and destination verified.`, + )])).toBe("approved"); + } + }); + + it("blocks organization signals when the ordinary review flag is absent", () => { + for (const item of [ + candidate("d", { + destination_account_scope: "organization", + requires_review: false, + review_reasons: ["embedded-metadata-probe-incomplete"], + }), + candidate("e", { + destination_account_scope: "personal", + requires_review: false, + review_reasons: [ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON], + }), + ]) { + expect(cloudReviewQueueState(item, [])).toBe("blocked"); + } + }); +}); From 1788a8e43cac3fea45daa05e6e6e8fde6e3841f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 03:47:51 +0900 Subject: [PATCH 2/8] test: expose durable tenant authority fail-open --- .../cloud_transfer_tenant_authority_gate.rs | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 src-tauri/tests/cloud_transfer_tenant_authority_gate.rs diff --git a/src-tauri/tests/cloud_transfer_tenant_authority_gate.rs b/src-tauri/tests/cloud_transfer_tenant_authority_gate.rs new file mode 100644 index 000000000..d6819cda9 --- /dev/null +++ b/src-tauri/tests/cloud_transfer_tenant_authority_gate.rs @@ -0,0 +1,176 @@ +//! Integration coverage for the durable organization-tenant authorization boundary. +//! +//! These tests exercise the public transfer gate rather than duplicating its predicate. They +//! prove that either canonical organization signal is sufficient to require an explicit human +//! tenant-authority attestation before a cloud copy can proceed. + +use disksage_lib::cloud::{ + candidate_review_fingerprint, ArchiveKind, CloudAccountScope, CloudCandidate, CloudProvider, + CloudRoot, MetadataEvidence, ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON, +}; +use disksage_lib::cloud_review::{create_attributed_decision, CloudReviewDisposition}; +use disksage_lib::cloud_transfer::candidate_blockers_with_review; + +#[cfg(windows)] +const CLOUD_ROOT_PATH: &str = r"C:\cloud"; +#[cfg(not(windows))] +const CLOUD_ROOT_PATH: &str = "/cloud"; +#[cfg(windows)] +const SOURCE_PATH: &str = r"C:\source\report.pdf"; +#[cfg(not(windows))] +const SOURCE_PATH: &str = "/source/report.pdf"; +#[cfg(windows)] +const DESTINATION_PATH: &str = r"C:\cloud\DiskSage Archive\report.pdf"; +#[cfg(not(windows))] +const DESTINATION_PATH: &str = "/cloud/DiskSage Archive/report.pdf"; + +/// Build a cloud root whose account scope exactly matches the candidate under test. +fn cloud_root(account_scope: CloudAccountScope) -> CloudRoot { + CloudRoot { + id: format!("icloud:{}", account_scope.as_str()), + provider: CloudProvider::Icloud, + account_scope, + label: "iCloud Drive".into(), + path: CLOUD_ROOT_PATH.into(), + readable: true, + access_issue: None, + } +} + +/// Build a realistic, otherwise eligible candidate with the requested organization signals. +fn candidate( + destination_account_scope: CloudAccountScope, + review_reasons: &[&str], + requires_review: bool, +) -> CloudCandidate { + let mut candidate = CloudCandidate { + metadata_fingerprint: "a".repeat(64), + review_fingerprint: String::new(), + src: SOURCE_PATH.into(), + dst: DESTINATION_PATH.into(), + provider: CloudProvider::Icloud, + destination_account_scope, + kind: ArchiveKind::Document, + bytes: 12, + age_days: 90, + created_ms: 1, + modified_ms: 2, + production_time_ms: 3, + production_time_source: "embedded:exiftool:CreateDate".into(), + production_time_confidence: "high".into(), + source_root: SOURCE_PATH.into(), + relative_path: "report.pdf".into(), + source_context: "source".into(), + requires_review, + review_reasons: review_reasons.iter().map(|reason| (*reason).into()).collect(), + content_title: Some("Report".into()), + content_authors: vec!["Author".into()], + content_context: vec!["Context".into()], + duration_ms: None, + dataset_profile: None, + metadata_evidence: vec![MetadataEvidence { + field: "production_time".into(), + value: "2026-01-01".into(), + source: "exiftool:CreateDate".into(), + confidence: "high".into(), + }], + blocked_reason: None, + }; + candidate.review_fingerprint = candidate_review_fingerprint(&candidate); + candidate +} + +/// Create an exact approved review decision that deliberately lacks tenant-authority attestation. +fn unconfirmed_decision(candidate: &CloudCandidate) -> disksage_lib::cloud_review::CloudReviewDecision { + create_attributed_decision( + candidate, + CloudReviewDisposition::Approved, + 100, + "human:integration-reviewer", + "Candidate metadata and destination were reviewed without organization tenant authority.", + ) + .expect("the realistic review decision should be valid") +} + +#[test] +fn either_organization_signal_requires_explicit_tenant_authority_attestation() { + let cases = [ + ( + "organization scope only", + CloudAccountScope::Organization, + vec!["embedded-metadata-probe-incomplete"], + true, + ), + ( + "organization reason only", + CloudAccountScope::Personal, + vec![ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON], + true, + ), + ( + "both canonical signals", + CloudAccountScope::Organization, + vec![ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON], + true, + ), + ( + "shared scope with organization reason", + CloudAccountScope::Shared, + vec![ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON], + true, + ), + ( + "unknown scope with organization reason", + CloudAccountScope::Unknown, + vec![ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON], + true, + ), + ( + "neither organization signal", + CloudAccountScope::Personal, + vec!["embedded-metadata-probe-incomplete"], + false, + ), + ]; + + for (label, scope, reasons, tenant_authority_required) in cases { + let candidate = candidate(scope, &reasons, true); + let decision = unconfirmed_decision(&candidate); + let blockers = + candidate_blockers_with_review(&candidate, &cloud_root(scope), Some(&decision)); + let blocked = blockers + .iter() + .any(|blocker| blocker == "organization-tenant-authority-attestation-required"); + assert_eq!( + blocked, tenant_authority_required, + "{label} produced blockers: {blockers:?}" + ); + } +} + +#[test] +fn organization_signals_require_tenant_authority_even_without_ordinary_review() { + let cases = [ + ( + "organization scope without ordinary review", + CloudAccountScope::Organization, + vec!["embedded-metadata-probe-incomplete"], + ), + ( + "organization reason without ordinary review", + CloudAccountScope::Personal, + vec![ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON], + ), + ]; + + for (label, scope, reasons) in cases { + let candidate = candidate(scope, &reasons, false); + let blockers = candidate_blockers_with_review(&candidate, &cloud_root(scope), None); + assert!( + blockers + .iter() + .any(|blocker| blocker == "organization-tenant-authority-attestation-required"), + "{label} must fail closed without tenant-authority attestation: {blockers:?}" + ); + } +} From be3a222a62685f22007eb097c0a86d7e4592cdb9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 03:49:53 +0900 Subject: [PATCH 3/8] fix: require tenant authority on either organization signal --- src-tauri/src/cloud_transfer.rs | 31 +++++++++++++++++-------------- src/lib/cloudReviewQueue.ts | 14 ++++++++++++-- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/src-tauri/src/cloud_transfer.rs b/src-tauri/src/cloud_transfer.rs index d8a54ef1c..e5fc42665 100644 --- a/src-tauri/src/cloud_transfer.rs +++ b/src-tauri/src/cloud_transfer.rs @@ -409,7 +409,7 @@ fn candidate_blockers_for_action( let mut exact_review_approved = false; let organization_tenant_authority_required = candidate.destination_account_scope == CloudAccountScope::Organization - && candidate + || candidate .review_reasons .iter() .any(|reason| reason == ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON); @@ -451,6 +451,9 @@ fn candidate_blockers_for_action( Some(_) => exact_review_approved = true, } } + if organization_tenant_authority_required && !candidate.requires_review { + blockers.push("organization-tenant-authority-attestation-required".into()); + } let existing_destination_candidate = candidate.blocked_reason.as_deref() == Some("destination-exists"); if candidate.blocked_reason.is_some() @@ -1457,7 +1460,7 @@ mod tests { CloudRoot { id: "icloud:test".into(), provider: CloudProvider::Icloud, - account_scope: CloudAccountScope::Organization, + account_scope: CloudAccountScope::Personal, label: "iCloud Drive".into(), path: ROOT.into(), readable: true, @@ -1472,7 +1475,7 @@ mod tests { src: SOURCE.into(), dst: DESTINATION.into(), provider: CloudProvider::Icloud, - destination_account_scope: CloudAccountScope::Organization, + destination_account_scope: CloudAccountScope::Personal, kind: ArchiveKind::Document, bytes: 12, age_days: 90, @@ -1684,7 +1687,7 @@ mod tests { .contains(&"source-equals-destination".to_string())); let mut changed_scope = root(); - changed_scope.account_scope = CloudAccountScope::Personal; + changed_scope.account_scope = CloudAccountScope::Organization; assert!(candidate_blockers(&candidate(), &changed_scope) .contains(&"destination-account-scope-mismatch".to_string())); @@ -1900,7 +1903,7 @@ mod tests { CloudReviewDisposition::Approved, 11, "human:local:reviewer", - "Metadata title, account scope, and destination reviewed.", + "[organization-tenant-authority-confirmed] Metadata title, account scope, and destination reviewed.", ) .unwrap(); assert!(candidate_blockers_with_review(&reviewed, &root(), Some(&approved)).is_empty()); @@ -1929,7 +1932,7 @@ mod tests { ); assert_eq!( reviewed_lineage.review_rationale.as_deref(), - Some("Metadata title, account scope, and destination reviewed.") + Some("[organization-tenant-authority-confirmed] Metadata title, account scope, and destination reviewed.") ); let mut organization_sensitive = reviewed.clone(); @@ -2010,7 +2013,7 @@ mod tests { CloudReviewDisposition::Approved, 13, "human:local:reviewer", - "Filename date is auxiliary; destination and surrounding context were reviewed.", + "[organization-tenant-authority-confirmed] Filename date is auxiliary; destination and surrounding context were reviewed.", ) .unwrap(); assert!( @@ -2251,7 +2254,7 @@ mod tests { let test_root = CloudRoot { id: "icloud:test".into(), provider: CloudProvider::Icloud, - account_scope: CloudAccountScope::Organization, + account_scope: CloudAccountScope::Personal, label: "iCloud Drive".into(), path: cloud.to_string_lossy().into_owned(), readable: true, @@ -2327,7 +2330,7 @@ mod tests { let test_root = CloudRoot { id: "icloud:test".into(), provider: CloudProvider::Icloud, - account_scope: CloudAccountScope::Organization, + account_scope: CloudAccountScope::Personal, label: "iCloud Drive".into(), path: cloud.to_string_lossy().into_owned(), readable: true, @@ -2381,7 +2384,7 @@ mod tests { let test_root = CloudRoot { id: "icloud:test".into(), provider: CloudProvider::Icloud, - account_scope: CloudAccountScope::Organization, + account_scope: CloudAccountScope::Personal, label: "iCloud Drive".into(), path: cloud.to_string_lossy().into_owned(), readable: true, @@ -2427,7 +2430,7 @@ mod tests { let test_root = CloudRoot { id: "icloud:test".into(), provider: CloudProvider::Icloud, - account_scope: CloudAccountScope::Organization, + account_scope: CloudAccountScope::Personal, label: "iCloud Drive".into(), path: cloud.to_string_lossy().into_owned(), readable: true, @@ -2483,7 +2486,7 @@ mod tests { let test_root = CloudRoot { id: "icloud:test".into(), provider: CloudProvider::Icloud, - account_scope: CloudAccountScope::Organization, + account_scope: CloudAccountScope::Personal, label: "iCloud Drive".into(), path: cloud.to_string_lossy().into_owned(), readable: true, @@ -2527,7 +2530,7 @@ mod tests { let test_root = CloudRoot { id: "icloud:test".into(), provider: CloudProvider::Icloud, - account_scope: CloudAccountScope::Organization, + account_scope: CloudAccountScope::Personal, label: "iCloud Drive".into(), path: cloud.to_string_lossy().into_owned(), readable: true, @@ -2568,7 +2571,7 @@ mod tests { let test_root = CloudRoot { id: "icloud:test".into(), provider: CloudProvider::Icloud, - account_scope: CloudAccountScope::Organization, + account_scope: CloudAccountScope::Personal, label: "iCloud Drive".into(), path: cloud.to_string_lossy().into_owned(), readable: true, diff --git a/src/lib/cloudReviewQueue.ts b/src/lib/cloudReviewQueue.ts index befa7aa14..3712cced8 100644 --- a/src/lib/cloudReviewQueue.ts +++ b/src/lib/cloudReviewQueue.ts @@ -44,13 +44,22 @@ export interface CloudReviewQueuePage { totalItems: number; } +/** + * Reports whether an approval needs explicit organization-tenant authority. + * + * The destination scope and the backend-authored review reason are independent + * safety signals. Either signal is sufficient to require the attestation so a + * missing or contradictory field cannot make an organization-sensitive + * candidate easier to approve. Only a candidate with neither signal uses the + * ordinary approval contract. + */ export function organizationTenantAuthorityRequired(candidate: CloudCandidate): boolean { return candidate.destination_account_scope === "organization" - && candidate.review_reasons.includes(ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON); + || candidate.review_reasons.includes(ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON); } function isReviewFormatControl(character: string): boolean { - const codepoint = character.codePointAt(0) ?? -1; + const codepoint = character.codePointAt(0)!; return codepoint === 0x00ad || (codepoint >= 0x0600 && codepoint <= 0x0605) || [0x061c, 0x06dd, 0x070f, 0x08e2, 0x180e, 0xfeff].includes(codepoint) @@ -163,6 +172,7 @@ export function cloudReviewQueueState( decisions: CloudReviewDecision[], ): CloudReviewQueueState { if (candidate.blocked_reason !== null) return "blocked"; + if (!candidate.requires_review && organizationTenantAuthorityRequired(candidate)) return "blocked"; if (!candidate.requires_review) return "ready"; return matchingReviewDecision(candidate, decisions)?.disposition ?? "unreviewed"; } From 0f411d173643a3e8a727745599cb23bd9d020ef0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 03:50:24 +0900 Subject: [PATCH 4/8] test: align organization lineage review fixture --- src-tauri/src/naruon_lineage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src-tauri/src/naruon_lineage.rs b/src-tauri/src/naruon_lineage.rs index 2ebbb0765..bef5ba30a 100644 --- a/src-tauri/src/naruon_lineage.rs +++ b/src-tauri/src/naruon_lineage.rs @@ -501,7 +501,7 @@ mod tests { CloudReviewDisposition::Approved, 25, "human:local:test", - "embedded metadata checked", + "[organization-tenant-authority-confirmed] embedded metadata checked", ) .unwrap(); let root = CloudRoot { From d896a9922409c5c87bbba34e7f5a232767734e64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 03:50:58 +0900 Subject: [PATCH 5/8] docs: record fail-closed tenant authority gate --- .../cloud-review-tenant-authority.md | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/architecture/cloud-review-tenant-authority.md diff --git a/docs/architecture/cloud-review-tenant-authority.md b/docs/architecture/cloud-review-tenant-authority.md new file mode 100644 index 000000000..93a1c5f0f --- /dev/null +++ b/docs/architecture/cloud-review-tenant-authority.md @@ -0,0 +1,64 @@ +# Cloud review tenant-authority decision + +## Status + +Accepted for the cloud review queue and durable Rust transfer gate. This document records both the frontend projection and the trusted mutation boundary; the frontend remains incapable of granting durable mutation authorization. + +## Context + +A cloud candidate carries two independent signals that an approval needs organization-tenant authority: + +1. `destination_account_scope` identifies an organization destination. +2. `review_reasons` contains `organization-cloud-sensitive-context-needs-explicit-tenant-approval` when the candidate evidence requires explicit tenant review. + +The previous predicate required both signals simultaneously. A missing, contradictory, stale, or malformed value in either field therefore made the approval path less restrictive. An approved decision with no tenant-authority attestation could become execution-ready even though the remaining signal still identified organization-sensitive handling. A candidate whose ordinary `requires_review` flag was false could also bypass the tenant-authority requirement entirely. + +This is an incorrect-authorization pattern: an authorization decision must not become more permissive because one of two security attributes is absent or contradictory. NIST SP 800-53 AC-3 requires access enforcement according to applicable policy, OWASP ASVS 5.0.0 treats authorization as an independently verified security control, and CWE-863 describes the broader weakness class in which an authorization check is performed incorrectly. + +## Decision + +Both TypeScript review projection and Rust transfer authorization use fail-closed disjunction: + +```text +organization destination scope +OR organization-sensitive tenant review reason +=> explicit organization-tenant authority attestation required +``` + +Either signal is sufficient. Only a candidate with neither signal follows the ordinary approval contract. + +An approved decision is accepted only when its rationale starts with the exact backend-defined marker `[organization-tenant-authority-confirmed]` followed by exactly one U+0020 ASCII space whenever the predicate is true. Held decisions remain admissible without the marker because they grant no execution-ready approval. If either organization signal is present while `requires_review` is false, both frontend and Rust fail closed instead of treating the candidate as ready. Candidate and decision fingerprints, reviewer attribution, rationale validation, copy-approval freshness, exact confirmation phrase, provider/account scope, and all other durable Rust authorization checks remain mandatory and independent. + +## Security invariants + +- Missing or contradictory organization signals increase or preserve restrictions; they never reduce them. +- A candidate with organization scope but without the organization review reason still requires tenant authority. +- A candidate with the organization review reason but a non-organization scope still requires tenant authority. +- Organization-sensitive evidence cannot bypass tenant authority merely because `requires_review` is false. +- A candidate with neither organization signal does not receive an organization-only prompt or blocker. +- A valid tenant attestation cannot replace exact candidate, review, destination, provider, account-scope, expiry, confirmation-phrase, or durable authorization binding. +- The frontend projection cannot mint, refresh, persist, or extend mutation authority. + +## Test-first evidence + +The TypeScript RED commit `76960c1db7707cfe402abd3d96409d3bf8baf0b6` introduced scope-only, reason-only, missing-attestation, and `requires_review = false` regressions before production changed. The Rust RED commit `1788a8e43cac3fea45daa05e6e6e8fde6e3841f8` exercised the public durable transfer gate for the same signal matrix. The production GREEN commit `be3a222a62685f22007eb097c0a86d7e4592cdb9` applies the disjunctive requirement in both frontend and Rust and blocks organization-sensitive candidates without an ordinary review flag. The follow-up `0f411d173643a3e8a727745599cb23bd9d020ef0` aligns an existing organization-scoped Naruon lineage fixture with the stricter attestation contract rather than weakening the gate. + +No predecessor-head CI, review, or approval evidence authorizes these commits. The unchanged exact head must independently pass repository Test and Release workflows, current security/SAST gates, exact production coverage, actionable review closure, branch/ruleset policy, and any qualifying independent approval required by live policy or explicit governance. + +## Rollback + +Rollback is a reviewed security-boundary change. Reverting only the disjunctive predicate or the no-ordinary-review blocker would knowingly restore the fail-open condition and is prohibited. A justified rollback must revert the production behavior, both regression suites, this decision record, and the matching changelog evidence together, and must introduce an independently reviewed replacement authorization contract that remains at least as restrictive. No database migration or persisted-schema rollback is involved. + +## Standalone and CWL integration boundary + +The tenant-authority gate is local to DiskSage's review and transfer authorization. It does not require Naruon, contextual-orchestrator, or a central CWL runtime to function. Naruon may consume bounded lineage or readiness evidence, but it cannot manufacture the tenant attestation or bypass DiskSage's exact Rust transfer checks. Central organization workflows may verify the implementation as repository evidence; they do not become runtime authorization. + +## References + +Joint Task Force. (2020). *Security and privacy controls for information systems and organizations* (NIST Special Publication 800-53 Rev. 5). National Institute of Standards and Technology. https://doi.org/10.6028/NIST.SP.800-53r5 + +National Institute of Standards and Technology. (2025, August 27). *NIST releases revision to SP 800-53 controls*. https://csrc.nist.gov/News/2025/nist-releases-revision-to-sp-800-53-controls + +MITRE. (2026). *CWE-863: Incorrect authorization* (CWE Version 4.20). https://cwe.mitre.org/data/definitions/863.html + +OWASP Foundation. (2025). *OWASP Application Security Verification Standard* (Version 5.0.0). https://owasp.org/www-project-application-security-verification-standard/ From dcceac0cfd1ebf21df774258b48be6ac4c0e26e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 03:51:15 +0900 Subject: [PATCH 6/8] docs: record tenant authority hardening --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 033f9bae4..9b18c8ca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and ### Security +- Require explicit organization-tenant authority when either the destination account scope is organization-owned or the canonical organization-sensitive review reason is present; fail closed in both frontend projection and durable Rust transfer authorization even when the ordinary review flag is absent, and regression-test contradictory signal combinations. - Re-verify the installed GGUF immediately before llama.cpp initialization and retain the verified model handle through llama.cpp loading: reject missing, linked, non-regular, identity-raced, short, oversized, unreadable, or SHA-256-mismatched artifacts with stable path-free errors; use a stable descriptor path on Unix and a Windows read-sharing guard so the mutable source pathname cannot be substituted between verification and model parsing. - Bind the default on-device GGUF model to an immutable upstream revision, exact byte count, and SHA-256 digest; replace whole-model buffering and named sibling staging with bounded streaming into an unnamed same-directory temporary file; ignore and preserve unrelated legacy `.part` paths; refuse destination overwrite with create-new semantics; capture destination ownership from the returned open file handle; re-read and rehash the still-open staging source while copying; flush, sync, re-read, and rehash the destination before final acceptance; reject same-file source or destination mutation; preserve foreign destination replacements through identity-bound cleanup; and keep model installation inside the Rust coverage surface with privacy-safe stable errors and deterministic race regressions. - Persist copy-approval provenance in immutable receipt lineage, reject stale, generic, mismatched, or tampered approvals, and retain explicit backward readability for pre-approval receipt formats. From 0ff788ec0f3c56134a81f904f268cd29b937d388 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 07:10:50 +0900 Subject: [PATCH 7/8] test: align cloud eviction fixture with tenant authority --- src-tauri/src/cloud_eviction.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/cloud_eviction.rs b/src-tauri/src/cloud_eviction.rs index aeb8468a1..b2fbabe06 100644 --- a/src-tauri/src/cloud_eviction.rs +++ b/src-tauri/src/cloud_eviction.rs @@ -880,7 +880,7 @@ mod tests { src: source.to_string_lossy().into_owned(), dst: destination.to_string_lossy().into_owned(), provider: CloudProvider::Onedrive, - destination_account_scope: crate::cloud::CloudAccountScope::Organization, + destination_account_scope: crate::cloud::CloudAccountScope::Personal, kind: ArchiveKind::Document, bytes: metadata.len(), age_days: 1, @@ -911,7 +911,7 @@ mod tests { let root = CloudRoot { id: cloud_dir.to_string_lossy().into_owned(), provider: CloudProvider::Onedrive, - account_scope: crate::cloud::CloudAccountScope::Organization, + account_scope: crate::cloud::CloudAccountScope::Personal, label: "test".into(), path: cloud_dir.to_string_lossy().into_owned(), readable: true, From 7c86c89198da81f409fda84dfc033184012af9af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 10 Aug 2026 09:57:43 +0900 Subject: [PATCH 8/8] test: cover organization-only tenant approval path --- .../cloud_transfer_tenant_authority_gate.rs | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src-tauri/tests/cloud_transfer_tenant_authority_gate.rs b/src-tauri/tests/cloud_transfer_tenant_authority_gate.rs index d6819cda9..846481faf 100644 --- a/src-tauri/tests/cloud_transfer_tenant_authority_gate.rs +++ b/src-tauri/tests/cloud_transfer_tenant_authority_gate.rs @@ -8,7 +8,9 @@ use disksage_lib::cloud::{ candidate_review_fingerprint, ArchiveKind, CloudAccountScope, CloudCandidate, CloudProvider, CloudRoot, MetadataEvidence, ORGANIZATION_TENANT_AUTHORITY_REVIEW_REASON, }; -use disksage_lib::cloud_review::{create_attributed_decision, CloudReviewDisposition}; +use disksage_lib::cloud_review::{ + create_attributed_decision, CloudReviewDisposition, ORGANIZATION_TENANT_AUTHORITY_ATTESTATION, +}; use disksage_lib::cloud_transfer::candidate_blockers_with_review; #[cfg(windows)] @@ -92,6 +94,21 @@ fn unconfirmed_decision(candidate: &CloudCandidate) -> disksage_lib::cloud_revie .expect("the realistic review decision should be valid") } +/// Create an exact approved decision with the canonical tenant-authority attestation marker. +fn confirmed_decision(candidate: &CloudCandidate) -> disksage_lib::cloud_review::CloudReviewDecision { + let rationale = format!( + "{ORGANIZATION_TENANT_AUTHORITY_ATTESTATION} Organization tenant authority was independently confirmed." + ); + create_attributed_decision( + candidate, + CloudReviewDisposition::Approved, + 100, + "human:integration-reviewer", + &rationale, + ) + .expect("the tenant-authority decision should be valid") +} + #[test] fn either_organization_signal_requires_explicit_tenant_authority_attestation() { let cases = [ @@ -148,6 +165,28 @@ fn either_organization_signal_requires_explicit_tenant_authority_attestation() { } } +#[test] +fn organization_scope_only_accepts_exact_tenant_authority_attestation() { + let candidate = candidate( + CloudAccountScope::Organization, + &["embedded-metadata-probe-incomplete"], + true, + ); + let decision = confirmed_decision(&candidate); + let blockers = candidate_blockers_with_review( + &candidate, + &cloud_root(CloudAccountScope::Organization), + Some(&decision), + ); + + assert!( + !blockers + .iter() + .any(|blocker| blocker == "organization-tenant-authority-attestation-required"), + "organization scope alone must not require an additional canonical review reason after exact attestation: {blockers:?}" + ); +} + #[test] fn organization_signals_require_tenant_authority_even_without_ordinary_review() { let cases = [