Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions visibility-filtering/hydration/viewer_hydrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,12 @@ impl ViewerHydrator {
)
}
Ok(Err(e)) => {
warn!(error = %e, "Gizmoduck viewer lookup failed; failing open");
(false, ViewerAge::Unknown, None)
warn!(error = %e, "Gizmoduck viewer lookup failed; failing closed on age");
(false, ViewerAge::LookupFailed, None)
}
Err(_) => {
warn!("Gizmoduck viewer lookup timed out; failing open");
(false, ViewerAge::Unknown, None)
warn!("Gizmoduck viewer lookup timed out; failing closed on age");
(false, ViewerAge::LookupFailed, None)
}
}
}
Expand Down Expand Up @@ -175,23 +175,27 @@ mod tests {
}

#[tokio::test]
async fn rpc_error_fails_open_to_logged_in_defaults() {
async fn rpc_error_fails_closed_on_age() {
let viewer = hydrate_with_broken_client(ViewerLookup::Fails).await;

assert_eq!(viewer.viewer, Viewer::LoggedIn(123));
assert!(!viewer.allows_sensitive_media);
assert_eq!(viewer.viewer_age, ViewerAge::Unknown);
assert_eq!(viewer.viewer_age, ViewerAge::LookupFailed);
assert!(viewer.viewer_age_lookup_failed());
assert!(!viewer.viewer_is_underage());
assert!(!viewer.viewer_has_no_stated_age());
assert_eq!(viewer.account_country_code, None);
assert_eq!(viewer.country_code.as_deref(), Some("us"));
}

#[tokio::test]
async fn rpc_timeout_fails_open_to_logged_in_defaults() {
async fn rpc_timeout_fails_closed_on_age() {
let viewer = hydrate_with_broken_client(ViewerLookup::Hangs).await;

assert_eq!(viewer.viewer, Viewer::LoggedIn(123));
assert!(!viewer.allows_sensitive_media);
assert_eq!(viewer.viewer_age, ViewerAge::Unknown);
assert_eq!(viewer.viewer_age, ViewerAge::LookupFailed);
assert!(viewer.viewer_age_lookup_failed());
assert_eq!(viewer.account_country_code, None);
assert_eq!(viewer.country_code.as_deref(), Some("us"));
}
Expand Down
10 changes: 10 additions & 0 deletions visibility-filtering/models/viewer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub const ADULT_AGE_YEARS: i32 = 18;
pub enum ViewerAge {
Known(i32),
NotStated,
/// Gizmoduck viewer RPC errored or timed out. Distinct from `Unknown`
/// (no such user) so age gates can fail closed instead of serving NSFW.
LookupFailed,
#[default]
Unknown,
}
Expand All @@ -41,6 +44,13 @@ impl ViewerFeatures {
pub fn viewer_has_no_stated_age(&self) -> bool {
matches!(self.viewer, Viewer::LoggedIn(_)) && self.viewer_age == ViewerAge::NotStated
}

/// Viewer age could not be confirmed because gizmoduck failed. Sensitive
/// media must hard-drop globally (same sink as underage), not fail open
/// as `Unknown` and not jurisdiction-scope as `NotStated`.
pub fn viewer_age_lookup_failed(&self) -> bool {
matches!(self.viewer, Viewer::LoggedIn(_)) && self.viewer_age == ViewerAge::LookupFailed
}
}

impl ViewerFeatures {
Expand Down
5 changes: 5 additions & 0 deletions visibility-filtering/rules/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ impl ViewerPredicates<'_> {
self.ctx.viewer.viewer_has_no_stated_age()
}

#[inline]
pub fn age_lookup_failed(&self) -> bool {
self.ctx.viewer.viewer_age_lookup_failed()
}

#[inline]
pub fn allows_sensitive_media(&self) -> bool {
self.ctx.viewer.allows_sensitive_media
Expand Down
8 changes: 8 additions & 0 deletions visibility-filtering/rules/golden_corpus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,14 @@ fn age_gating_cases() -> Vec<Case> {
expected_action: Allow,
expected_decided_by: None,
},
Case {
name: "gizmoduck_age_lookup_failed_drops_sensitive_media",
level: TimelineHome,
viewer: viewer_with_age(ViewerAge::LookupFailed),
candidate: labeled_media(SafetyLabelType::NSFW_HIGH_RECALL),
expected_action: Drop(FilteredReason::ContainNsfwMedia),
expected_decided_by: Some("SensitiveViewerUnderageDropRule"),
},
]
}

Expand Down
21 changes: 20 additions & 1 deletion visibility-filtering/rules/tweet_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@ fn sensitive_viewer_logged_out(context: &RuleContext<'_>) -> VfAction {
}

fn sensitive_viewer_underage(context: &RuleContext<'_>) -> VfAction {
if context.viewer().is_underage() && sensitive_base_condition(context) {
if (context.viewer().is_underage() || context.viewer().age_lookup_failed())
&& sensitive_base_condition(context)
{
VfAction::Drop(FilteredReason::ContainNsfwMedia)
} else {
VfAction::Allow
Expand Down Expand Up @@ -680,6 +682,23 @@ mod tests {
assert_allows(underage, &gating_viewer(ViewerAge::Unknown), &text);
assert_allows(no_age, &gating_viewer(ViewerAge::Unknown), &text);

for firing in sensitive_firing_candidates() {
assert_drops(
underage,
&gating_viewer(ViewerAge::LookupFailed),
&firing,
&reason,
);
}
assert_allows(no_age, &gating_viewer(ViewerAge::LookupFailed), &hp);
let us_failed = ViewerFeatures {
country_code: Some("us".into()),
..gating_viewer(ViewerAge::LookupFailed)
};
assert_drops(underage, &us_failed, &text, &reason);
let clean = candidate().build();
assert_allows(underage, &gating_viewer(ViewerAge::LookupFailed), &clean);

let opted_in = ViewerFeatures {
allows_sensitive_media: true,
..gating_viewer(ViewerAge::Known(15))
Expand Down