Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,6 @@ monobean/resources/lib

# Temporary test cache dir
/tests/.cache_tmp

## local python version file
.python-version
71 changes: 71 additions & 0 deletions ceres/src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use anyhow::Result;
use common::errors::MegaError;
use jupiter::storage::user_storage::UserStorage;

/// User authentication information extracted from Git push process
#[derive(Debug, Clone)]
pub struct PushUserInfo {
pub username: String,
pub primary_email: String,
pub all_emails: Vec<String>, // User's email collection
}

/// Extract user identity from AccessToken during Git push
#[allow(async_fn_in_trait)]
pub trait UserAuthExtractor {
/// Extract user information from AccessToken
async fn extract_user_from_token(&self, access_token: &str) -> Result<PushUserInfo, MegaError>;

/// Verify if email belongs to the authenticated user
async fn verify_email_ownership(&self, username: &str, email: &str) -> Result<bool, MegaError>;

/// Extract user information from username (for cases where token is already validated)
async fn extract_user_from_username(&self, username: &str) -> Result<PushUserInfo, MegaError>;
}

/// Default implementation for user authentication
pub struct DefaultUserAuthExtractor {
user_storage: UserStorage,
}

impl DefaultUserAuthExtractor {
pub fn new(user_storage: UserStorage) -> Self {
Self { user_storage }
}
}

impl UserAuthExtractor for DefaultUserAuthExtractor {
async fn extract_user_from_token(&self, access_token: &str) -> Result<PushUserInfo, MegaError> {
// Find username by token
let username = self.user_storage.find_user_by_token(access_token).await?
.ok_or_else(|| MegaError::with_message("Invalid or expired access token"))?;

self.extract_user_from_username(&username).await
}

async fn extract_user_from_username(&self, username: &str) -> Result<PushUserInfo, MegaError> {
// Get user information
let user = self.user_storage.find_user_by_name(username).await?
.ok_or_else(|| MegaError::with_message("User not found"))?;

// Get user's email collection - for now, use the primary email
// TODO: Extend to support multiple emails per user
let all_emails = vec![user.email.clone()];

Ok(PushUserInfo {
username: user.name.clone(),
primary_email: user.email.clone(),
all_emails,
})
}

async fn verify_email_ownership(&self, username: &str, email: &str) -> Result<bool, MegaError> {
// Get user information
let user = self.user_storage.find_user_by_name(username).await?
.ok_or_else(|| MegaError::with_message("User not found"))?;

// Check if email matches primary email
// TODO: Extend to check against all user emails when multiple emails are supported
Ok(user.email.to_lowercase() == email.to_lowercase())
}
}
1 change: 1 addition & 0 deletions ceres/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod api_service;
pub mod auth;
pub mod lfs;
pub mod merge_checker;
pub mod model;
Expand Down
2 changes: 1 addition & 1 deletion ceres/src/pack/monorepo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ impl RepoHandler for MonoRepo {
} else {
String::new()
};
storage.save_entry(&commit_id, entry_list).await
storage.save_entry(&commit_id, entry_list, self.username.clone()).await
}

async fn check_entry(&self, entry: &Entry) -> Result<(), GitError> {
Expand Down
47 changes: 45 additions & 2 deletions ceres/src/protocol/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use import_refs::RefCommand;
use jupiter::storage::Storage;
use repo::Repo;

use crate::auth::{DefaultUserAuthExtractor, UserAuthExtractor, PushUserInfo};
use crate::pack::{import_repo::ImportRepo, monorepo::MonoRepo, RepoHandler};

pub mod import_refs;
Expand All @@ -31,6 +32,7 @@ pub struct SmartProtocol {
pub service_type: Option<ServiceType>,
pub storage: Storage,
pub username: Option<String>,
pub authenticated_user: Option<PushUserInfo>,
}

#[derive(Debug, PartialEq, Clone, Copy, Default)]
Expand Down Expand Up @@ -140,6 +142,7 @@ impl SmartProtocol {
service_type: None,
storage,
username: None,
authenticated_user: None,
}
}

Expand All @@ -153,6 +156,7 @@ impl SmartProtocol {
service_type: None,
storage,
username: None,
authenticated_user: None,
}
}

Expand Down Expand Up @@ -230,18 +234,57 @@ impl SmartProtocol {
&& username == auth_config.test_user_name
&& token == auth_config.test_user_token
{
// For test user, create a basic PushUserInfo
match self.create_user_auth_extractor() {
Ok(user_auth) => {
if let Ok(user_info) = user_auth.extract_user_from_username(username).await {
self.authenticated_user = Some(user_info);
return true;
} else {
tracing::warn!("Failed to extract user info for test user: {}", username);
}
}
Err(e) => {
tracing::warn!("Failed to create user auth extractor for test user: {}", e);
}
}
return true;
}
return self
let token_valid = self
.storage
.user_storage()
.check_token(username, token)
.await
.unwrap();
.unwrap_or(false);

if token_valid {
// Extract user information for valid token
match self.create_user_auth_extractor() {
Ok(user_auth) => {
if let Ok(user_info) = user_auth.extract_user_from_username(username).await {
self.authenticated_user = Some(user_info);
return true;
} else {
tracing::warn!("Failed to extract user info for username: {}", username);
}
}
Err(e) => {
tracing::warn!("Failed to create user auth extractor: {}", e);
}
}
}

return token_valid;
}
}
false
}

/// Create a user authentication extractor
fn create_user_auth_extractor(&self) -> Result<DefaultUserAuthExtractor, MegaError> {
let user_storage = self.storage.user_storage();
Ok(DefaultUserAuthExtractor::new(user_storage))
}
}

