Skip to main content

soma_gateway/gateway/
oauth.rs

1use thiserror::Error;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum GatewayOAuthSurface {
5    AdminOAuthOperation,
6    CodeModeAdmin,
7    CodeModeShared,
8    ProtectedPublicRoute,
9    RelayToolCall,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct IdentityMatrixRow {
14    pub surface: GatewayOAuthSurface,
15    pub authenticated_caller: &'static str,
16    pub upstream_credential_subject: &'static str,
17    pub relay_cache_subject: &'static str,
18    pub protected_public_route_caller_subject: &'static str,
19    pub source_of_truth: &'static str,
20    pub caller_supplied_subject_accepted: bool,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct GatewaySubject {
25    pub subject: String,
26    pub caller_supplied: bool,
27}
28
29#[derive(Debug, Error, PartialEq, Eq)]
30pub enum GatewayOAuthError {
31    #[error("caller supplied subject is accepted only for admin OAuth operations")]
32    CallerSuppliedSubjectDenied,
33}
34
35pub fn identity_matrix() -> Vec<IdentityMatrixRow> {
36    vec![
37        IdentityMatrixRow {
38            surface: GatewayOAuthSurface::AdminOAuthOperation,
39            authenticated_caller: "admin principal",
40            upstream_credential_subject: "caller selected subject",
41            relay_cache_subject: "caller selected subject",
42            protected_public_route_caller_subject: "not applicable",
43            source_of_truth: "admin OAuth params",
44            caller_supplied_subject_accepted: true,
45        },
46        IdentityMatrixRow {
47            surface: GatewayOAuthSurface::CodeModeAdmin,
48            authenticated_caller: "admin principal",
49            upstream_credential_subject: "shared gateway subject",
50            relay_cache_subject: "shared gateway subject",
51            protected_public_route_caller_subject: "not applicable",
52            source_of_truth: "gateway config",
53            caller_supplied_subject_accepted: false,
54        },
55        IdentityMatrixRow {
56            surface: GatewayOAuthSurface::CodeModeShared,
57            authenticated_caller: "shared gateway principal",
58            upstream_credential_subject: "shared gateway subject",
59            relay_cache_subject: "shared gateway subject",
60            protected_public_route_caller_subject: "not applicable",
61            source_of_truth: "gateway config",
62            caller_supplied_subject_accepted: false,
63        },
64        IdentityMatrixRow {
65            surface: GatewayOAuthSurface::ProtectedPublicRoute,
66            authenticated_caller: "public route caller",
67            upstream_credential_subject: "shared gateway subject",
68            relay_cache_subject: "shared gateway subject",
69            protected_public_route_caller_subject: "stripped before upstream",
70            source_of_truth: "protected route config",
71            caller_supplied_subject_accepted: false,
72        },
73        IdentityMatrixRow {
74            surface: GatewayOAuthSurface::RelayToolCall,
75            authenticated_caller: "downstream MCP session",
76            upstream_credential_subject: "resolved gateway subject",
77            relay_cache_subject: "resolved gateway subject",
78            protected_public_route_caller_subject: "not applicable",
79            source_of_truth: "gateway-authenticated principal",
80            caller_supplied_subject_accepted: false,
81        },
82    ]
83}
84
85pub fn resolve_subject(
86    surface: GatewayOAuthSurface,
87    configured_subject: &str,
88    caller_supplied_subject: Option<&str>,
89) -> Result<GatewaySubject, GatewayOAuthError> {
90    if let Some(subject) = caller_supplied_subject {
91        if surface == GatewayOAuthSurface::AdminOAuthOperation {
92            return Ok(GatewaySubject {
93                subject: subject.to_owned(),
94                caller_supplied: true,
95            });
96        }
97        return Err(GatewayOAuthError::CallerSuppliedSubjectDenied);
98    }
99    Ok(GatewaySubject {
100        subject: configured_subject.to_owned(),
101        caller_supplied: false,
102    })
103}
104
105pub fn strip_public_authorization_header<'a>(
106    headers: impl IntoIterator<Item = (&'a str, &'a str)>,
107) -> Vec<(&'a str, &'a str)> {
108    headers
109        .into_iter()
110        .filter(|(name, _)| !name.eq_ignore_ascii_case("authorization"))
111        .collect()
112}
113
114#[cfg(test)]
115#[path = "oauth_tests.rs"]
116mod tests;