git_deploy: relax validation requirements
Do not report invalid if the remote or branch does not match the configured values. In these conditions report them as "repo_warnings" that frontends may display to the user. Hard recovery now requires a recovery URL detected from the git repo's "origin" remote. This closes a potential security issue where a malicioius repo could be cloned over an installed repo. Signed-off-by: Eric Callahan <arskine.code@gmail.com>
This commit is contained in:
parent
35396a5b2a
commit
a7b9e5783d
|
@ -33,7 +33,7 @@ class GitDeploy(AppDeploy):
|
||||||
super().__init__(config, cmd_helper)
|
super().__init__(config, cmd_helper)
|
||||||
self.repo = GitRepo(
|
self.repo = GitRepo(
|
||||||
cmd_helper, self.path, self.name, self.origin,
|
cmd_helper, self.path, self.name, self.origin,
|
||||||
self.moved_origin, self.channel
|
self.moved_origin, self.primary_branch, self.channel
|
||||||
)
|
)
|
||||||
if self.type != 'git_repo':
|
if self.type != 'git_repo':
|
||||||
self.need_channel_update = True
|
self.need_channel_update = True
|
||||||
|
@ -47,6 +47,8 @@ class GitDeploy(AppDeploy):
|
||||||
async def initialize(self) -> Dict[str, Any]:
|
async def initialize(self) -> Dict[str, Any]:
|
||||||
storage = await super().initialize()
|
storage = await super().initialize()
|
||||||
self.repo.restore_state(storage)
|
self.repo.restore_state(storage)
|
||||||
|
if not self.needs_refresh():
|
||||||
|
self.repo.log_repo_info()
|
||||||
return storage
|
return storage
|
||||||
|
|
||||||
async def refresh(self) -> None:
|
async def refresh(self) -> None:
|
||||||
|
@ -62,11 +64,8 @@ class GitDeploy(AppDeploy):
|
||||||
f"Channel: {self.channel}, "
|
f"Channel: {self.channel}, "
|
||||||
f"Need Channel Update: {self.need_channel_update}"
|
f"Need Channel Update: {self.need_channel_update}"
|
||||||
)
|
)
|
||||||
invalids = self.repo.report_invalids(self.primary_branch)
|
if not self.repo.check_is_valid():
|
||||||
if invalids:
|
self.log_info("Repo validation check failed")
|
||||||
msgs = '\n'.join(invalids)
|
|
||||||
self.log_info(
|
|
||||||
f"Repo validation checks failed:\n{msgs}")
|
|
||||||
if self.server.is_debug_enabled():
|
if self.server.is_debug_enabled():
|
||||||
self._is_valid = True
|
self._is_valid = True
|
||||||
self.log_info(
|
self.log_info(
|
||||||
|
@ -276,6 +275,7 @@ class GitRepo:
|
||||||
alias: str,
|
alias: str,
|
||||||
origin_url: str,
|
origin_url: str,
|
||||||
moved_origin_url: Optional[str],
|
moved_origin_url: Optional[str],
|
||||||
|
primary_branch: str,
|
||||||
channel: str
|
channel: str
|
||||||
) -> None:
|
) -> None:
|
||||||
self.server = cmd_helper.get_server()
|
self.server = cmd_helper.get_server()
|
||||||
|
@ -286,7 +286,10 @@ class GitRepo:
|
||||||
git_base = git_path.name
|
git_base = git_path.name
|
||||||
self.backup_path = git_dir.joinpath(f".{git_base}_repo_backup")
|
self.backup_path = git_dir.joinpath(f".{git_base}_repo_backup")
|
||||||
self.origin_url = origin_url
|
self.origin_url = origin_url
|
||||||
|
if not self.origin_url.endswith(".git"):
|
||||||
|
self.origin_url += ".git"
|
||||||
self.moved_origin_url = moved_origin_url
|
self.moved_origin_url = moved_origin_url
|
||||||
|
self.primary_branch = primary_branch
|
||||||
self.recovery_message = \
|
self.recovery_message = \
|
||||||
f"""
|
f"""
|
||||||
Manually restore via SSH with the following commands:
|
Manually restore via SSH with the following commands:
|
||||||
|
@ -297,6 +300,7 @@ class GitRepo:
|
||||||
sudo service {self.alias} start
|
sudo service {self.alias} start
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
self.repo_warnings: List[str] = []
|
||||||
self.init_evt: Optional[asyncio.Event] = None
|
self.init_evt: Optional[asyncio.Event] = None
|
||||||
self.initialized: bool = False
|
self.initialized: bool = False
|
||||||
self.git_operation_lock = asyncio.Lock()
|
self.git_operation_lock = asyncio.Lock()
|
||||||
|
@ -319,6 +323,10 @@ class GitRepo:
|
||||||
self.current_commit: str = storage.get('current_commit', "?")
|
self.current_commit: str = storage.get('current_commit', "?")
|
||||||
self.upstream_commit: str = storage.get('upstream_commit', "?")
|
self.upstream_commit: str = storage.get('upstream_commit', "?")
|
||||||
self.upstream_url: str = storage.get('upstream_url', "?")
|
self.upstream_url: str = storage.get('upstream_url', "?")
|
||||||
|
self.recovery_url: str = storage.get(
|
||||||
|
'recovery_url',
|
||||||
|
self.upstream_url if self.git_remote == "origin" else "?"
|
||||||
|
)
|
||||||
self.full_version_string: str = storage.get('full_version_string', "?")
|
self.full_version_string: str = storage.get('full_version_string', "?")
|
||||||
self.branches: List[str] = storage.get('branches', [])
|
self.branches: List[str] = storage.get('branches', [])
|
||||||
self.dirty: bool = storage.get('dirty', False)
|
self.dirty: bool = storage.get('dirty', False)
|
||||||
|
@ -328,10 +336,8 @@ class GitRepo:
|
||||||
'commits_behind', [])
|
'commits_behind', [])
|
||||||
self.tag_data: Dict[str, Any] = storage.get('tag_data', {})
|
self.tag_data: Dict[str, Any] = storage.get('tag_data', {})
|
||||||
self.diverged: bool = storage.get("diverged", False)
|
self.diverged: bool = storage.get("diverged", False)
|
||||||
self.repo_verified: bool = storage.get(
|
|
||||||
"verified", storage.get("is_valid", False)
|
|
||||||
)
|
|
||||||
self.repo_corrupt: bool = storage.get('corrupt', False)
|
self.repo_corrupt: bool = storage.get('corrupt', False)
|
||||||
|
self._check_warnings()
|
||||||
|
|
||||||
def get_persistent_data(self) -> Dict[str, Any]:
|
def get_persistent_data(self) -> Dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
|
@ -345,6 +351,7 @@ class GitRepo:
|
||||||
'current_commit': self.current_commit,
|
'current_commit': self.current_commit,
|
||||||
'upstream_commit': self.upstream_commit,
|
'upstream_commit': self.upstream_commit,
|
||||||
'upstream_url': self.upstream_url,
|
'upstream_url': self.upstream_url,
|
||||||
|
'recovery_url': self.recovery_url,
|
||||||
'full_version_string': self.full_version_string,
|
'full_version_string': self.full_version_string,
|
||||||
'branches': self.branches,
|
'branches': self.branches,
|
||||||
'dirty': self.dirty,
|
'dirty': self.dirty,
|
||||||
|
@ -353,7 +360,6 @@ class GitRepo:
|
||||||
'commits_behind': self.commits_behind,
|
'commits_behind': self.commits_behind,
|
||||||
'tag_data': self.tag_data,
|
'tag_data': self.tag_data,
|
||||||
'diverged': self.diverged,
|
'diverged': self.diverged,
|
||||||
'verified': self.repo_verified,
|
|
||||||
'corrupt': self.repo_corrupt
|
'corrupt': self.repo_corrupt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -379,7 +385,21 @@ class GitRepo:
|
||||||
self.upstream_url = await self.remote(f"get-url {self.git_remote}")
|
self.upstream_url = await self.remote(f"get-url {self.git_remote}")
|
||||||
if await self._check_moved_origin():
|
if await self._check_moved_origin():
|
||||||
need_fetch = True
|
need_fetch = True
|
||||||
|
if self.git_remote == "origin":
|
||||||
|
self.recovery_url = self.upstream_url
|
||||||
|
else:
|
||||||
|
remote_list = (await self.remote("")).splitlines()
|
||||||
|
logging.debug(
|
||||||
|
f"Git Repo {self.alias}: Detected Remotes - {remote_list}"
|
||||||
|
)
|
||||||
|
if "origin" in remote_list:
|
||||||
|
self.recovery_url = await self.remote("get-url origin")
|
||||||
|
else:
|
||||||
|
logging.info(
|
||||||
|
f"Git Repo {self.alias}: Unable to detect recovery URL, "
|
||||||
|
"Hard Recovery not available"
|
||||||
|
)
|
||||||
|
self.recovery_url = "?"
|
||||||
if need_fetch:
|
if need_fetch:
|
||||||
await self.fetch()
|
await self.fetch()
|
||||||
self.diverged = await self.check_diverged()
|
self.diverged = await self.check_diverged()
|
||||||
|
@ -431,7 +451,7 @@ class GitRepo:
|
||||||
if i < 30 or tag is not None:
|
if i < 30 or tag is not None:
|
||||||
commit['tag'] = tag
|
commit['tag'] = tag
|
||||||
self.commits_behind.append(commit)
|
self.commits_behind.append(commit)
|
||||||
|
self._check_warnings()
|
||||||
self.log_repo_info()
|
self.log_repo_info()
|
||||||
except Exception:
|
except Exception:
|
||||||
logging.exception(f"Git Repo {self.alias}: Initialization failure")
|
logging.exception(f"Git Repo {self.alias}: Initialization failure")
|
||||||
|
@ -684,6 +704,10 @@ class GitRepo:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def log_repo_info(self) -> None:
|
def log_repo_info(self) -> None:
|
||||||
|
warnings = ""
|
||||||
|
if self.repo_warnings:
|
||||||
|
warnings = "\nRepo Warnings:\n"
|
||||||
|
warnings += '\n'.join([f" {warn}" for warn in self.repo_warnings])
|
||||||
logging.info(
|
logging.info(
|
||||||
f"Git Repo {self.alias} Detected:\n"
|
f"Git Repo {self.alias} Detected:\n"
|
||||||
f"Owner: {self.git_owner}\n"
|
f"Owner: {self.git_owner}\n"
|
||||||
|
@ -692,6 +716,7 @@ class GitRepo:
|
||||||
f"Remote: {self.git_remote}\n"
|
f"Remote: {self.git_remote}\n"
|
||||||
f"Branch: {self.git_branch}\n"
|
f"Branch: {self.git_branch}\n"
|
||||||
f"Remote URL: {self.upstream_url}\n"
|
f"Remote URL: {self.upstream_url}\n"
|
||||||
|
f"Recovery URL: {self.recovery_url}\n"
|
||||||
f"Current Commit SHA: {self.current_commit}\n"
|
f"Current Commit SHA: {self.current_commit}\n"
|
||||||
f"Upstream Commit SHA: {self.upstream_commit}\n"
|
f"Upstream Commit SHA: {self.upstream_commit}\n"
|
||||||
f"Current Version: {self.current_version}\n"
|
f"Current Version: {self.current_version}\n"
|
||||||
|
@ -702,27 +727,31 @@ class GitRepo:
|
||||||
f"Tag Data: {self.tag_data}\n"
|
f"Tag Data: {self.tag_data}\n"
|
||||||
f"Bound Repo: {self.bound_repo}\n"
|
f"Bound Repo: {self.bound_repo}\n"
|
||||||
f"Diverged: {self.diverged}"
|
f"Diverged: {self.diverged}"
|
||||||
|
f"{warnings}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def report_invalids(self, primary_branch: str) -> List[str]:
|
def _check_warnings(self) -> None:
|
||||||
invalids: List[str] = []
|
if self.upstream_url == "?":
|
||||||
|
self.repo_warnings.append("Failed to detect repo url")
|
||||||
|
return
|
||||||
|
self.repo_warnings.clear()
|
||||||
upstream_url = self.upstream_url.lower()
|
upstream_url = self.upstream_url.lower()
|
||||||
if upstream_url[-4:] != ".git":
|
if upstream_url[-4:] != ".git":
|
||||||
upstream_url += ".git"
|
upstream_url += ".git"
|
||||||
if upstream_url != self.origin_url.lower():
|
if upstream_url != self.origin_url.lower():
|
||||||
invalids.append(f"Unofficial remote url: {self.upstream_url}")
|
self.repo_warnings.append(f"Unofficial remote url: {self.upstream_url}")
|
||||||
if self.git_branch != primary_branch or self.git_remote != "origin":
|
if self.git_branch != self.primary_branch or self.git_remote != "origin":
|
||||||
invalids.append(
|
self.repo_warnings.append(
|
||||||
"Repo not on valid remote branch, expected: "
|
"Repo not on offical remote/branch, expected: "
|
||||||
f"origin/{primary_branch}, detected: "
|
f"origin/{self.primary_branch}, detected: "
|
||||||
f"{self.git_remote}/{self.git_branch}")
|
f"{self.git_remote}/{self.git_branch}")
|
||||||
if self.head_detached:
|
if self.head_detached:
|
||||||
invalids.append("Detached HEAD detected")
|
self.repo_warnings.append("Detached HEAD detected")
|
||||||
if self.diverged:
|
if self.diverged:
|
||||||
invalids.append("Repo has diverged from remote")
|
self.repo_warnings.append("Repo has diverged from remote")
|
||||||
if not invalids:
|
|
||||||
self.repo_verified = True
|
def check_is_valid(self):
|
||||||
return invalids
|
return not self.head_detached and not self.diverged
|
||||||
|
|
||||||
def _verify_repo(self, check_remote: bool = False) -> None:
|
def _verify_repo(self, check_remote: bool = False) -> None:
|
||||||
if not self.valid_git_repo:
|
if not self.valid_git_repo:
|
||||||
|
@ -821,9 +850,9 @@ class GitRepo:
|
||||||
|
|
||||||
async def clone(self) -> None:
|
async def clone(self) -> None:
|
||||||
async with self.git_operation_lock:
|
async with self.git_operation_lock:
|
||||||
if not self.repo_verified:
|
if self.recovery_url == "?":
|
||||||
raise self.server.error(
|
raise self.server.error(
|
||||||
"Repo has not been verified, clone aborted"
|
"Recovery url has not been detected, clone aborted"
|
||||||
)
|
)
|
||||||
self.cmd_helper.notify_update_response(
|
self.cmd_helper.notify_update_response(
|
||||||
f"Git Repo {self.alias}: Starting Clone Recovery...")
|
f"Git Repo {self.alias}: Starting Clone Recovery...")
|
||||||
|
@ -831,7 +860,7 @@ class GitRepo:
|
||||||
if self.backup_path.exists():
|
if self.backup_path.exists():
|
||||||
await event_loop.run_in_thread(shutil.rmtree, self.backup_path)
|
await event_loop.run_in_thread(shutil.rmtree, self.backup_path)
|
||||||
await self._check_lock_file_exists(remove=True)
|
await self._check_lock_file_exists(remove=True)
|
||||||
git_cmd = f"clone {self.origin_url} {self.backup_path}"
|
git_cmd = f"clone {self.recovery_url} {self.backup_path}"
|
||||||
try:
|
try:
|
||||||
await self._run_git_cmd_async(git_cmd, 1, False, False)
|
await self._run_git_cmd_async(git_cmd, 1, False, False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -915,6 +944,8 @@ class GitRepo:
|
||||||
'branch': self.git_branch,
|
'branch': self.git_branch,
|
||||||
'owner': self.git_owner,
|
'owner': self.git_owner,
|
||||||
'repo_name': self.git_repo_name,
|
'repo_name': self.git_repo_name,
|
||||||
|
'remote_url': self.upstream_url,
|
||||||
|
'recovery_url': self.recovery_url,
|
||||||
'version': self.current_version,
|
'version': self.current_version,
|
||||||
'remote_version': self.upstream_version,
|
'remote_version': self.upstream_version,
|
||||||
'current_hash': self.current_commit,
|
'current_hash': self.current_commit,
|
||||||
|
@ -925,7 +956,8 @@ class GitRepo:
|
||||||
'git_messages': self.git_messages,
|
'git_messages': self.git_messages,
|
||||||
'full_version_string': self.full_version_string,
|
'full_version_string': self.full_version_string,
|
||||||
'pristine': not self.dirty,
|
'pristine': not self.dirty,
|
||||||
'corrupt': self.repo_corrupt
|
'corrupt': self.repo_corrupt,
|
||||||
|
'warnings': self.repo_warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_version(self, upstream: bool = False) -> Tuple[Any, ...]:
|
def get_version(self, upstream: bool = False) -> Tuple[Any, ...]:
|
||||||
|
|
Loading…
Reference in New Issue