Skip to content

Commit 6b0c9a9

Browse files
gustavoavenameta-codesync[bot]
authored andcommitted
restricted_paths: propagate config source errors
Summary: ## This stack This stack moves restricted paths toward AclManifest-backed restriction lookup and Shadow-mode comparison logging while keeping existing `RestrictedPaths` public APIs and config-backed enforcement behavior stable. The end state is that path and manifest access checks can evaluate both the legacy config/manifest-id-store source and the new AclManifest source, log source-prefixed comparison results to Scuba, and still return config-authoritative authorization results while Shadow mode is being validated. ## This diff (behaviour change) This diff changes Shadow source-comparison logging when the config source fails. Previously, the logging helper could emit an error-only Scuba row for a config-source failure, leaving no config-authoritative authorization result to return. After this diff, `log_source_results_to_scuba` requires a config result and, after determining that a source-comparison row would be relevant, maps an `Err` config result into an `anyhow::Error` and returns it before writing Scuba fields. The behavior change is intentional because Shadow mode remains config-authoritative: AclManifest errors are comparison telemetry when config succeeds, but a config-source error means the authoritative source failed and should be propagated instead of silently failing open. Reviewed By: lmvasquezg Differential Revision: D104046886 fbshipit-source-id: c049b1cb2ea6e3182a062836621da826bf536a4f
1 parent d62bab7 commit 6b0c9a9

2 files changed

Lines changed: 163 additions & 137 deletions

File tree

eden/mononoke/repo_attributes/restricted_paths/src/access_log.rs

Lines changed: 100 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ struct RestrictedPathAggregateLogData<'a> {
7474
}
7575

