1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use futures::future::BoxFuture;
5use serde::Serialize;
6use thiserror::Error;
7
8use crate::config::UpstreamConfig;
9use crate::upstream::http_body_cap::BodyCappedHttpClient;
10
11pub type UpstreamOAuthHttpClient = rmcp::transport::AuthClient<BodyCappedHttpClient>;
12
13#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
14pub struct BeginAuthorization {
15 pub authorization_url: String,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct UpstreamOAuthCredentialStatus {
20 pub access_token_expires_at: i64,
21 pub refresh_token_present: bool,
22}
23
24#[derive(Debug, Error, Clone, PartialEq, Eq)]
25#[error("{kind}: {message}")]
26pub struct UpstreamOAuthError {
27 kind: String,
28 message: String,
29}
30
31impl UpstreamOAuthError {
32 #[must_use]
33 pub fn new(kind: impl Into<String>, message: impl Into<String>) -> Self {
34 Self {
35 kind: kind.into(),
36 message: message.into(),
37 }
38 }
39
40 #[must_use]
41 pub fn internal(message: impl Into<String>) -> Self {
42 Self::new("internal_error", message)
43 }
44
45 #[must_use]
46 pub fn kind(&self) -> &str {
47 &self.kind
48 }
49
50 #[must_use]
51 pub fn message(&self) -> &str {
52 &self.message
53 }
54}
55
56pub trait UpstreamOAuthProvider: Send + Sync {
57 fn authenticated_http_client<'a>(
58 &'a self,
59 upstream: &'a UpstreamConfig,
60 subject: &'a str,
61 http_client: BodyCappedHttpClient,
62 ) -> BoxFuture<'a, Result<UpstreamOAuthHttpClient, UpstreamOAuthError>>;
63
64 fn evict_subject(&self, _upstream: &str, _subject: &str) {}
65
66 fn evict_upstream(&self, _upstream: &str) {}
67}
68
69pub trait UpstreamOAuthManager: Send + Sync {
70 fn begin_authorization<'a>(
71 &'a self,
72 subject: &'a str,
73 ) -> BoxFuture<'a, Result<BeginAuthorization, UpstreamOAuthError>>;
74
75 fn credential_status<'a>(
76 &'a self,
77 subject: &'a str,
78 ) -> BoxFuture<'a, Result<Option<UpstreamOAuthCredentialStatus>, UpstreamOAuthError>>;
79
80 fn clear_credentials<'a>(
81 &'a self,
82 subject: &'a str,
83 ) -> BoxFuture<'a, Result<(), UpstreamOAuthError>>;
84
85 fn access_token<'a>(
86 &'a self,
87 subject: &'a str,
88 ) -> BoxFuture<'a, Result<String, UpstreamOAuthError>>;
89}
90
91#[derive(Clone)]
92pub struct UpstreamOAuthRuntime {
93 provider: Arc<dyn UpstreamOAuthProvider>,
94 managers: Arc<BTreeMap<String, Arc<dyn UpstreamOAuthManager>>>,
95}
96
97impl UpstreamOAuthRuntime {
98 #[must_use]
99 pub fn new(
100 provider: Arc<dyn UpstreamOAuthProvider>,
101 managers: BTreeMap<String, Arc<dyn UpstreamOAuthManager>>,
102 ) -> Self {
103 Self {
104 provider,
105 managers: Arc::new(managers),
106 }
107 }
108
109 #[must_use]
110 pub fn provider(&self) -> Arc<dyn UpstreamOAuthProvider> {
111 Arc::clone(&self.provider)
112 }
113
114 #[must_use]
115 pub fn manager(&self, upstream: &str) -> Option<Arc<dyn UpstreamOAuthManager>> {
116 self.managers.get(upstream).cloned()
117 }
118
119 pub fn evict_subject(&self, upstream: &str, subject: &str) {
120 self.provider.evict_subject(upstream, subject);
121 }
122
123 pub fn evict_upstream(&self, upstream: &str) {
124 self.provider.evict_upstream(upstream);
125 }
126}
127
128#[cfg(test)]
129#[path = "oauth_tests.rs"]
130mod tests;