mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-07 18:07:47 +08:00
merge(main): sync latest main into security branch
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user