1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
//! Authorization boundary design rules
//!
//! Detects auth-related issues:
//! - Authorization boundary leaks (cross-subgraph auth violations)
//! - Missing @auth directives on sensitive fields
//! - Scope mismatches between subgraphs
use serde_json::Value;
use super::{AuthIssue, DesignAudit, IssueSeverity};
/// Analyze authorization patterns in the schema
pub fn analyze(schema: &Value, audit: &mut DesignAudit) {
check_auth_boundary_leaks(schema, audit);
check_missing_auth_directives(schema, audit);
}
/// Detect authorization boundary leaks
fn check_auth_boundary_leaks(schema: &Value, audit: &mut DesignAudit) {
if let Some(subgraphs) = schema.get("subgraphs").and_then(|v| v.as_array()) {
// First, collect fields that require auth from each type
let mut auth_required_fields: std::collections::HashMap<String, Vec<String>> =
std::collections::HashMap::new();
for subgraph in subgraphs {
if let Some(entities) = subgraph.get("entities").and_then(|v| v.as_array()) {
for entity in entities {
let entity_name = if let Some(name) = entity.as_str() {
name.to_string()
} else if let Some(name) = entity.get("name").and_then(|v| v.as_str()) {
name.to_string()
} else {
continue;
};
if let Some(fields) = entity.get("fields").and_then(|v| v.as_array()) {
for field in fields {
let requires_auth = field
.get("requires_auth")
.and_then(|v| v.as_bool())
.unwrap_or(false);
if requires_auth {
let field_name = field
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
auth_required_fields
.entry(entity_name.clone())
.or_default()
.push(field_name);
}
}
}
}
}
}
// Now check for cross-subgraph references to auth-required fields
for subgraph in subgraphs {
if let Some(references) = subgraph.get("references").and_then(|v| v.as_array()) {
for reference in references {
let target_type = reference
.get("target_type")
.and_then(|v| v.as_str())
.or_else(|| reference.get("target_subgraph").and_then(|v| v.as_str()))
.unwrap_or("unknown");
let accessed_fields =
reference.get("accessed_fields").and_then(|v| v.as_array());
let has_auth_check =
reference.get("has_auth_check").and_then(|v| v.as_bool()).unwrap_or(false);
if !has_auth_check {
if let Some(target_auth_fields) = auth_required_fields.get(target_type) {
if let Some(fields) = accessed_fields {
for field in fields {
if let Some(field_str) = field.as_str() {
if target_auth_fields.contains(&field_str.to_string()) {
audit.auth_issues.push(AuthIssue {
severity: IssueSeverity::Critical,
message: format!(
"Auth boundary leak: Cross-subgraph reference accesses protected field {}.{} without auth check",
target_type, field_str
),
suggestion: "Add authorization check or use auth-scoped references".to_string(),
affected_field: Some(field_str.to_string()),
});
}
}
}
}
}
}
}
}
}
}
}
/// Detect missing auth directives
fn check_missing_auth_directives(schema: &Value, audit: &mut DesignAudit) {
if let Some(types) = schema.get("types").and_then(|v| v.as_array()) {
for type_def in types {
let type_name = type_def.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
if type_name == "Mutation" || type_name == "Subscription" {
if let Some(fields) = type_def.get("fields").and_then(|v| v.as_array()) {
for field in fields {
let requires_auth =
field.get("requires_auth").and_then(|v| v.as_bool()).unwrap_or(false);
if !requires_auth {
let field_name =
field.get("name").and_then(|v| v.as_str()).unwrap_or("unknown");
audit.auth_issues.push(AuthIssue {
severity: IssueSeverity::Warning,
message: format!(
"{}.{} is not protected by auth directive",
type_name, field_name
),
suggestion: "Add @auth directive or authentication requirement"
.to_string(),
affected_field: Some(field_name.to_string()),
});
}
}
}
}
}
}
}