Skip to main content

fakecloud_core/
anonymous.rs

1//! How an unsigned (anonymous) request to an AWS API is treated.
2//!
3//! AWS answers most unsigned API requests with a missing-credentials error
4//! before looking at them. A few services authorize the anonymous principal
5//! against resource policies instead (S3, SQS), and the operations the Smithy
6//! models mark unauthenticated (Cognito's user-facing auth flows, STS's
7//! web-identity and SAML federation) are served without credentials.
8
9#[path = "anonymous_gen.rs"]
10mod generated;
11
12/// What a service does with an unsigned request to one of its operations
13/// ([`crate::service::AwsService::anonymous_access`]).
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum AnonymousAccess {
16    /// Refused for its missing credentials before anything else looks at it.
17    Refused,
18    /// Authorized as the anonymous principal against the resource's policy
19    /// (and ACLs), like any other caller.
20    Authorized,
21    /// Needs no credentials: served without authorization.
22    Unauthenticated,
23}
24
25impl AnonymousAccess {
26    /// The model's verdict for `operation` of `service` (by signing name):
27    /// [`AnonymousAccess::Unauthenticated`] for an operation marked
28    /// `smithy.api#optionalAuth` or `smithy.api#auth: []`,
29    /// [`AnonymousAccess::Refused`] otherwise.
30    pub fn for_operation(service: &str, operation: &str) -> Self {
31        if is_unauthenticated_operation(service, operation) {
32            Self::Unauthenticated
33        } else {
34            Self::Refused
35        }
36    }
37}
38
39/// Whether the Smithy model marks `operation` of `service` (its SigV4 signing
40/// name) as callable without credentials.
41pub fn is_unauthenticated_operation(service: &str, operation: &str) -> bool {
42    generated::UNAUTHENTICATED_OPERATIONS
43        .binary_search(&(service, operation))
44        .is_ok()
45}
46
47#[cfg(test)]
48mod tests {
49    use super::*;
50
51    /// Re-derives the table from the vendored models, so a model refresh
52    /// that adds or drops an unauthenticated operation fails here until
53    /// `scripts/generate-unauthenticated-operations.py` is rerun.
54    #[test]
55    fn table_matches_the_models() {
56        let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../aws-models");
57        let mut derived: Vec<(String, String)> = Vec::new();
58        for entry in
59            std::fs::read_dir(&dir).unwrap_or_else(|e| panic!("read {}: {e}", dir.display()))
60        {
61            let path = entry.unwrap().path();
62            if path.extension().is_none_or(|e| e != "json") {
63                continue;
64            }
65            let text = std::fs::read_to_string(&path).unwrap();
66            if !text.contains("smithy.api#optionalAuth") && !text.contains("\"smithy.api#auth\"") {
67                continue;
68            }
69            let model: serde_json::Value = serde_json::from_str(&text).unwrap();
70            let Some(shapes) = model["shapes"].as_object() else {
71                continue;
72            };
73            let Some(service) = shapes.values().find(|s| s["type"] == "service") else {
74                continue;
75            };
76            let Some(signing) = service["traits"]["aws.auth#sigv4"]["name"].as_str() else {
77                continue;
78            };
79            let service_open = service["traits"]["smithy.api#auth"]
80                .as_array()
81                .is_some_and(|a| a.is_empty());
82            for (id, shape) in shapes {
83                if shape["type"] != "operation" {
84                    continue;
85                }
86                let traits = &shape["traits"];
87                let auth = traits.get("smithy.api#auth");
88                let open = traits.get("smithy.api#optionalAuth").is_some()
89                    || auth
90                        .and_then(|a| a.as_array())
91                        .is_some_and(|a| a.is_empty())
92                    || (auth.is_none() && service_open);
93                if open {
94                    let op = id.split_once('#').map_or(id.as_str(), |(_, op)| op);
95                    derived.push((signing.to_string(), op.to_string()));
96                }
97            }
98        }
99        derived.sort();
100        derived.dedup();
101        let table: Vec<(String, String)> = generated::UNAUTHENTICATED_OPERATIONS
102            .iter()
103            .map(|(s, o)| (s.to_string(), o.to_string()))
104            .collect();
105        assert_eq!(
106            table, derived,
107            "rerun scripts/generate-unauthenticated-operations.py"
108        );
109    }
110
111    #[test]
112    fn table_is_sorted_for_binary_search() {
113        assert!(generated::UNAUTHENTICATED_OPERATIONS
114            .windows(2)
115            .all(|w| w[0] < w[1]));
116    }
117
118    #[test]
119    fn model_marked_operations_are_unauthenticated() {
120        for (service, op) in [
121            ("cognito-idp", "InitiateAuth"),
122            ("cognito-idp", "SignUp"),
123            ("cognito-idp", "ConfirmSignUp"),
124            ("cognito-identity", "GetId"),
125            ("cognito-identity", "GetOpenIdToken"),
126            ("cognito-identity", "GetCredentialsForIdentity"),
127            ("sts", "AssumeRoleWithWebIdentity"),
128            ("sts", "AssumeRoleWithSAML"),
129        ] {
130            assert_eq!(
131                AnonymousAccess::for_operation(service, op),
132                AnonymousAccess::Unauthenticated,
133                "{service} {op}"
134            );
135        }
136        for (service, op) in [
137            ("sts", "GetCallerIdentity"),
138            ("sts", "AssumeRoot"),
139            ("cognito-idp", "AdminInitiateAuth"),
140            ("cognito-identity", "DescribeIdentityPool"),
141            ("kms", "ListKeys"),
142        ] {
143            assert_eq!(
144                AnonymousAccess::for_operation(service, op),
145                AnonymousAccess::Refused,
146                "{service} {op}"
147            );
148        }
149    }
150}