Skip to content
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,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.
- Enable an explicit fail-closed Tauri Content Security Policy to keep executable scripts and fonts local, grant production network authority only to the Tauri IPC transport, confine Vite WebSocket HMR to a separate development-only CSP, deny object/frame/base-URI authority, deny form submissions with explicit `form-action 'none'`, deny unused worker, media, and web-app-manifest fetch authority with explicit `'none'` directives, and regression-test against null, wildcard, remote-script/style, eval, and development-authority leakage.
- 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.
Expand Down
64 changes: 64 additions & 0 deletions docs/architecture/cloud-review-tenant-authority.md
Original file line number Diff line number Diff line change
@@ -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/
4 changes: 2 additions & 2 deletions src-tauri/src/cloud_eviction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 17 additions & 14 deletions src-tauri/src/cloud_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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()));

Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/naruon_lineage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading
Loading