1use async_trait::async_trait;
7use greentic_oauth_core::{AccessToken, OAuthResult};
8use greentic_types::{DistributorRef, GitProviderRef, RegistryRef, RepoRef, ScannerRef, TenantCtx};
9
10pub struct OauthBrokerHost<B> {
12 broker: B,
13}
14
15impl<B> OauthBrokerHost<B> {
16 pub fn new(broker: B) -> Self {
18 Self { broker }
19 }
20
21 pub fn broker(&self) -> &B {
23 &self.broker
24 }
25}
26
27impl<B> OauthBrokerHost<B>
28where
29 B: OAuthBroker + Send + Sync,
30{
31 pub async fn request_git_token(
33 &self,
34 tenant: &TenantCtx,
35 provider: GitProviderRef,
36 repo: RepoRef,
37 scopes: &[String],
38 ) -> OAuthResult<AccessToken> {
39 request_git_token(&self.broker, tenant, provider, repo, scopes).await
40 }
41
42 pub async fn request_oci_token(
44 &self,
45 tenant: &TenantCtx,
46 registry: RegistryRef,
47 scopes: &[String],
48 ) -> OAuthResult<AccessToken> {
49 request_oci_token(&self.broker, tenant, registry, scopes).await
50 }
51
52 pub async fn request_scanner_token(
54 &self,
55 tenant: &TenantCtx,
56 scanner: ScannerRef,
57 scopes: &[String],
58 ) -> OAuthResult<AccessToken> {
59 request_scanner_token(&self.broker, tenant, scanner, scopes).await
60 }
61
62 pub async fn request_repo_token(
64 &self,
65 tenant: &TenantCtx,
66 repo: RepoRef,
67 scopes: &[String],
68 ) -> OAuthResult<AccessToken> {
69 request_repo_token(&self.broker, tenant, repo, scopes).await
70 }
71
72 pub async fn request_distributor_token(
74 &self,
75 tenant: &TenantCtx,
76 distributor: DistributorRef,
77 scopes: &[String],
78 ) -> OAuthResult<AccessToken> {
79 request_distributor_token(&self.broker, tenant, distributor, scopes).await
80 }
81}
82
83#[async_trait]
85pub trait OAuthBroker {
86 async fn request_token(
87 &self,
88 tenant: &TenantCtx,
89 resource: &str,
90 scopes: &[String],
91 ) -> OAuthResult<AccessToken>;
92}
93
94pub async fn request_git_token<B>(
96 broker: &B,
97 tenant: &TenantCtx,
98 provider: GitProviderRef,
99 repo: RepoRef,
100 scopes: &[String],
101) -> OAuthResult<AccessToken>
102where
103 B: OAuthBroker + ?Sized,
104{
105 let _ = repo;
108 broker
109 .request_token(tenant, provider.as_str(), scopes)
110 .await
111}
112
113pub async fn request_oci_token<B>(
115 broker: &B,
116 tenant: &TenantCtx,
117 registry: RegistryRef,
118 scopes: &[String],
119) -> OAuthResult<AccessToken>
120where
121 B: OAuthBroker + ?Sized,
122{
123 broker
124 .request_token(tenant, registry.as_str(), scopes)
125 .await
126}
127
128pub async fn request_scanner_token<B>(
130 broker: &B,
131 tenant: &TenantCtx,
132 scanner: ScannerRef,
133 scopes: &[String],
134) -> OAuthResult<AccessToken>
135where
136 B: OAuthBroker + ?Sized,
137{
138 broker.request_token(tenant, scanner.as_str(), scopes).await
139}
140
141pub async fn request_repo_token<B>(
143 broker: &B,
144 tenant: &TenantCtx,
145 repo: RepoRef,
146 scopes: &[String],
147) -> OAuthResult<AccessToken>
148where
149 B: OAuthBroker + ?Sized,
150{
151 broker.request_token(tenant, repo.as_str(), scopes).await
152}
153
154pub async fn request_distributor_token<B>(
156 broker: &B,
157 tenant: &TenantCtx,
158 distributor: DistributorRef,
159 scopes: &[String],
160) -> OAuthResult<AccessToken>
161where
162 B: OAuthBroker + ?Sized,
163{
164 broker
165 .request_token(tenant, distributor.as_str(), scopes)
166 .await
167}
168
169pub mod linker {
171 pub use greentic_interfaces_wasmtime::oauth_broker_broker_v1_0::Component as OauthBrokerComponent;
172 pub use greentic_interfaces_wasmtime::oauth_broker_broker_v1_0::*;
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use greentic_oauth_core::OAuthError;
179 use std::sync::Mutex;
180
181 #[tokio::test]
182 async fn maps_broker_error_and_propagates_tenant() {
183 let tenant =
184 TenantCtx::new("dev".parse().unwrap(), "acme".parse().unwrap()).with_team(None);
185 let tracker = Mutex::new(None);
186 let scopes_tracker = Mutex::new(None);
187 let broker = MockBroker {
188 captured: &tracker,
189 captured_scopes: &scopes_tracker,
190 error: OAuthError::Broker("boom".into()),
191 };
192
193 let err = request_git_token(
194 &broker,
195 &tenant,
196 "git".parse().unwrap(),
197 "repo".parse().unwrap(),
198 &[],
199 )
200 .await
201 .expect_err("should surface broker error");
202
203 match err {
204 OAuthError::Broker(msg) => {
205 assert!(
206 msg.contains("boom"),
207 "expected broker error message, got {msg}"
208 );
209 }
210 other => panic!("unexpected error mapping: {other:?}"),
211 }
212
213 let seen = tracker.lock().unwrap().clone().expect("tenant captured");
214 assert_eq!(seen, tenant, "TenantCtx must propagate to broker impl");
215 let seen_scopes = scopes_tracker.lock().unwrap().clone().unwrap_or_default();
216 assert_eq!(seen_scopes, Vec::<String>::new(), "scopes forwarded");
217 }
218
219 struct MockBroker<'a> {
220 captured: &'a Mutex<Option<TenantCtx>>,
221 captured_scopes: &'a Mutex<Option<Vec<String>>>,
222 error: OAuthError,
223 }
224
225 #[async_trait]
226 impl OAuthBroker for MockBroker<'_> {
227 async fn request_token(
228 &self,
229 tenant: &TenantCtx,
230 resource: &str,
231 scopes: &[String],
232 ) -> OAuthResult<AccessToken> {
233 *self.captured.lock().unwrap() = Some(tenant.clone());
234 let _ = resource;
235 *self.captured_scopes.lock().unwrap() = Some(scopes.to_vec());
236 Err(self.error.clone())
237 }
238 }
239}