7676
/// Complete Scuba row payload for restricted-path access logging.
77-
///
78-
/// `aggregate` is optional in this no-op refactor because Shadow source
79-
/// comparison still emits error-only rows when config fails. The follow-up
80-
/// behavior diff can make it required after those rows stop being logged.
8177
struct RestrictedPathLogData<'a> {
8278
repo_id: RepositoryId,
8379
access_data: RestrictedPathAccessData,
@@ -256,8 +252,8 @@ pub async fn is_part_of_group(
256252
/// This emits one row when a Shadow-mode lookup source either finds a
257253
/// restriction or fails. Aggregate authorization fields remain
258254
/// config-authoritative, while source disagreement is summarized with compact
259-
/// mismatch fields. If neither source ran, or every source that ran completed
260-
/// unrestricted, no row is written.
255+
/// mismatch fields. If every available source completed unrestricted, no row
256+
/// is written.
261257
#[cfg_attr(
262258
not(test),
263259
expect(
@@ -268,7 +264,7 @@ pub async fn is_part_of_group(
268264
pub(crate) fn log_source_results_to_scuba(
269265
ctx: &CoreContext,
270266
repo_id: RepositoryId,
271-
config_result: Option<&SourceRestrictionResult>,
267+
config_result: &SourceRestrictionResult,
272268
acl_manifest_result: Option<&SourceRestrictionResult>,
273269
acl_manifest_mode: AclManifestMode,
274270
access_data: RestrictedPathAccessData,
@@ -285,52 +281,60 @@ pub(crate) fn log_source_results_to_scuba(
285281
RestrictedPathLogData {
286282
repo_id,
287283
access_data,
288-
aggregate: config_result.and_then(|result| {
289-
let result = result.as_ref().ok()?;
290-
Some(RestrictedPathAggregateLogData {
291-
restricted_paths: result.restriction_paths.as_deref(),
284+
aggregate: config_result
285+
.as_ref()
286+
.ok()
287+
.map(|config| RestrictedPathAggregateLogData {
288+
restricted_paths: config.restriction_paths.as_deref(),
292289
authorization: LoggedAuthorization {
293-
has_authorization: result.has_authorization,
294-
is_allowlisted_tooling: result.is_allowlisted_tooling,
295-
is_rollout_allowlisted: result.is_rollout_allowlisted,
296-
has_acl_access: result.has_acl_access,
290+
has_authorization: config.has_authorization,
291+
is_allowlisted_tooling: config.is_allowlisted_tooling,
292+
is_rollout_allowlisted: config.is_rollout_allowlisted,
293+
has_acl_access: config.has_acl_access,
297294
},
298-
acls: result.restriction_acls.iter().collect(),
299-
})
300-
}),
295+
acls: config.restriction_acls.iter().collect(),
296+
}),
301297
considered_restricted_by: considered_restricted_by_for_source_results(
302298
config_result,
303299
acl_manifest_result,
304300
),
305301
source_comparison: Some(source_comparison),
306302
},
307303
scuba,
308-
)
304+
)?;
305+
306+
config_result
307+
.as_ref()
308+
.map(|_| ())
309+
.map_err(|err| anyhow::anyhow!("{:#}", err))
309310
}
310311

311312
/// Build the source-comparison fields for rows that need Shadow telemetry.
312313
///
313314
/// Returns `None` when every source that ran completed unrestricted, matching
314315
/// the old behavior of skipping those rows entirely.
315316
fn source_comparison_log_context(
316-
config_result: Option<&SourceRestrictionResult>,
317+
config_result: &SourceRestrictionResult,
317318
acl_manifest_result: Option<&SourceRestrictionResult>,
318319
acl_manifest_mode: AclManifestMode,
319320
) -> Option<SourceComparisonLogContext> {
320321
let any_source_restricted =
321-
is_source_restricted(config_result) || is_source_restricted(acl_manifest_result);
322-
let any_source_failed = is_source_error(config_result) || is_source_error(acl_manifest_result);
322+
is_source_restricted(Some(config_result)) || is_source_restricted(acl_manifest_result);
323+
let any_source_failed =
324+
is_source_error(Some(config_result)) || is_source_error(acl_manifest_result);
323325
if !any_source_restricted && !any_source_failed {
324326
return None;
325327
}
326328

327329
Some(SourceComparisonLogContext {
328330
acl_manifest_mode,
329-
config_error: config_result
330-
.and_then(|result| result.as_ref().err().map(|err| format!("{:#}", err))),
331+
config_error: config_result.as_ref().err().map(|err| format!("{:#}", err)),
331332
acl_manifest_error: acl_manifest_result
332333
.and_then(|result| result.as_ref().err().map(|err| format!("{:#}", err))),
333-
shadow_mismatch: shadow_mismatch_for_source_results(config_result, acl_manifest_result),
334+
shadow_mismatch: shadow_mismatch_for_source_results(
335+
Some(config_result),
336+
acl_manifest_result,
337+
),
334338
shadow_mismatch_detail: shadow_mismatch_detail_for_source_results(
335339
config_result,
336340
acl_manifest_result,
@@ -339,17 +343,17 @@ fn source_comparison_log_context(
339343
}
340344

341345
fn considered_restricted_by_for_source_results(
342-
config_result: Option<&SourceRestrictionResult>,
346+
config_result: &SourceRestrictionResult,
343347
acl_manifest_result: Option<&SourceRestrictionResult>,
344348
) -> Vec<String> {
345-
[
346-
(SourceKind::Config, config_result),
347-
(SourceKind::AclManifest, acl_manifest_result),
348-
]
349-
.into_iter()
350-
.filter(|&(_, result)| is_source_restricted(result))
351-
.map(|(source, _)| source.as_scuba_value().to_string())
352-
.collect()
349+
let mut restricted_sources = Vec::new();
350+
if is_source_restricted(Some(config_result)) {
351+
restricted_sources.push(SourceKind::Config.as_scuba_value().to_string());
352+
}
353+
if is_source_restricted(acl_manifest_result) {
354+
restricted_sources.push(SourceKind::AclManifest.as_scuba_value().to_string());
355+
}
356+
restricted_sources
353357
}
354358

355359
fn is_source_restricted(result: Option<&SourceRestrictionResult>) -> bool {
@@ -361,7 +365,7 @@ fn is_source_error(result: Option<&SourceRestrictionResult>) -> bool {
361365
}
362366

363367
fn shadow_mismatch_detail_for_source_results(
364-
config_result: Option<&SourceRestrictionResult>,
368+
config_result: &SourceRestrictionResult,
365369
acl_manifest_result: Option<&SourceRestrictionResult>,
366370
) -> Option<String> {
367371
let differences = shadow_mismatch_differences(config_result, acl_manifest_result);
@@ -370,7 +374,7 @@ fn shadow_mismatch_detail_for_source_results(
370374
}
371375

372376
let mut detail = serde_json::Map::new();
373-
if let Some(config_detail) = source_result_detail(config_result) {
377+
if let Some(config_detail) = source_result_detail(Some(config_result)) {
374378
detail.insert("config".to_string(), config_detail);
375379
}
376380
if let Some(acl_manifest_detail) = source_result_detail(acl_manifest_result) {
@@ -407,8 +411,8 @@ fn shadow_mismatch_for_source_results(
407411
return true;
408412
}
409413

410-
successful_source_comparison(config_result)
411-
.zip(successful_source_comparison(acl_manifest_result))
414+
successful_source_result_comparison(config_result)
415+
.zip(successful_source_result_comparison(acl_manifest_result))
412416
.is_some_and(|(config, acl_manifest)| {
413417
restriction_comparison(config) != restriction_comparison(acl_manifest)
414418
})
@@ -419,21 +423,17 @@ fn source_error_status(result: Option<&SourceRestrictionResult>) -> Option<bool>
419423
}
420424

421425
fn shadow_mismatch_differences(
422-
config_result: Option<&SourceRestrictionResult>,
426+
config_result: &SourceRestrictionResult,
423427
acl_manifest_result: Option<&SourceRestrictionResult>,
424428
) -> Vec<&'static str> {
425429
let error_differences = [
426-
(SourceKind::Config, config_result),
427-
(SourceKind::AclManifest, acl_manifest_result),
428-
]
429-
.into_iter()
430-
.filter_map(|(source_kind, result)| {
431-
is_source_error(result).then_some(source_kind.error_field())
432-
});
430+
is_source_error(Some(config_result)).then_some(SourceKind::Config.error_field()),
431+
is_source_error(acl_manifest_result).then_some(SourceKind::AclManifest.error_field()),
432+
];
433433

434434
let successful_source_differences = match (
435-
successful_source_comparison(config_result),
436-
successful_source_comparison(acl_manifest_result),
435+
successful_source_result_comparison(Some(config_result)),
436+
successful_source_result_comparison(acl_manifest_result),
437437
) {
438438
(Some(config), Some(acl_manifest)) => [
439439
(config.restricted != acl_manifest.restricted, "restricted"),
@@ -457,43 +457,53 @@ fn shadow_mismatch_differences(
457457
};
458458

459459
error_differences
460+
.into_iter()
461+
.flatten()
460462
.chain(successful_source_differences)
461463
.collect()
462464
}
463465

464466
fn source_result_detail(result: Option<&SourceRestrictionResult>) -> Option<Value> {
465467
match result? {
466-
Ok(result) => Some(json!({
467-
"restricted": result.is_restricted(),
468-
"has_authorization": result.has_authorization,
469-
"restriction_acls": sorted_acls_to_strings(&result.restriction_acls),
470-
"restriction_paths": result
471-
.restriction_paths
472-
.as_deref()
473-
.map(sorted_paths_to_strings),
474-
})),
468+
Ok(result) => Some(successful_source_detail(result)),
475469
Err(err) => Some(json!({
476470
"error": format!("{:#}", err),
477471
})),
478472
}
479473
}
480474

481-
fn successful_source_comparison(
475+
fn successful_source_detail(result: &SourceRestrictionCheckResult) -> Value {
476+
json!({
477+
"restricted": result.is_restricted(),
478+
"has_authorization": result.has_authorization,
479+
"restriction_acls": sorted_acls_to_strings(&result.restriction_acls),
480+
"restriction_paths": result
481+
.restriction_paths
482+
.as_deref()
483+
.map(sorted_paths_to_strings),
484+
})
485+
}
486+
487+
fn successful_source_result_comparison(
482488
result: Option<&SourceRestrictionResult>,
483489
) -> Option<SourceComparisonData> {
484490
let result = match result? {
485491
Ok(result) => result,
486492
Err(_) => return None,
487493
};
488-
Some(SourceComparisonData {
494+
Some(successful_source_comparison(result))
495+
}
496+
497+
fn successful_source_comparison(result: &SourceRestrictionCheckResult) -> SourceComparisonData {
498+
SourceComparisonData {
489499
restricted: result.is_restricted(),
490500
has_authorization: result.has_authorization,
491501
restriction_acls: sorted_acls_to_strings(&result.restriction_acls),
492502
restriction_paths: result
493503
.restriction_paths
494504
.as_deref()
495505
.map(sorted_paths_to_strings),
496-
})
506+
}
497507
}
498508

499509
fn acls_to_strings(acls: &[MononokeIdentity]) -> Vec<String> {
@@ -786,44 +796,45 @@ fn log_checked_access_to_restricted_path(
786796
// Override sampling for unauthorized SCSC accesses to restricted paths
787797
#[cfg(fbcode_build)]
788798
{
789-
if let Some(aggregate) = log_data.aggregate.as_ref() {
790-
use clientinfo::ClientEntryPoint;
799+
use clientinfo::ClientEntryPoint;
791800

792-
let is_scsc = ctx
793-
.metadata()
794-
.client_request_info()
795-
.is_some_and(|cri| cri.entry_point == ClientEntryPoint::ScsClient);
801+
let is_scsc = ctx
802+
.metadata()
803+
.client_request_info()
804+
.is_some_and(|cri| cri.entry_point == ClientEntryPoint::ScsClient);
796805

797-
if is_scsc && !aggregate.authorization.has_authorization {
798-
ctx.set_override_sampling();
799-
}
806+
if let Some(aggregate) = log_data.aggregate.as_ref()
807+
&& is_scsc
808+
&& !aggregate.authorization.has_authorization
809+
{
810+
ctx.set_override_sampling();
800811
}
801812
}
802813

803814
// Log to schematized logger (logs to both Scuba and Hive) if enabled via JK
804815
// Only available in fbcode builds
805816
#[cfg(fbcode_build)]
806817
{
807-
if let Some(aggregate) = log_data.aggregate.as_ref() {
808-
let use_schematized_logger = justknobs::eval(
809-
"scm/mononoke:restricted_paths_use_schematized_logger",
810-
None,
811-
None,
812-
)?;
813-
814-
if use_schematized_logger {
815-
if let Err(e) = schematized_logger::log_access_to_schematized_logger(
816-
ctx,
817-
log_data.repo_id,
818-
aggregate.restricted_paths.unwrap_or(&[]),
819-
&log_data.access_data,
820-
aggregate.authorization.has_authorization,
821-
aggregate.authorization.is_allowlisted_tooling,
822-
aggregate.authorization.is_rollout_allowlisted,
823-
&aggregate.acls,
824-
) {
825-
tracing::error!("Failed to log to schematized logger: {:?}", e);
826-
}
818+
let use_schematized_logger = justknobs::eval(
819+
"scm/mononoke:restricted_paths_use_schematized_logger",
820+
None,
821+
None,
822+
)?;
823+
824+
if let Some(aggregate) = log_data.aggregate.as_ref()
825+
&& use_schematized_logger
826+
{
827+
if let Err(e) = schematized_logger::log_access_to_schematized_logger(
828+
ctx,
829+
log_data.repo_id,
830+
aggregate.restricted_paths.unwrap_or(&[]),
831+
&log_data.access_data,
832+
aggregate.authorization.has_authorization,
833+
aggregate.authorization.is_allowlisted_tooling,
834+
aggregate.authorization.is_rollout_allowlisted,
835+
&aggregate.acls,
836+
) {
837+
tracing::error!("Failed to log to schematized logger: {:?}", e);
827838
}
828839
}
829840
}

0 commit comments

Comments
 (0)