#[cfg(test)]
Expand Down
173 changes: 173 additions & 0 deletions ceres/src/protocol/smart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use tokio_stream::wrappers::ReceiverStream;
use callisto::sea_orm_active_enums::RefTypeEnum;
use common::errors::ProtocolError;

use crate::auth::UserAuthExtractor;
use crate::pack::monorepo::MonoRepo;
use crate::protocol::import_refs::RefCommand;
use crate::protocol::ZERO_ID;
Expand Down Expand Up @@ -251,6 +252,10 @@ impl SmartProtocol {
}
add_pkt_line_string(&mut report_status, command.get_status());
}

// 4. Process commit bindings for successful ref updates
self.process_commit_bindings().await;

if repo_handler.is_monorepo() {
let any = repo_handler.into_any();
if let Ok(monorepo) = any.downcast::<MonoRepo>() {
Expand Down Expand Up @@ -332,6 +337,162 @@ impl SmartProtocol {
read_until_white_space(pkt_line),
)
}

/// Process commit bindings for successfully pushed commits
async fn process_commit_bindings(&self) {
for command in &self.command_list {
// Only process successful branch updates (not tags or failed commands)
if command.ref_type == RefTypeEnum::Branch
&& command.status == "ok"
&& command.new_id != ZERO_ID
{
if let Err(e) = self.bind_commit_to_user(&command.new_id).await {
tracing::warn!("Failed to bind commit {} to user: {}", command.new_id, e);
// Don't fail the push on binding errors
}
}
}
}

/// Bind a single commit to a user based on authenticated user and author email
async fn bind_commit_to_user(
&self,
commit_sha: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let repo_handler = self.repo_handler().await?;
let commit_binding_storage = self.storage.commit_binding_storage();

// Extract author email from commit based on repo type
let author_email = if repo_handler.is_monorepo() {
// For monorepo, get from mono storage
let commit = self
.storage
.mono_storage()
.get_commit_by_hash(commit_sha)
.await?;

if let Some(commit_model) = commit {
// Extract author email from commit (assuming author field contains "Name <email>" format)
if let Some(author) = &commit_model.author {
extract_email_from_author(author)
} else {
return Ok(()); // Skip if no author info
}
} else {
return Ok(()); // Skip if commit not found
}
} else {
// For import repo, we need to find the repo_id first
let import_dir = self.storage.config().monorepo.import_dir.clone();
if !self.path.starts_with(import_dir) {
return Ok(()); // Skip if not in import directory
}

let path_str = self.path.to_str().unwrap();
let repo = self
.storage
.git_db_storage()
.find_git_repo_exact_match(path_str)
.await?;

if let Some(repo) = repo {
let commit = self
.storage
.git_db_storage()
.get_commit_by_hash(repo.id, commit_sha)
.await?;

if let Some(commit_model) = commit {
// Extract author email from commit
if let Some(author) = &commit_model.author {
extract_email_from_author(author)
} else {
return Ok(()); // Skip if no author info
}
} else {
return Ok(()); // Skip if commit not found
}
} else {
return Ok(()); // Skip if repo not found
}
};

if author_email.is_empty() {
return Ok(()); // Skip if no valid email
}

// Enhanced user matching logic based on authentication status
let (final_user_id, is_anonymous) = if let Some(authenticated_user) = &self.authenticated_user {
// User is authenticated - check if commit author email belongs to them
match self.create_user_auth_extractor() {
Ok(user_auth) => {
let email_belongs_to_user = user_auth
.verify_email_ownership(&authenticated_user.username, &author_email)
.await
.unwrap_or(false);

if email_belongs_to_user {
// Author email belongs to authenticated user
(Some(authenticated_user.username.clone()), false)
} else {
// Author email doesn't belong to authenticated user - try general matching
let user_storage = self.storage.user_storage();
let matched_user = user_storage.find_user_by_email(&author_email).await?;

if let Some(user) = matched_user {
(Some(user.name.clone()), false)
} else {
// Mark as anonymous since we can't match the email
(None, true)
}
}
}
Err(e) => {
// Log the error from create_user_auth_extractor for debugging
tracing::warn!(
"Failed to create user auth extractor for commit binding: {}. \
Falling back to email-based matching.",
e
);

// Fallback to email-based matching if auth extractor fails
let user_storage = self.storage.user_storage();
let matched_user = user_storage.find_user_by_email(&author_email).await?;

if let Some(user) = matched_user {
(Some(user.name.clone()), false)
} else {
(None, true)
}
}
}
} else {
// No authenticated user - try to match by email only
let user_storage = self.storage.user_storage();
let matched_user = user_storage.find_user_by_email(&author_email).await?;

if let Some(user) = matched_user {
(Some(user.name.clone()), false)
} else {
(None, true)
}
};

// Upsert the binding
commit_binding_storage
.upsert_binding(commit_sha, &author_email, final_user_id, is_anonymous)
.await?;

tracing::info!(
"Bound commit {} (author: {}) to user: {:?} (authenticated: {})",
commit_sha,
author_email,
if is_anonymous { "anonymous" } else { "matched" },
self.authenticated_user.is_some()
);

Ok(())
}
}

fn read_until_white_space(bytes: &mut Bytes) -> String {
Expand Down Expand Up @@ -400,6 +561,18 @@ pub fn read_pkt_line(bytes: &mut Bytes) -> (usize, Bytes) {
(pkt_length, pkt_line)
}

/// Extract email address from Git author string (format: "Name <email>")
fn extract_email_from_author(author: &str) -> String {
if let Some(start) = author.rfind('<') {
if let Some(end) = author.rfind('>') {
if start < end {
return author[start + 1..end].trim().to_string();
}
}
}
String::new()
}

#[cfg(test)]
pub mod test {
use bytes::{Bytes, BytesMut};
Expand Down
Loading
Loading