cookie_agent_models/recipes/
auth.rs1use std::collections::{BTreeMap, BTreeSet};
2
3use cookie_agent_identity::{AuthFieldName, AuthParameterId};
4use serde::Serialize;
5
6use crate::authoring::{AuthDefinition, AuthOverride, SafeAuthParameterValue, SecretString};
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
9#[serde(rename_all = "snake_case")]
10pub enum CredentialKind {
11 ApiKey,
12 AccessToken,
13 AccessKeyId,
14 SecretAccessKey,
15 SessionToken,
16}
17
18#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
19pub struct AuthCredential {
20 pub name: &'static str,
21 pub kind: CredentialKind,
22 pub required: bool,
23}
24
25#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum WireAuth {
28 None,
29 BearerApiKey,
30 HeaderApiKey,
31 AnthropicApiKey,
32 GoogleApiKey,
33 OAuthAccessToken,
34 AwsSigV4Static,
35 AzureApiKey,
36}
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)]
39pub struct AuthMethodRecipe {
40 pub id: &'static str,
41 pub credentials: &'static [AuthCredential],
42 pub wire: WireAuth,
43 pub owned_headers: &'static [&'static str],
44}
45
46const API_KEY: &[AuthCredential] = &[AuthCredential {
47 name: "api_key",
48 kind: CredentialKind::ApiKey,
49 required: true,
50}];
51const ACCESS_TOKEN: &[AuthCredential] = &[AuthCredential {
52 name: "access_token",
53 kind: CredentialKind::AccessToken,
54 required: true,
55}];
56const AWS_STATIC: &[AuthCredential] = &[
57 AuthCredential {
58 name: "access_key_id",
59 kind: CredentialKind::AccessKeyId,
60 required: true,
61 },
62 AuthCredential {
63 name: "secret_access_key",
64 kind: CredentialKind::SecretAccessKey,
65 required: true,
66 },
67 AuthCredential {
68 name: "session_token",
69 kind: CredentialKind::SessionToken,
70 required: false,
71 },
72];
73
74const METHODS: &[AuthMethodRecipe] = &[
75 AuthMethodRecipe {
76 id: "no-auth-v1",
77 credentials: &[],
78 wire: WireAuth::None,
79 owned_headers: &[],
80 },
81 AuthMethodRecipe {
82 id: "bearer-api-key-v1",
83 credentials: API_KEY,
84 wire: WireAuth::BearerApiKey,
85 owned_headers: &["authorization"],
86 },
87 AuthMethodRecipe {
88 id: "api-key-header-v1",
89 credentials: API_KEY,
90 wire: WireAuth::HeaderApiKey,
91 owned_headers: &[],
92 },
93 AuthMethodRecipe {
94 id: "anthropic-api-key-v1",
95 credentials: API_KEY,
96 wire: WireAuth::AnthropicApiKey,
97 owned_headers: &["x-api-key"],
98 },
99 AuthMethodRecipe {
100 id: "google-api-key-header-v1",
101 credentials: API_KEY,
102 wire: WireAuth::GoogleApiKey,
103 owned_headers: &["x-goog-api-key"],
104 },
105 AuthMethodRecipe {
106 id: "oauth-access-token-v1",
107 credentials: ACCESS_TOKEN,
108 wire: WireAuth::OAuthAccessToken,
109 owned_headers: &["authorization"],
110 },
111 AuthMethodRecipe {
112 id: "aws-sigv4-credentials-v1",
113 credentials: AWS_STATIC,
114 wire: WireAuth::AwsSigV4Static,
115 owned_headers: &[
116 "authorization",
117 "host",
118 "x-amz-date",
119 "x-amz-content-sha256",
120 "x-amz-security-token",
121 ],
122 },
123 AuthMethodRecipe {
124 id: "azure-api-key-v1",
125 credentials: API_KEY,
126 wire: WireAuth::AzureApiKey,
127 owned_headers: &["api-key"],
128 },
129];
130
131#[must_use]
132pub fn auth_method(id: &str) -> Option<&'static AuthMethodRecipe> {
133 METHODS.iter().find(|method| method.id == id)
134}
135
136pub fn auth_methods() -> impl ExactSizeIterator<Item = &'static AuthMethodRecipe> {
137 METHODS.iter()
138}
139
140#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
141pub enum AuthValidationError {
142 #[error("unsupported_auth_method")]
143 UnsupportedAuthMethod,
144 #[error("invalid_auth_shape")]
145 InvalidShape,
146 #[error("ambiguous_api_key")]
147 AmbiguousApiKey,
148}
149
150pub fn validate_auth_override(
151 auth: &AuthOverride,
152 allowed: &[&str],
153) -> Result<&'static AuthMethodRecipe, AuthValidationError> {
154 validate(
155 auth.method.as_str(),
156 &BTreeMap::new(),
157 &auth.values,
158 allowed,
159 )
160}
161
162pub fn validate_auth_definition(
163 auth: &AuthDefinition,
164 allowed: &[&str],
165) -> Result<&'static AuthMethodRecipe, AuthValidationError> {
166 validate(
167 auth.method.as_str(),
168 &auth.parameters,
169 &auth.values,
170 allowed,
171 )
172}
173
174fn validate(
175 id: &str,
176 parameters: &BTreeMap<AuthParameterId, SafeAuthParameterValue>,
177 values: &BTreeMap<AuthFieldName, SecretString>,
178 allowed: &[&str],
179) -> Result<&'static AuthMethodRecipe, AuthValidationError> {
180 if !allowed.contains(&id) {
181 return Err(AuthValidationError::UnsupportedAuthMethod);
182 }
183 let method = auth_method(id).ok_or(AuthValidationError::UnsupportedAuthMethod)?;
184 let actual = values
185 .keys()
186 .map(AuthFieldName::as_str)
187 .collect::<BTreeSet<_>>();
188 let required = method
189 .credentials
190 .iter()
191 .filter(|field| field.required)
192 .map(|field| field.name)
193 .collect::<BTreeSet<_>>();
194 let permitted = method
195 .credentials
196 .iter()
197 .map(|field| field.name)
198 .collect::<BTreeSet<_>>();
199 if !required.is_subset(&actual) || !actual.is_subset(&permitted) {
200 return Err(AuthValidationError::InvalidShape);
201 }
202 if id == "api-key-header-v1" {
203 let header = parameters
204 .iter()
205 .find(|(name, _)| name.as_str() == "header_name")
206 .map(|(_, value)| value.as_str());
207 if parameters.len() != 1 || !matches!(header, Some("x-api-key" | "api-key")) {
208 return Err(AuthValidationError::InvalidShape);
209 }
210 } else if !parameters.is_empty() {
211 return Err(AuthValidationError::InvalidShape);
212 }
213 Ok(method)
214}