Skip to main content

greentic_oauth_host/
lib.rs

1//! Host-facing helpers for the `greentic:oauth-broker@1.0.0` world.
2//!
3//! This crate intentionally keeps host-specific wiring (Wasmtime linker helpers)
4//! separate from the core logic used by downstream Rust crates.
5
6use async_trait::async_trait;
7use greentic_oauth_core::{AccessToken, OAuthResult};
8use greentic_types::{DistributorRef, GitProviderRef, RegistryRef, RepoRef, ScannerRef, TenantCtx};
9
10/// Wrapper holding an OAuth broker client and exposing convenience helpers.
11pub struct OauthBrokerHost<B> {
12    broker: B,
13}
14
15impl<B> OauthBrokerHost<B> {
16    /// Construct a new host backed by the provided broker implementation.
17    pub fn new(broker: B) -> Self {
18        Self { broker }
19    }
20
21    /// Access the underlying broker.
22    pub fn broker(&self) -> &B {
23        &self.broker
24    }
25}
26
27impl<B> OauthBrokerHost<B>
28where
29    B: OAuthBroker + Send + Sync,
30{
31    /// Request a Git provider token for a repo.
32    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    /// Request an OCI registry token.
43    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    /// Request a scanner token.
53    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    /// Request a token scoped to a repo (used by Store-facing APIs).
63    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    /// Request a distributor token (used by Distributor-facing APIs).
73    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/// Trait abstracting broker communication for testability.
84#[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
94/// Request a Git provider token for a repo.
95pub 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    // Repo is currently not used by the broker; it is carried for caller clarity and
106    // future scoping rules.
107    let _ = repo;
108    broker
109        .request_token(tenant, provider.as_str(), scopes)
110        .await
111}
112
113/// Request an OCI registry token.
114pub 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
128/// Request a scanner token.
129pub 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
141/// Request a token tied to a repo (Store-facing convenience).
142pub 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
154/// Request a token for a distributor endpoint.
155pub 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
169/// Canonical Wasmtime linker exports for the oauth-broker world.
170pub 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}