fix: use SSH key during repository preflight and clone
This commit is contained in:
@@ -35,6 +35,7 @@ from src.utils.git_control import (
|
|||||||
)
|
)
|
||||||
from src.utils.git_history import get_commit_detail, get_commit_history
|
from src.utils.git_history import get_commit_detail, get_commit_history
|
||||||
from src.utils.git_url_parser import parse_git_url
|
from src.utils.git_url_parser import parse_git_url
|
||||||
|
from src.services.ssh_keys import _get_fernet
|
||||||
|
|
||||||
router = APIRouter(prefix="/projects", tags=["git-repositories"])
|
router = APIRouter(prefix="/projects", tags=["git-repositories"])
|
||||||
|
|
||||||
@@ -95,19 +96,61 @@ def _build_provider_clone_url(owner: str, repo: str) -> str:
|
|||||||
return f"git@git.commumedia.org:{owner}/{repo}.git"
|
return f"git@git.commumedia.org:{owner}/{repo}.git"
|
||||||
|
|
||||||
|
|
||||||
def _preflight_remote_repository(remote_url: str) -> None:
|
def _prepare_ssh_env(ssh_key: SSHKey | None) -> dict | None:
|
||||||
|
"""Prepare environment variables for git commands with SSH authentication.
|
||||||
|
|
||||||
|
Returns a dict of extra env vars, or None if no SSH key provided.
|
||||||
|
The caller is responsible for cleaning up the temporary key file.
|
||||||
|
"""
|
||||||
|
if ssh_key is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
# Decrypt private key
|
||||||
|
fernet = _get_fernet()
|
||||||
|
private_key = fernet.decrypt(ssh_key.private_key_encrypted.encode()).decode()
|
||||||
|
|
||||||
|
# Write to temp file with restricted permissions
|
||||||
|
fd, key_path = tempfile.mkstemp(prefix="ssh_key_")
|
||||||
|
try:
|
||||||
|
os.write(fd, private_key.encode())
|
||||||
|
finally:
|
||||||
|
os.close(fd)
|
||||||
|
os.chmod(key_path, 0o600)
|
||||||
|
|
||||||
|
# Return env vars and the key path for cleanup
|
||||||
|
env = {
|
||||||
|
"GIT_SSH_COMMAND": f"ssh -i {key_path} -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||||
|
}
|
||||||
|
return env, key_path
|
||||||
|
|
||||||
|
|
||||||
|
def _preflight_remote_repository(remote_url: str, ssh_key: SSHKey | None = None) -> None:
|
||||||
"""Verify a remote repository is reachable before cloning."""
|
"""Verify a remote repository is reachable before cloning."""
|
||||||
|
env = None
|
||||||
|
key_path = None
|
||||||
|
|
||||||
|
if ssh_key is not None:
|
||||||
|
ssh_result = _prepare_ssh_env(ssh_key)
|
||||||
|
if ssh_result:
|
||||||
|
env, key_path = ssh_result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git", "ls-remote", remote_url],
|
["git", "ls-remote", remote_url],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=60,
|
timeout=60,
|
||||||
|
env={**os.environ, **env} if env else None,
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="remote repository check timed out")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="remote repository check timed out")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
|
||||||
|
finally:
|
||||||
|
if key_path and os.path.exists(key_path):
|
||||||
|
os.unlink(key_path)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -116,18 +159,30 @@ def _preflight_remote_repository(remote_url: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _clone_working_repository(remote_url: str, repo_path: str) -> None:
|
def _clone_working_repository(remote_url: str, repo_path: str, ssh_key: SSHKey | None = None) -> None:
|
||||||
|
env = None
|
||||||
|
key_path = None
|
||||||
|
|
||||||
|
if ssh_key is not None:
|
||||||
|
ssh_result = _prepare_ssh_env(ssh_key)
|
||||||
|
if ssh_result:
|
||||||
|
env, key_path = ssh_result
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git", "clone", remote_url, repo_path],
|
["git", "clone", remote_url, repo_path],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=300,
|
timeout=300,
|
||||||
|
env={**os.environ, **env} if env else None,
|
||||||
)
|
)
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="clone operation timed out")
|
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="clone operation timed out")
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
|
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="git command not found")
|
||||||
|
finally:
|
||||||
|
if key_path and os.path.exists(key_path):
|
||||||
|
os.unlink(key_path)
|
||||||
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@@ -352,11 +407,9 @@ async def create_repository(
|
|||||||
if parse_result["base_url"]:
|
if parse_result["base_url"]:
|
||||||
remote_url = parse_result["base_url"]
|
remote_url = parse_result["base_url"]
|
||||||
|
|
||||||
if remote_url:
|
|
||||||
_preflight_remote_repository(remote_url)
|
|
||||||
|
|
||||||
# Validate SSH key if provided
|
# Validate SSH key if provided
|
||||||
ssh_key_id = None
|
ssh_key_id = None
|
||||||
|
ssh_key = None
|
||||||
if data.ssh_key_id:
|
if data.ssh_key_id:
|
||||||
try:
|
try:
|
||||||
ssh_key_id = uuid.UUID(data.ssh_key_id)
|
ssh_key_id = uuid.UUID(data.ssh_key_id)
|
||||||
@@ -369,13 +422,16 @@ async def create_repository(
|
|||||||
if ssh_key.user_id != user_id and ssh_key.project_id != project_id:
|
if ssh_key.user_id != user_id and ssh_key.project_id != project_id:
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ssh key does not belong to user or project")
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="ssh key does not belong to user or project")
|
||||||
|
|
||||||
|
if remote_url:
|
||||||
|
_preflight_remote_repository(remote_url, ssh_key)
|
||||||
|
|
||||||
repo_path = _get_repo_path(user_id, project_id, data.name)
|
repo_path = _get_repo_path(user_id, project_id, data.name)
|
||||||
|
|
||||||
# Ensure parent directory exists
|
# Ensure parent directory exists
|
||||||
os.makedirs(os.path.dirname(repo_path), exist_ok=True)
|
os.makedirs(os.path.dirname(repo_path), exist_ok=True)
|
||||||
|
|
||||||
if remote_url:
|
if remote_url:
|
||||||
_clone_working_repository(remote_url, repo_path)
|
_clone_working_repository(remote_url, repo_path, ssh_key)
|
||||||
else:
|
else:
|
||||||
_init_working_repository(repo_path)
|
_init_working_repository(repo_path)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user