| 1467 |
1467 |
|
|
| 1468 |
1468 |
|
Ok(Json(data))
|
| 1469 |
1469 |
|
}
|
|
1470 |
+ |
|
|
1471 |
+ |
// ── Git authorization ──
|
|
1472 |
+ |
|
|
1473 |
+ |
#[derive(Deserialize)]
|
|
1474 |
+ |
pub(super) struct GitAuthorizeRequest {
|
|
1475 |
+ |
user_id: UserId,
|
|
1476 |
+ |
/// "git-upload-pack", "git-receive-pack", or "git-upload-archive"
|
|
1477 |
+ |
operation: String,
|
|
1478 |
+ |
owner: String,
|
|
1479 |
+ |
repo_name: String,
|
|
1480 |
+ |
}
|
|
1481 |
+ |
|
|
1482 |
+ |
#[derive(Serialize)]
|
|
1483 |
+ |
struct GitAuthorizeResponse {
|
|
1484 |
+ |
repo_path: String,
|
|
1485 |
+ |
}
|
|
1486 |
+ |
|
|
1487 |
+ |
/// POST /api/internal/git/authorize
|
|
1488 |
+ |
///
|
|
1489 |
+ |
/// Authorize a git operation and return the on-disk repo path.
|
|
1490 |
+ |
/// Auto-creates bare repos on first push if the authenticated user owns the namespace.
|
|
1491 |
+ |
#[tracing::instrument(skip_all, name = "internal::git_authorize")]
|
|
1492 |
+ |
pub(super) async fn git_authorize(
|
|
1493 |
+ |
State(state): State<AppState>,
|
|
1494 |
+ |
_auth: ServiceAuth,
|
|
1495 |
+ |
Json(req): Json<GitAuthorizeRequest>,
|
|
1496 |
+ |
) -> Result<impl IntoResponse> {
|
|
1497 |
+ |
let git_root = state
|
|
1498 |
+ |
.config
|
|
1499 |
+ |
.git_repos_path
|
|
1500 |
+ |
.as_deref()
|
|
1501 |
+ |
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("GIT_REPOS_PATH not configured")))?;
|
|
1502 |
+ |
|
|
1503 |
+ |
// Look up the namespace owner
|
|
1504 |
+ |
let owner_user = db::users::get_user_by_username(
|
|
1505 |
+ |
&state.db,
|
|
1506 |
+ |
&Username::from_trusted(req.owner.clone()),
|
|
1507 |
+ |
)
|
|
1508 |
+ |
.await?
|
|
1509 |
+ |
.ok_or(AppError::NotFound)?;
|
|
1510 |
+ |
|
|
1511 |
+ |
let repo = match db::git_repos::get_repo_by_user_and_name(
|
|
1512 |
+ |
&state.db,
|
|
1513 |
+ |
owner_user.id,
|
|
1514 |
+ |
&req.repo_name,
|
|
1515 |
+ |
)
|
|
1516 |
+ |
.await?
|
|
1517 |
+ |
{
|
|
1518 |
+ |
Some(repo) => repo,
|
|
1519 |
+ |
None => {
|
|
1520 |
+ |
// Auto-create on push if the authenticated user owns the namespace
|
|
1521 |
+ |
if req.operation != "git-receive-pack" || req.user_id != owner_user.id {
|
|
1522 |
+ |
return Err(AppError::NotFound);
|
|
1523 |
+ |
}
|
|
1524 |
+ |
|
|
1525 |
+ |
let owner_dir = std::path::Path::new(git_root).join(&req.owner);
|
|
1526 |
+ |
let repo_dir = owner_dir.join(format!("{}.git", req.repo_name));
|
|
1527 |
+ |
|
|
1528 |
+ |
std::fs::create_dir_all(&owner_dir)
|
|
1529 |
+ |
.map_err(|e| AppError::Internal(e.into()))?;
|
|
1530 |
+ |
git2::Repository::init_bare(&repo_dir)
|
|
1531 |
+ |
.map_err(|e| AppError::Internal(e.into()))?;
|
|
1532 |
+ |
|
|
1533 |
+ |
// Install post-receive hook if build trigger token is configured
|
|
1534 |
+ |
if let Some(ref token) = state.config.build_trigger_token {
|
|
1535 |
+ |
let hook_content = crate::build_runner::post_receive_hook(token);
|
|
1536 |
+ |
let hooks_dir = repo_dir.join("hooks");
|
|
1537 |
+ |
std::fs::create_dir_all(&hooks_dir)
|
|
1538 |
+ |
.map_err(|e| AppError::Internal(e.into()))?;
|
|
1539 |
+ |
let hook_path = hooks_dir.join("post-receive");
|
|
1540 |
+ |
std::fs::write(&hook_path, &hook_content)
|
|
1541 |
+ |
.map_err(|e| AppError::Internal(e.into()))?;
|
|
1542 |
+ |
#[cfg(unix)]
|
|
1543 |
+ |
{
|
|
1544 |
+ |
use std::os::unix::fs::PermissionsExt;
|
|
1545 |
+ |
let _ = std::fs::set_permissions(
|
|
1546 |
+ |
&hook_path,
|
|
1547 |
+ |
std::fs::Permissions::from_mode(0o755),
|
|
1548 |
+ |
);
|
|
1549 |
+ |
}
|
|
1550 |
+ |
}
|
|
1551 |
+ |
|
|
1552 |
+ |
// Fix ownership so the git user can write
|
|
1553 |
+ |
let status = std::process::Command::new("chown")
|
|
1554 |
+ |
.args(["-R", "git:git"])
|
|
1555 |
+ |
.arg(&repo_dir)
|
|
1556 |
+ |
.status()
|
|
1557 |
+ |
.map_err(|e| AppError::Internal(e.into()))?;
|
|
1558 |
+ |
if !status.success() {
|
|
1559 |
+ |
return Err(AppError::Internal(anyhow::anyhow!("chown failed on {}", repo_dir.display())));
|
|
1560 |
+ |
}
|
|
1561 |
+ |
|
|
1562 |
+ |
tracing::info!(owner = %req.owner, repo = %req.repo_name, "auto-created bare repository");
|
|
1563 |
+ |
db::git_repos::create_repo(&state.db, owner_user.id, &req.repo_name).await?
|
|
1564 |
+ |
}
|
|
1565 |
+ |
};
|
|
1566 |
+ |
|
|
1567 |
+ |
// Permission check
|
|
1568 |
+ |
match req.operation.as_str() {
|
|
1569 |
+ |
"git-receive-pack" => {
|
|
1570 |
+ |
if req.user_id != owner_user.id {
|
|
1571 |
+ |
return Err(AppError::Forbidden);
|
|
1572 |
+ |
}
|
|
1573 |
+ |
}
|
|
1574 |
+ |
"git-upload-pack" | "git-upload-archive" => {
|
|
1575 |
+ |
if repo.visibility == "private" && req.user_id != owner_user.id {
|
|
1576 |
+ |
return Err(AppError::NotFound);
|
|
1577 |
+ |
}
|
|
1578 |
+ |
}
|
|
1579 |
+ |
_ => return Err(AppError::BadRequest("unsupported git operation".into())),
|
|
1580 |
+ |
}
|
|
1581 |
+ |
|
|
1582 |
+ |
let repo_path = std::path::Path::new(git_root)
|
|
1583 |
+ |
.join(&req.owner)
|
|
1584 |
+ |
.join(format!("{}.git", req.repo_name));
|
|
1585 |
+ |
|
|
1586 |
+ |
Ok(Json(GitAuthorizeResponse {
|
|
1587 |
+ |
repo_path: repo_path.to_string_lossy().into_owned(),
|
|
1588 |
+ |
}))
|
|
1589 |
+ |
}
|