Add typed SSH remote endpoints
This commit is contained in:
@@ -7,7 +7,10 @@ use ironstorage::{
|
||||
authentication::{
|
||||
AuthenticationTimeout, DEFAULT_AUTHENTICATION_TIMEOUT, MAX_AUTHENTICATION_TIMEOUT,
|
||||
},
|
||||
config::{ConfigError, ConfigLoader, EditorSource, MobileAppearance},
|
||||
config::{
|
||||
ConfigError, ConfigLoader, EditorSource, MobileAppearance, RemoteEndpoint, RemoteTransport,
|
||||
SshRepositoryPath,
|
||||
},
|
||||
desktop::DesktopStorage,
|
||||
git::GitIdentity,
|
||||
mobile::MobileTab,
|
||||
@@ -84,12 +87,10 @@ fn explicit_relative_configuration_resolves_deterministically() -> TestResult {
|
||||
assert_eq!(config.git_remotes().len(), 1);
|
||||
let remote = &config.git_remotes()[0];
|
||||
assert_eq!(remote.name().as_str(), "origin");
|
||||
assert_eq!(
|
||||
remote.url().as_str(),
|
||||
"https://git.example.test/alice/store.git"
|
||||
);
|
||||
assert_eq!(remote.server_id().as_str(), "personal-git");
|
||||
assert_eq!(remote.application_id().as_str(), "ironstorage-cli");
|
||||
assert_eq!(remote.url(), "https://git.example.test/alice/store.git");
|
||||
let (server_id, application_id) = remote.https_credentials().expect("HTTPS credentials");
|
||||
assert_eq!(server_id.as_str(), "personal-git");
|
||||
assert_eq!(application_id.as_str(), "ironstorage-cli");
|
||||
assert_eq!(
|
||||
config.clipboard_timeout().duration(),
|
||||
DEFAULT_CLIPBOARD_TIMEOUT
|
||||
@@ -564,10 +565,9 @@ fn missing_and_invalid_required_fields_are_typed() -> TestResult {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_configuration_rejects_non_https_and_embedded_credentials() -> TestResult {
|
||||
fn git_configuration_rejects_forbidden_transports_and_embedded_credentials() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
for url in [
|
||||
"ssh://git@example.test/store.git",
|
||||
"git://example.test/store.git",
|
||||
"file:///tmp/store.git",
|
||||
"../store.git",
|
||||
@@ -602,6 +602,159 @@ application_id = "application"
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_remote_endpoints_parse_to_one_typed_contract() -> TestResult {
|
||||
let absolute = RemoteEndpoint::parse("ssh://git@example.test:2222/repos/store.git")?;
|
||||
assert_eq!(absolute.transport(), RemoteTransport::Ssh);
|
||||
let ssh = absolute.as_ssh().expect("SSH endpoint");
|
||||
assert_eq!(ssh.user(), Some("git"));
|
||||
assert_eq!(ssh.host(), "example.test");
|
||||
assert_eq!(ssh.port(), 2222);
|
||||
assert_eq!(
|
||||
ssh.path(),
|
||||
&SshRepositoryPath::Absolute("/repos/store.git".to_owned())
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
RemoteEndpoint::parse("ssh://git@example.test/repos/store.git")?,
|
||||
RemoteEndpoint::parse("git@example.test:/repos/store.git")?
|
||||
);
|
||||
assert_eq!(
|
||||
RemoteEndpoint::parse("ssh://git@example.test/~alice/store.git")?,
|
||||
RemoteEndpoint::parse("git@example.test:~alice/store.git")?
|
||||
);
|
||||
|
||||
let relative = RemoteEndpoint::parse("git@example.test:team/store.git")?;
|
||||
assert_eq!(
|
||||
relative.as_ssh().expect("SSH endpoint").path(),
|
||||
&SshRepositoryPath::Relative("team/store.git".to_owned())
|
||||
);
|
||||
let ipv6 = RemoteEndpoint::parse("ssh://git@[2001:db8::1]:2200/store.git")?;
|
||||
assert_eq!(ipv6.as_ssh().expect("IPv6 endpoint").port(), 2200);
|
||||
let ipv4 = RemoteEndpoint::parse("git@192.0.2.10:team/store.git")?;
|
||||
assert_eq!(ipv4.as_ssh().expect("IPv4 endpoint").host(), "192.0.2.10");
|
||||
|
||||
let unicode = RemoteEndpoint::parse("git@bücher.example:team/密码.git")?;
|
||||
let unicode = unicode.as_ssh().expect("Unicode endpoint");
|
||||
assert_eq!(unicode.host(), "xn--bcher-kva.example");
|
||||
assert_eq!(unicode.path().as_str(), "team/密码.git");
|
||||
|
||||
let inert = "team/repo';touch${IFS}pwned.git";
|
||||
assert_eq!(
|
||||
RemoteEndpoint::parse(&format!("git@example.test:{inert}"))?
|
||||
.as_ssh()
|
||||
.expect("literal path")
|
||||
.path()
|
||||
.as_str(),
|
||||
inert
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_credentials_are_explicitly_transport_specific() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "git@example.test:team/store.git"
|
||||
server_id = "server"
|
||||
application_id = "application"
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(&fixture.explicit_path()))
|
||||
.expect_err("HTTPS credentials cannot configure SSH"),
|
||||
ConfigError::InvalidField {
|
||||
field: "git.remotes.https_credentials"
|
||||
}
|
||||
);
|
||||
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "https://example.test/team/store.git"
|
||||
"#,
|
||||
)?;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.loader()
|
||||
.load(Some(&fixture.explicit_path()))
|
||||
.expect_err("HTTPS credentials are required"),
|
||||
ConfigError::MissingField {
|
||||
field: "git.remotes.server_id"
|
||||
}
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_remote_configuration_round_trips_without_https_credentials() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
fixture.write_explicit(
|
||||
r#"
|
||||
vault = "vault"
|
||||
default_key = "alice"
|
||||
key_material = "keys"
|
||||
|
||||
[[git.remotes]]
|
||||
name = "origin"
|
||||
url = "git@example.test:team/store.git"
|
||||
"#,
|
||||
)?;
|
||||
let config = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
let remote = &config.git_remotes()[0];
|
||||
assert_eq!(remote.url(), "git@example.test:team/store.git");
|
||||
assert!(remote.https_credentials().is_none());
|
||||
|
||||
config.update_git_identity(&GitIdentity::new("Alice", "alice@example.test")?)?;
|
||||
let reloaded = fixture.loader().load(Some(&fixture.explicit_path()))?;
|
||||
assert_eq!(reloaded.git_remotes(), config.git_remotes());
|
||||
let persisted = fs::read_to_string(fixture.explicit_path())?;
|
||||
assert!(!persisted.contains("server_id"));
|
||||
assert!(!persisted.contains("application_id"));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ssh_remote_parser_rejects_ambiguous_local_and_executable_inputs() {
|
||||
for remote in [
|
||||
"",
|
||||
"ssh://example.test",
|
||||
"ssh://example.test/",
|
||||
"ssh://user:secret@example.test/store.git",
|
||||
"ssh://example.test/store.git?option=value",
|
||||
"ssh://example.test/store.git#fragment",
|
||||
"ssh://example.test/%0Acommand",
|
||||
"git://example.test/store.git",
|
||||
"file:///tmp/store.git",
|
||||
"ext::helper command",
|
||||
"../store.git",
|
||||
"/tmp/store.git",
|
||||
"C:/store.git",
|
||||
"git@2001:db8::1:store.git",
|
||||
"git@example.test:-upload-pack=evil",
|
||||
"git@example.test:repo\ncommand",
|
||||
] {
|
||||
assert!(
|
||||
RemoteEndpoint::parse(remote).is_err(),
|
||||
"forbidden remote should fail closed: {remote:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duplicate_remote_names_and_credential_references_are_rejected() -> TestResult {
|
||||
let fixture = ConfigurationFixture::new()?;
|
||||
|
||||
Reference in New Issue
Block a user