merge(main): sync latest main into security branch

This commit is contained in:
elky
2026-09-05 00:30:16 +08:00
40 changed files with 2513 additions and 119 deletions
@@ -1,11 +1,18 @@
use super::generic::{
provider_account_state_from_metadata, template_for_provider_type, GenericProviderOAuthAdapter,
};
use crate::provider::ProviderOAuthAdapter;
use crate::core::OAuthError;
use crate::network::{OAuthHttpExecutor, OAuthHttpRequest};
use crate::provider::{ProviderOAuthAdapter, ProviderOAuthTokenSet, ProviderOAuthTransportContext};
use serde_json::Value;
use std::collections::BTreeMap;
pub const ANTIGRAVITY_USER_INFO_URL: &str = "https://www.googleapis.com/oauth2/v2/userinfo";
#[derive(Debug, Clone)]
pub struct AntigravityProviderOAuthAdapter {
inner: GenericProviderOAuthAdapter,
user_info_url: String,
}
impl Default for AntigravityProviderOAuthAdapter {
@@ -15,6 +22,7 @@ impl Default for AntigravityProviderOAuthAdapter {
template_for_provider_type("antigravity")
.expect("antigravity template should exist"),
),
user_info_url: ANTIGRAVITY_USER_INFO_URL.to_string(),
}
}
}
@@ -35,6 +43,88 @@ impl AntigravityProviderOAuthAdapter {
.with_oauth_credentials_for_tests(client_id, client_secret);
self
}
pub fn with_token_url_override(mut self, token_url: impl Into<String>) -> Self {
self.inner = self.inner.with_token_url_override(token_url);
self
}
pub fn with_user_info_url_override(mut self, user_info_url: impl Into<String>) -> Self {
self.user_info_url = user_info_url.into();
self
}
async fn enrich_google_identity(
&self,
executor: &dyn OAuthHttpExecutor,
ctx: &ProviderOAuthTransportContext,
mut result: ProviderOAuthTokenSet,
) -> Result<ProviderOAuthTokenSet, OAuthError> {
if result
.auth_config
.get("email")
.and_then(Value::as_str)
.is_some_and(|email| !email.trim().is_empty())
{
return Ok(result);
}
let response = executor
.execute(OAuthHttpRequest {
request_id: "provider-oauth:antigravity-user-info".to_string(),
method: reqwest::Method::GET,
url: self.user_info_url.clone(),
headers: BTreeMap::from([
("accept".to_string(), "application/json".to_string()),
(
"authorization".to_string(),
result.token_set.bearer_header_value(),
),
]),
content_type: None,
json_body: None,
body_bytes: None,
network: ctx.network.clone(),
transport_profile: None,
})
.await?;
if !(200..300).contains(&response.status_code) {
return Err(OAuthError::HttpStatus {
status_code: response.status_code,
body_excerpt: response.body_text.trim().chars().take(500).collect(),
});
}
let profile = response
.json_body
.or_else(|| serde_json::from_str::<Value>(&response.body_text).ok())
.ok_or_else(|| OAuthError::invalid_response("userinfo response is not json"))?;
if profile.get("verified_email").and_then(Value::as_bool) == Some(false) {
return Err(OAuthError::invalid_response(
"userinfo response returned an unverified email",
));
}
let email = profile
.get("email")
.and_then(Value::as_str)
.map(str::trim)
.filter(|email| !email.is_empty())
.ok_or_else(|| OAuthError::invalid_response("userinfo response missing email"))?
.to_string();
if let Some(auth_config) = result.auth_config.as_object_mut() {
auth_config.insert("email".to_string(), Value::String(email.clone()));
}
if let Some(token_payload) = result
.token_set
.raw_payload
.as_mut()
.and_then(Value::as_object_mut)
{
token_payload.insert("email".to_string(), Value::String(email));
}
Ok(result)
}
}
#[async_trait::async_trait]
@@ -77,9 +167,11 @@ impl ProviderOAuthAdapter for AntigravityProviderOAuthAdapter {
state: &str,
pkce_verifier: Option<&str>,
) -> Result<crate::provider::ProviderOAuthTokenSet, crate::core::OAuthError> {
self.inner
let result = self
.inner
.exchange_code(executor, ctx, code, state, pkce_verifier)
.await
.await?;
self.enrich_google_identity(executor, ctx, result).await
}
async fn import_credentials(
@@ -129,7 +221,7 @@ impl ProviderOAuthAdapter for AntigravityProviderOAuthAdapter {
#[cfg(test)]
mod tests {
use super::AntigravityProviderOAuthAdapter;
use super::{AntigravityProviderOAuthAdapter, ANTIGRAVITY_USER_INFO_URL};
use crate::network::{OAuthHttpExecutor, OAuthHttpRequest, OAuthHttpResponse};
use crate::provider::{
ProviderOAuthAccount, ProviderOAuthAdapter, ProviderOAuthTransportContext,
@@ -137,9 +229,15 @@ mod tests {
use async_trait::async_trait;
use serde_json::json;
use std::collections::BTreeMap;
use std::sync::Mutex;
struct UnusedExecutor;
#[derive(Default)]
struct GoogleOAuthExecutor {
requests: Mutex<Vec<OAuthHttpRequest>>,
}
fn transport_context() -> ProviderOAuthTransportContext {
ProviderOAuthTransportContext {
provider_id: String::new(),
@@ -166,6 +264,43 @@ mod tests {
}
}
#[async_trait]
impl OAuthHttpExecutor for GoogleOAuthExecutor {
async fn execute(
&self,
request: OAuthHttpRequest,
) -> Result<OAuthHttpResponse, crate::core::OAuthError> {
let request_id = request.request_id.clone();
self.requests
.lock()
.expect("requests should lock")
.push(request);
match request_id.as_str() {
"provider-oauth:exchange-code" => Ok(OAuthHttpResponse {
status_code: 200,
body_text: json!({
"access_token": "google-access-token",
"refresh_token": "google-refresh-token",
"token_type": "Bearer",
"expires_in": 3600
})
.to_string(),
json_body: None,
}),
"provider-oauth:antigravity-user-info" => Ok(OAuthHttpResponse {
status_code: 200,
body_text: json!({
"email": "[email protected]",
"verified_email": true
})
.to_string(),
json_body: None,
}),
other => panic!("unexpected OAuth request: {other}"),
}
}
}
#[test]
fn antigravity_authorize_requests_offline_refresh_token() {
let adapter = AntigravityProviderOAuthAdapter::default()
@@ -190,6 +325,46 @@ mod tests {
);
}
#[tokio::test]
async fn antigravity_exchange_fetches_google_email_for_account_identity() {
let adapter = AntigravityProviderOAuthAdapter::default();
let ctx = transport_context();
let executor = GoogleOAuthExecutor::default();
let result = adapter
.exchange_code(
&executor,
&ctx,
"authorization-code",
"state-1",
Some("verifier-1"),
)
.await
.expect("Antigravity OAuth exchange should succeed");
assert_eq!(
result.auth_config.get("email"),
Some(&json!("[email protected]"))
);
assert_eq!(
result
.token_set
.raw_payload
.as_ref()
.and_then(|payload| payload.get("email")),
Some(&json!("[email protected]"))
);
let requests = executor.requests.lock().expect("requests should lock");
assert_eq!(requests.len(), 2);
assert_eq!(requests[1].url, ANTIGRAVITY_USER_INFO_URL);
assert_eq!(requests[1].method, reqwest::Method::GET);
assert_eq!(
requests[1].headers.get("authorization").map(String::as_str),
Some("Bearer google-access-token")
);
assert_eq!(requests[1].network, ctx.network);
}
#[tokio::test]
async fn antigravity_probe_marks_forbidden_metadata_invalid() {
let adapter = AntigravityProviderOAuthAdapter::default();
@@ -5,7 +5,7 @@ mod generic;
mod kiro;
mod windsurf;
pub use antigravity::AntigravityProviderOAuthAdapter;
pub use antigravity::{AntigravityProviderOAuthAdapter, ANTIGRAVITY_USER_INFO_URL};
pub use claude_code::{
ClaudeCodeProviderOAuthAdapter, CLAUDE_CODE_AUTHORIZE_URL, CLAUDE_CODE_CLIENT_ID,
CLAUDE_CODE_COOKIE_SCOPE, CLAUDE_CODE_OAUTH_SCOPES, CLAUDE_CODE_PROVIDER_TYPE,