Skip to content

Commit 96a6908

Browse files
better azure and gcp credential vending support
1 parent 17180c6 commit 96a6908

11 files changed

Lines changed: 887 additions & 28 deletions

File tree

pangolin/Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pangolin/pangolin_api/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ bytes = "1.5"
2828
serde_yaml = "0.9"
2929
utoipa = { version = "4", features = ["axum_extras", "chrono", "uuid"] }
3030
utoipa-swagger-ui = { version = "6", features = ["axum"] }
31+
async-trait = { workspace = true }
3132

3233
# Cloud provider SDKs (optional features)
3334
aws-config = { version = "1.0", optional = true }
@@ -49,3 +50,4 @@ cloud-credentials = ["aws-sts", "azure-oauth", "gcp-oauth"]
4950
hyper = { version = "1.0", features = ["full"] }
5051
pangolin_store = { path = "../pangolin_store" }
5152
serial_test = "2.0"
53+
wiremock = "0.6"
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
use super::{CredentialSigner, VendedCredentials};
2+
use async_trait::async_trait;
3+
use chrono::{DateTime, Utc, Duration};
4+
use std::collections::HashMap;
5+
use anyhow::{Result, anyhow};
6+
7+
#[cfg(feature = "azure-oauth")]
8+
use azure_core::auth::TokenCredential;
9+
#[cfg(feature = "azure-oauth")]
10+
use azure_identity::ClientSecretCredential;
11+
12+
/// Azure ADLS Gen2 credential signer that generates SAS tokens
13+
pub struct AzureSasSigner {
14+
pub account_name: String,
15+
pub account_key: Option<String>,
16+
pub tenant_id: Option<String>,
17+
pub client_id: Option<String>,
18+
pub client_secret: Option<String>,
19+
pub container: String,
20+
}
21+
22+
impl AzureSasSigner {
23+
pub fn new(
24+
account_name: String,
25+
account_key: Option<String>,
26+
tenant_id: Option<String>,
27+
client_id: Option<String>,
28+
client_secret: Option<String>,
29+
container: String,
30+
) -> Self {
31+
Self {
32+
account_name,
33+
account_key,
34+
tenant_id,
35+
client_id,
36+
client_secret,
37+
container,
38+
}
39+
}
40+
}
41+
42+
#[async_trait]
43+
impl CredentialSigner for AzureSasSigner {
44+
async fn generate_credentials(
45+
&self,
46+
_resource_path: &str,
47+
_permissions: &[String],
48+
duration: Duration,
49+
) -> Result<VendedCredentials> {
50+
#[cfg(feature = "azure-oauth")]
51+
{
52+
tracing::info!("🔑 Generating Azure credentials");
53+
54+
let expires_at = Utc::now() + duration;
55+
56+
// Try OAuth2 if credentials are available
57+
if let (Some(tenant_id), Some(client_id), Some(client_secret)) =
58+
(&self.tenant_id, &self.client_id, &self.client_secret) {
59+
60+
let authority_host = azure_core::Url::parse("https://login.microsoftonline.com")
61+
.map_err(|e| anyhow!("Failed to parse authority host: {}", e))?;
62+
63+
let credential = ClientSecretCredential::new(
64+
azure_core::new_http_client(),
65+
authority_host,
66+
tenant_id.clone(),
67+
client_id.clone(),
68+
client_secret.clone(),
69+
);
70+
71+
let token = credential
72+
.get_token(&["https://storage.azure.com/.default"])
73+
.await
74+
.map_err(|e| anyhow!("Azure token acquisition failed: {}", e))?;
75+
76+
let mut config = HashMap::new();
77+
config.insert("credential-type".to_string(), "azure-oauth".to_string());
78+
config.insert("azure-oauth-token".to_string(), token.token.secret().to_string());
79+
config.insert("azure-account-name".to_string(), self.account_name.clone());
80+
config.insert("azure-container".to_string(), self.container.clone());
81+
82+
tracing::info!("✅ Successfully generated Azure OAuth2 token");
83+
84+
return Ok(VendedCredentials {
85+
prefix: format!("abfss://{}@{}.dfs.core.windows.net/",
86+
self.container, self.account_name),
87+
config,
88+
expires_at: Some(expires_at),
89+
});
90+
}
91+
92+
// Fallback to account key if available
93+
if let Some(account_key) = &self.account_key {
94+
let mut config = HashMap::new();
95+
config.insert("credential-type".to_string(), "azure-key".to_string());
96+
config.insert("azure-account-name".to_string(), self.account_name.clone());
97+
config.insert("azure-account-key".to_string(), account_key.clone());
98+
config.insert("azure-container".to_string(), self.container.clone());
99+
100+
tracing::info!("✅ Using Azure account key credentials");
101+
102+
return Ok(VendedCredentials {
103+
prefix: format!("abfss://{}@{}.dfs.core.windows.net/",
104+
self.container, self.account_name),
105+
config,
106+
expires_at: None, // Account keys don't expire
107+
});
108+
}
109+
110+
Err(anyhow!("Azure credentials not configured properly"))
111+
}
112+
113+
#[cfg(not(feature = "azure-oauth"))]
114+
{
115+
tracing::warn!("Azure OAuth feature not enabled, returning placeholder credentials");
116+
let mut config = HashMap::new();
117+
config.insert("credential-type".to_string(), "azure-sas".to_string());
118+
config.insert("azure-sas-token".to_string(), "PLACEHOLDER_SAS_TOKEN".to_string());
119+
config.insert("azure-account-name".to_string(), self.account_name.clone());
120+
config.insert("azure-container".to_string(), self.container.clone());
121+
122+
Ok(VendedCredentials {
123+
prefix: format!("abfss://{}@{}.dfs.core.windows.net/",
124+
self.container, self.account_name),
125+
config,
126+
expires_at: Some(Utc::now() + duration),
127+
})
128+
}
129+
}
130+
131+
fn storage_type(&self) -> &str {
132+
"azure"
133+
}
134+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use super::{CredentialSigner, VendedCredentials};
2+
use async_trait::async_trait;
3+
use chrono::{DateTime, Utc, Duration};
4+
use std::collections::HashMap;
5+
use anyhow::{Result, anyhow};
6+
7+
#[cfg(feature = "gcp-oauth")]
8+
use gcp_auth::{CustomServiceAccount, TokenProvider};
9+
10+
/// GCP Cloud Storage credential signer that generates downscoped OAuth2 tokens
11+
pub struct GcpTokenSigner {
12+
pub project_id: String,
13+
pub bucket: String,
14+
pub service_account_key_json: Option<String>,
15+
}
16+
17+
impl GcpTokenSigner {
18+
pub fn new(
19+
project_id: String,
20+
bucket: String,
21+
service_account_key_json: Option<String>,
22+
) -> Self {
23+
Self {
24+
project_id,
25+
bucket,
26+
service_account_key_json,
27+
}
28+
}
29+
}
30+
31+
#[async_trait]
32+
impl CredentialSigner for GcpTokenSigner {
33+
async fn generate_credentials(
34+
&self,
35+
_resource_path: &str,
36+
permissions: &[String],
37+
duration: Duration,
38+
) -> Result<VendedCredentials> {
39+
#[cfg(feature = "gcp-oauth")]
40+
{
41+
tracing::info!("🔑 Generating GCP OAuth2 token for resource: {}", resource_path);
42+
43+
if let Some(sa_key) = &self.service_account_key_json {
44+
let service_account = CustomServiceAccount::from_json(sa_key)
45+
.map_err(|e| anyhow!("Failed to parse GCP service account key: {}", e))?;
46+
47+
// Determine scopes based on permissions
48+
let mut scopes = vec![];
49+
let has_write = permissions.iter().any(|p| p == "write" || p == "delete");
50+
51+
if has_write {
52+
scopes.push("https://www.googleapis.com/auth/devstorage.read_write");
53+
} else {
54+
scopes.push("https://www.googleapis.com/auth/devstorage.read_only");
55+
}
56+
57+
let token = service_account
58+
.token(&scopes)
59+
.await
60+
.map_err(|e| anyhow!("GCP token acquisition failed: {}", e))?;
61+
62+
let expires_at = Utc::now() + duration;
63+
64+
let mut config = HashMap::new();
65+
config.insert("credential-type".to_string(), "gcp-oauth".to_string());
66+
config.insert("gcp-oauth-token".to_string(), token.as_str().to_string());
67+
config.insert("gcp-project-id".to_string(), self.project_id.clone());
68+
config.insert("gcp-bucket".to_string(), self.bucket.clone());
69+
70+
tracing::info!("✅ Successfully generated GCP OAuth2 token (expires: {})", expires_at);
71+
72+
return Ok(VendedCredentials {
73+
prefix: format!("gs://{}/", self.bucket),
74+
config,
75+
expires_at: Some(expires_at),
76+
});
77+
}
78+
79+
Err(anyhow!("GCP service account key not configured"))
80+
}
81+
82+
#[cfg(not(feature = "gcp-oauth"))]
83+
{
84+
tracing::warn!("GCP OAuth feature not enabled, returning placeholder credentials");
85+
let mut config = HashMap::new();
86+
config.insert("credential-type".to_string(), "gcp-oauth".to_string());
87+
config.insert("gcp-oauth-token".to_string(), "PLACEHOLDER_GCP_TOKEN".to_string());
88+
config.insert("gcp-project-id".to_string(), self.project_id.clone());
89+
config.insert("gcp-bucket".to_string(), self.bucket.clone());
90+
91+
Ok(VendedCredentials {
92+
prefix: format!("gs://{}/", self.bucket),
93+
config,
94+
expires_at: Some(Utc::now() + duration),
95+
})
96+
}
97+
}
98+
99+
fn storage_type(&self) -> &str {
100+
"gcs"
101+
}
102+
}

0 commit comments

Comments
 (0)