fix(git): LFS token validation and remove IP rate limiting
- Implement proper token validation via user_token table (SHA256+base64 hash) - Query token_hash, check IsRevoked, validate expiry - Remove IP-based rate limiting (handled by k8s ingress) - Remove unused client_ip() helper function - user_uid() now async and queries database for real user
This commit is contained in:
parent
ef529d772b
commit
52a0131b56
@ -4,6 +4,10 @@ use crate::http::handler::is_valid_lfs_oid;
|
|||||||
use crate::http::lfs::{BatchRequest, CreateLockRequest, LfsHandler};
|
use crate::http::lfs::{BatchRequest, CreateLockRequest, LfsHandler};
|
||||||
use crate::http::utils::get_repo_model;
|
use crate::http::utils::get_repo_model;
|
||||||
use actix_web::{Error, HttpRequest, HttpResponse, web};
|
use actix_web::{Error, HttpRequest, HttpResponse, web};
|
||||||
|
use base64::Engine;
|
||||||
|
use models::users::user_token;
|
||||||
|
use sea_orm::prelude::*;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
fn base_url(req: &HttpRequest) -> String {
|
fn base_url(req: &HttpRequest) -> String {
|
||||||
@ -28,10 +32,19 @@ fn bearer_token(req: &HttpRequest) -> Result<String, Error> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn hash_token(token: &str) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(token.as_bytes());
|
||||||
|
base64::prelude::BASE64_STANDARD.encode(hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
/// Derive the acting user from the authenticated bearer token, not from a
|
/// Derive the acting user from the authenticated bearer token, not from a
|
||||||
/// client-supplied header. This prevents privilege escalation where a
|
/// client-supplied header. This prevents privilege escalation where a
|
||||||
/// malicious client could impersonate any user via the `X-User-Uid` header.
|
/// malicious client could impersonate any user via the `X-User-Uid` header.
|
||||||
fn user_uid(req: &HttpRequest, repo: &models::repos::repo::Model) -> Result<uuid::Uuid, Error> {
|
async fn user_uid(
|
||||||
|
req: &HttpRequest,
|
||||||
|
db: &db::database::AppDatabase,
|
||||||
|
) -> Result<uuid::Uuid, Error> {
|
||||||
let auth_header = req
|
let auth_header = req
|
||||||
.headers()
|
.headers()
|
||||||
.get("authorization")
|
.get("authorization")
|
||||||
@ -43,20 +56,26 @@ fn user_uid(req: &HttpRequest, repo: &models::repos::repo::Model) -> Result<uuid
|
|||||||
.strip_prefix("Bearer ")
|
.strip_prefix("Bearer ")
|
||||||
.ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid authorization format"))?;
|
.ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid authorization format"))?;
|
||||||
|
|
||||||
// In a production deployment, `token` would be a signed JWT or opaque
|
let token_hash = hash_token(token);
|
||||||
// token mapped to a real user. For now, require a valid UUID token and
|
|
||||||
// use it as the user identity, falling back to the repo owner only
|
|
||||||
// when no auth is present (which is rejected above).
|
|
||||||
token
|
|
||||||
.parse::<uuid::Uuid>()
|
|
||||||
.map_err(|_| actix_web::error::ErrorUnauthorized("Invalid token"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn client_ip(req: &HttpRequest) -> String {
|
let token_model = user_token::Entity::find()
|
||||||
req.connection_info()
|
.filter(user_token::Column::TokenHash.eq(&token_hash))
|
||||||
.realip_remote_addr()
|
.filter(user_token::Column::IsRevoked.eq(false))
|
||||||
.unwrap_or("unknown")
|
.one(db.reader())
|
||||||
.to_string()
|
.await
|
||||||
|
.map_err(|e| actix_web::error::ErrorInternalServerError(e.to_string()))?;
|
||||||
|
|
||||||
|
let token_model = token_model
|
||||||
|
.ok_or_else(|| actix_web::error::ErrorUnauthorized("Invalid token"))?;
|
||||||
|
|
||||||
|
// Check expiry
|
||||||
|
if let Some(expires_at) = token_model.expires_at {
|
||||||
|
if expires_at < chrono::Utc::now() {
|
||||||
|
return Err(actix_web::error::ErrorUnauthorized("Token expired"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(token_model.user)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn lfs_batch(
|
pub async fn lfs_batch(
|
||||||
@ -67,13 +86,6 @@ pub async fn lfs_batch(
|
|||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let (namespace, repo_name) = path.into_inner();
|
let (namespace, repo_name) = path.into_inner();
|
||||||
|
|
||||||
let ip = client_ip(&req);
|
|
||||||
if !state.rate_limiter.is_ip_read_allowed(&ip).await {
|
|
||||||
return Err(actix_web::error::ErrorTooManyRequests(
|
|
||||||
"Rate limit exceeded",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
||||||
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
||||||
|
|
||||||
@ -99,13 +111,6 @@ pub async fn lfs_upload(
|
|||||||
return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
|
return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ip = client_ip(&req);
|
|
||||||
if !state.rate_limiter.is_ip_write_allowed(&ip).await {
|
|
||||||
return Err(actix_web::error::ErrorTooManyRequests(
|
|
||||||
"Rate limit exceeded",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
||||||
let token = bearer_token(&req)?;
|
let token = bearer_token(&req)?;
|
||||||
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
||||||
@ -129,13 +134,6 @@ pub async fn lfs_download(
|
|||||||
return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
|
return Err(actix_web::error::ErrorBadRequest("Invalid OID format"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let ip = client_ip(&req);
|
|
||||||
if !state.rate_limiter.is_ip_read_allowed(&ip).await {
|
|
||||||
return Err(actix_web::error::ErrorTooManyRequests(
|
|
||||||
"Rate limit exceeded",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
||||||
let token = bearer_token(&req)?;
|
let token = bearer_token(&req)?;
|
||||||
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
||||||
@ -156,15 +154,8 @@ pub async fn lfs_lock_create(
|
|||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let (namespace, repo_name) = path.into_inner();
|
let (namespace, repo_name) = path.into_inner();
|
||||||
|
|
||||||
let ip = client_ip(&req);
|
|
||||||
if !state.rate_limiter.is_ip_write_allowed(&ip).await {
|
|
||||||
return Err(actix_web::error::ErrorTooManyRequests(
|
|
||||||
"Rate limit exceeded",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
||||||
let uid = user_uid(&req, &repo)?;
|
let uid = user_uid(&req, &state.db).await?;
|
||||||
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
||||||
|
|
||||||
match handler.lock_object(&body.oid, uid).await {
|
match handler.lock_object(&body.oid, uid).await {
|
||||||
@ -215,15 +206,8 @@ pub async fn lfs_lock_delete(
|
|||||||
) -> Result<HttpResponse, Error> {
|
) -> Result<HttpResponse, Error> {
|
||||||
let (namespace, repo_name, lock_id) = path.into_inner();
|
let (namespace, repo_name, lock_id) = path.into_inner();
|
||||||
|
|
||||||
let ip = client_ip(&req);
|
|
||||||
if !state.rate_limiter.is_ip_write_allowed(&ip).await {
|
|
||||||
return Err(actix_web::error::ErrorTooManyRequests(
|
|
||||||
"Rate limit exceeded",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
let repo = get_repo_model(&namespace, &repo_name, &state.db).await?;
|
||||||
let uid = user_uid(&req, &repo)?;
|
let uid = user_uid(&req, &state.db).await?;
|
||||||
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
let handler = LfsHandler::new(PathBuf::from(&repo.storage_path), repo, state.db.clone());
|
||||||
|
|
||||||
match handler.unlock_object(&lock_id, uid).await {
|
match handler.unlock_object(&lock_id, uid).await {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user