Add typed SSH remote endpoints
This commit is contained in:
1 parent
f0d6a04be1
commit
f636f3b551
16 files changed
+1712
-310
No files matched your search
+65
-34
@@ -24,7 +24,7 @@ use sha1::{Digest as _, Sha1};
|
||||
use zeroize::Zeroize as _;
|
||||
|
||||
use crate::{
|
||||
config::{ApplicationId, GitRemote, ServerId},
|
||||
config::{ApplicationId, GitRemote, RemoteEndpoint, RemoteTransport, ServerId},
|
||||
crypto::{KeyHandle, KeyStore, SecretProvider},
|
||||
mutation::{TreeCommit, TreeCommitError, TreeCommitter},
|
||||
recipient::{PolicyCommit, PolicyCommitError, PolicyCommitter},
|
||||
@@ -384,6 +384,9 @@ pub enum GitError {
|
||||
DirtyWorktree,
|
||||
InvalidRemoteName,
|
||||
ForbiddenRemoteUrl,
|
||||
UnsupportedRemoteTransport {
|
||||
transport: RemoteTransport,
|
||||
},
|
||||
RemoteNotFound {
|
||||
name: String,
|
||||
},
|
||||
@@ -429,7 +432,13 @@ impl fmt::Display for GitError {
|
||||
Self::DirtyWorktree => formatter.write_str("the worktree has uncommitted changes"),
|
||||
Self::InvalidRemoteName => formatter.write_str("the Git remote name is invalid"),
|
||||
Self::ForbiddenRemoteUrl => {
|
||||
formatter.write_str("Git remotes must use credential-free HTTPS URLs")
|
||||
formatter.write_str("the Git remote URL is invalid or forbidden")
|
||||
}
|
||||
Self::UnsupportedRemoteTransport { transport } => {
|
||||
write!(
|
||||
formatter,
|
||||
"this build does not support {transport} Git remotes"
|
||||
)
|
||||
}
|
||||
Self::RemoteNotFound { name } => write!(formatter, "Git remote not found: {name}"),
|
||||
Self::CredentialsUnavailable => {
|
||||
@@ -861,7 +870,7 @@ impl GitRepository {
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Vec<String>, GitError> {
|
||||
control.report(GitProgressPhase::Validating)?;
|
||||
validate_https_remote(configured.url().as_str())?;
|
||||
require_https_remote(configured)?;
|
||||
ensure_clone_parent(parent)?;
|
||||
let temporary = private_temporary_directory(parent, "probe")?;
|
||||
let result = (|| {
|
||||
@@ -874,7 +883,7 @@ impl GitRepository {
|
||||
)
|
||||
.map_err(invalid)?;
|
||||
let mut repository = Self::open(&store, identity)?;
|
||||
repository.add_remote(configured.name().as_str(), configured.url().as_str())?;
|
||||
repository.add_remote(configured.name().as_str(), configured.url())?;
|
||||
repository.fetch_with_transport_controlled(
|
||||
configured,
|
||||
credentials,
|
||||
@@ -951,7 +960,7 @@ impl GitRepository {
|
||||
control: &GitOperationControl,
|
||||
) -> Result<Self, GitError> {
|
||||
control.report(GitProgressPhase::Validating)?;
|
||||
validate_https_remote(configured.url().as_str())?;
|
||||
require_https_remote(configured)?;
|
||||
if let Some(branch) = branch {
|
||||
validate_remote_name(branch)?;
|
||||
}
|
||||
@@ -984,7 +993,7 @@ impl GitRepository {
|
||||
)
|
||||
.map_err(invalid)?;
|
||||
let mut repository = Self::open(&store, identity.clone())?;
|
||||
repository.add_remote(configured.name().as_str(), configured.url().as_str())?;
|
||||
repository.add_remote(configured.name().as_str(), configured.url())?;
|
||||
repository.pull_with_transport_controlled(
|
||||
configured,
|
||||
branch,
|
||||
@@ -1167,13 +1176,15 @@ impl GitRepository {
|
||||
name: name.to_owned(),
|
||||
})?;
|
||||
let text = url.to_bstring().to_str_lossy().into_owned();
|
||||
validate_https_remote(&text)?;
|
||||
RemoteEndpoint::parse(&text).map_err(|_| GitError::ForbiddenRemoteUrl)?;
|
||||
Ok(text)
|
||||
}
|
||||
|
||||
pub fn add_remote(&mut self, name: &str, url: &str) -> Result<(), GitError> {
|
||||
validate_remote_name(name)?;
|
||||
validate_https_remote(url)?;
|
||||
ensure_remote_transport_available(
|
||||
&RemoteEndpoint::parse(url).map_err(|_| GitError::ForbiddenRemoteUrl)?,
|
||||
)?;
|
||||
if self.remotes().iter().any(|existing| existing == name) {
|
||||
return Err(GitError::InvalidRepository(format!(
|
||||
"remote {name} already exists"
|
||||
@@ -1197,7 +1208,9 @@ impl GitRepository {
|
||||
|
||||
pub fn set_remote_url(&mut self, name: &str, url: &str) -> Result<(), GitError> {
|
||||
validate_remote_name(name)?;
|
||||
validate_https_remote(url)?;
|
||||
ensure_remote_transport_available(
|
||||
&RemoteEndpoint::parse(url).map_err(|_| GitError::ForbiddenRemoteUrl)?,
|
||||
)?;
|
||||
if !self.remotes().iter().any(|existing| existing == name) {
|
||||
return Err(GitError::RemoteNotFound {
|
||||
name: name.to_owned(),
|
||||
@@ -1248,7 +1261,9 @@ impl GitRepository {
|
||||
));
|
||||
}
|
||||
validate_remote_name(&parts[1..parts.len() - 1].join("."))?;
|
||||
validate_https_remote(value)?;
|
||||
ensure_remote_transport_available(
|
||||
&RemoteEndpoint::parse(value).map_err(|_| GitError::ForbiddenRemoteUrl)?,
|
||||
)?;
|
||||
}
|
||||
if value.contains(['\n', '\r', '\0']) {
|
||||
return Err(GitError::InvalidRepository(
|
||||
@@ -1293,12 +1308,12 @@ impl GitRepository {
|
||||
control.checkpoint(GitProgressPhase::Validating)?;
|
||||
let name = configured.name().as_str();
|
||||
let actual_url = self.remote_url(name)?;
|
||||
if actual_url != configured.url().as_str() {
|
||||
if !same_remote_endpoint(&actual_url, configured.url())? {
|
||||
return Err(GitError::ForbiddenRemoteUrl);
|
||||
}
|
||||
let (_, server_id, application_id) = require_https_remote(configured)?;
|
||||
control.checkpoint(GitProgressPhase::Authenticating)?;
|
||||
let credential =
|
||||
credentials.credential(configured.server_id(), configured.application_id())?;
|
||||
let credential = credentials.credential(server_id, application_id)?;
|
||||
let received_pack = transport.fetch_controlled(self, configured, &credential, control)?;
|
||||
Ok(FetchOutcome {
|
||||
remote: name.to_owned(),
|
||||
@@ -1333,7 +1348,7 @@ impl GitRepository {
|
||||
.map_err(|_| GitError::CredentialsUnavailable)?
|
||||
.to_owned();
|
||||
let username = credential.username().to_owned();
|
||||
let expected_origin = configured.url().clone();
|
||||
let expected_origin = require_https_remote(configured)?.0.clone();
|
||||
let remote = self
|
||||
.repository
|
||||
.find_fetch_remote(Some(name.into()))
|
||||
@@ -1576,10 +1591,10 @@ impl GitRepository {
|
||||
}
|
||||
let name = configured.name().as_str();
|
||||
let actual_url = self.remote_url(name)?;
|
||||
if actual_url != configured.url().as_str() {
|
||||
if !same_remote_endpoint(&actual_url, configured.url())? {
|
||||
return Err(GitError::ForbiddenRemoteUrl);
|
||||
}
|
||||
let url = validate_https_remote(&actual_url)?;
|
||||
let (url, server_id, application_id) = require_https_remote(configured)?;
|
||||
let branch = branch.map_or_else(
|
||||
|| self.current_branch(),
|
||||
|branch| {
|
||||
@@ -1594,10 +1609,9 @@ impl GitRepository {
|
||||
.map_err(|_| GitError::UnbornHead)?
|
||||
.detach();
|
||||
control.checkpoint(GitProgressPhase::Authenticating)?;
|
||||
let credential =
|
||||
credentials.credential(configured.server_id(), configured.application_id())?;
|
||||
let credential = credentials.credential(server_id, application_id)?;
|
||||
let advertisement =
|
||||
transport.advertise_receive_pack_controlled(&url, &credential, control)?;
|
||||
transport.advertise_receive_pack_controlled(url, &credential, control)?;
|
||||
let advertised = parse_receive_pack_advertisement(&advertisement)?;
|
||||
let old = advertised.refs.get(&reference).copied();
|
||||
if let Some(old) = old {
|
||||
@@ -1635,7 +1649,7 @@ impl GitRepository {
|
||||
let mut request = encode_pkt_line(command.as_bytes())?;
|
||||
request.extend_from_slice(b"0000");
|
||||
request.extend_from_slice(&pack);
|
||||
let response = transport.receive_pack_controlled(&url, &credential, request, control)?;
|
||||
let response = transport.receive_pack_controlled(url, &credential, request, control)?;
|
||||
parse_receive_pack_result(&response, &reference)?;
|
||||
self.update_remote_tracking(name, &branch, new)?;
|
||||
Ok(PushOutcome {
|
||||
@@ -1706,7 +1720,7 @@ impl GitRepository {
|
||||
let name = configured.name().as_str();
|
||||
let actual_url = if self.remotes().iter().any(|remote| remote == name) {
|
||||
let actual = self.remote_url(name)?;
|
||||
if actual != configured.url().as_str() {
|
||||
if !same_remote_endpoint(&actual, configured.url())? {
|
||||
return Err(GitError::ForbiddenRemoteUrl);
|
||||
}
|
||||
actual
|
||||
@@ -1756,7 +1770,7 @@ impl GitRepository {
|
||||
let status = self.status()?;
|
||||
let name = configured.name().as_str();
|
||||
let actual_url = self.remote_url(name)?;
|
||||
if actual_url != configured.url().as_str() {
|
||||
if !same_remote_endpoint(&actual_url, configured.url())? {
|
||||
return Err(GitError::ForbiddenRemoteUrl);
|
||||
}
|
||||
let local_id = self
|
||||
@@ -3375,18 +3389,34 @@ fn private_temporary_directory(parent: &Path, purpose: &str) -> Result<PathBuf,
|
||||
.ok_or_else(|| io("create private Git directory", parent))
|
||||
}
|
||||
|
||||
fn validate_https_remote(value: &str) -> Result<url::Url, GitError> {
|
||||
let parsed = url::Url::parse(value).map_err(|_| GitError::ForbiddenRemoteUrl)?;
|
||||
if parsed.scheme() != "https"
|
||||
|| parsed.host_str().is_none()
|
||||
|| !parsed.username().is_empty()
|
||||
|| parsed.password().is_some()
|
||||
|| parsed.query().is_some()
|
||||
|| parsed.fragment().is_some()
|
||||
{
|
||||
return Err(GitError::ForbiddenRemoteUrl);
|
||||
fn require_https_remote(
|
||||
remote: &GitRemote,
|
||||
) -> Result<(&url::Url, &ServerId, &ApplicationId), GitError> {
|
||||
let url = remote
|
||||
.endpoint()
|
||||
.as_https()
|
||||
.ok_or(GitError::UnsupportedRemoteTransport {
|
||||
transport: RemoteTransport::Ssh,
|
||||
})?;
|
||||
let (server_id, application_id) = remote
|
||||
.https_credentials()
|
||||
.ok_or(GitError::ForbiddenRemoteUrl)?;
|
||||
Ok((url, server_id, application_id))
|
||||
}
|
||||
|
||||
fn same_remote_endpoint(actual: &str, configured: &str) -> Result<bool, GitError> {
|
||||
let actual = RemoteEndpoint::parse(actual).map_err(|_| GitError::ForbiddenRemoteUrl)?;
|
||||
let configured = RemoteEndpoint::parse(configured).map_err(|_| GitError::ForbiddenRemoteUrl)?;
|
||||
Ok(actual == configured)
|
||||
}
|
||||
|
||||
fn ensure_remote_transport_available(endpoint: &RemoteEndpoint) -> Result<(), GitError> {
|
||||
if endpoint.transport() == RemoteTransport::Ssh && !cfg!(feature = "ssh") {
|
||||
return Err(GitError::UnsupportedRemoteTransport {
|
||||
transport: RemoteTransport::Ssh,
|
||||
});
|
||||
}
|
||||
Ok(parsed)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn same_https_origin(expected: &url::Url, requested: &url::Url) -> bool {
|
||||
@@ -3485,7 +3515,8 @@ fn validate_local_config_security(config: &gix_config::File) -> Result<(), GitEr
|
||||
));
|
||||
}
|
||||
if let Ok(url) = config.raw_value_by("remote", Some(name.into()), "url") {
|
||||
validate_https_remote(url.to_str().map_err(|_| GitError::ForbiddenRemoteUrl)?)?;
|
||||
RemoteEndpoint::parse(url.to_str().map_err(|_| GitError::ForbiddenRemoteUrl)?)
|
||||
.map_err(|_| GitError::ForbiddenRemoteUrl)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user