fix: constrain repository SSH commands to authorized source
This commit is contained in:
@@ -575,7 +575,8 @@ async fn create_working_directory(
|
|||||||
}
|
}
|
||||||
let working_directory = state
|
let working_directory = state
|
||||||
.runtime
|
.runtime
|
||||||
.create_working_directory(request)
|
.create_working_directory_from_resource(request)
|
||||||
|
.await
|
||||||
.map_err(RuntimeHttpRestError::runtime)?;
|
.map_err(RuntimeHttpRestError::runtime)?;
|
||||||
Ok(Json(RuntimeHttpWorkingDirectoryResponse {
|
Ok(Json(RuntimeHttpWorkingDirectoryResponse {
|
||||||
working_directory,
|
working_directory,
|
||||||
|
|||||||
@@ -28,11 +28,9 @@ use worker_runtime::{Runtime, RuntimeOptions};
|
|||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
let mut arguments = std::env::args().skip(1).collect::<Vec<_>>();
|
let mut arguments = std::env::args().skip(1).collect::<Vec<_>>();
|
||||||
if arguments.first().map(String::as_str) == Some("__repository-read-only-ssh") {
|
if arguments.first().map(String::as_str) == Some("__repository-ssh") {
|
||||||
arguments.remove(0);
|
arguments.remove(0);
|
||||||
return match worker_runtime::working_directory::run_repository_read_only_ssh_client(
|
return match worker_runtime::working_directory::run_repository_ssh_client(&arguments) {
|
||||||
&arguments,
|
|
||||||
) {
|
|
||||||
Ok(status) => ExitCode::from(u8::try_from(status).unwrap_or(1)),
|
Ok(status) => ExitCode::from(u8::try_from(status).unwrap_or(1)),
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
eprintln!("{error}");
|
eprintln!("{error}");
|
||||||
|
|||||||
@@ -378,6 +378,20 @@ impl Runtime {
|
|||||||
.map_err(RuntimeError::from)
|
.map_err(RuntimeError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn create_working_directory_from_resource(
|
||||||
|
&self,
|
||||||
|
mut request: WorkingDirectoryRequest,
|
||||||
|
) -> Result<CatalogWorkingDirectoryStatus, RuntimeError> {
|
||||||
|
if let Some(ssh) = request
|
||||||
|
.materialization
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|materialization| materialization.ssh.as_mut())
|
||||||
|
{
|
||||||
|
self.resolve_repository_access_resource(ssh).await?;
|
||||||
|
}
|
||||||
|
self.create_working_directory(request)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn authorize_working_directory_repository_access(
|
pub fn authorize_working_directory_repository_access(
|
||||||
&self,
|
&self,
|
||||||
request: WorkingDirectoryRepositoryAccessRequest,
|
request: WorkingDirectoryRepositoryAccessRequest,
|
||||||
@@ -397,6 +411,59 @@ impl Runtime {
|
|||||||
.map_err(RuntimeError::from)
|
.map_err(RuntimeError::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn resolve_repository_access_resource(
|
||||||
|
&self,
|
||||||
|
ssh: &mut crate::catalog::RepositorySshMaterializationAccess,
|
||||||
|
) -> Result<(), RuntimeError> {
|
||||||
|
if !ssh.private_key.expose().is_empty() && !ssh.known_hosts_entry.expose().is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let (client, runtime_id) = {
|
||||||
|
let state = self.lock()?;
|
||||||
|
let client = state.backend_resource_client.clone().ok_or_else(|| {
|
||||||
|
RuntimeError::InvalidRequest(
|
||||||
|
"Backend Repository access resource client is unavailable".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let runtime_id = state.runtime_identity.clone().ok_or_else(|| {
|
||||||
|
RuntimeError::InvalidRequest("Runtime identity is unavailable".to_string())
|
||||||
|
})?;
|
||||||
|
(client, runtime_id)
|
||||||
|
};
|
||||||
|
let mut response = client
|
||||||
|
.0
|
||||||
|
.fetch_resource(BackendResourceFetchRequest {
|
||||||
|
handle: ssh.secret_resource.clone(),
|
||||||
|
runtime_id,
|
||||||
|
worker_id: None,
|
||||||
|
audit_correlation_id: ssh.secret_resource.audit_correlation_id.clone(),
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.map_err(repository_resource_error)?;
|
||||||
|
if response.kind != BackendResourceKind::RepositorySshAccess
|
||||||
|
|| response.content_type != REPOSITORY_SSH_ACCESS_CONTENT_TYPE
|
||||||
|
|| response.resource_id != ssh.secret_resource.resource_id
|
||||||
|
|| response.digest != ssh.secret_resource.digest
|
||||||
|
|| response.bytes.len() as u64 > ssh.secret_resource.max_bytes
|
||||||
|
{
|
||||||
|
return Err(RuntimeError::InvalidRequest(
|
||||||
|
"Backend Repository SSH access resource response was invalid".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let secret = serde_json::from_slice::<RepositorySshAccessSecret>(&response.bytes);
|
||||||
|
response.bytes.fill(0);
|
||||||
|
let mut secret = secret.map_err(|_| {
|
||||||
|
RuntimeError::InvalidRequest(
|
||||||
|
"Backend Repository SSH access resource payload was invalid".to_string(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
ssh.private_key =
|
||||||
|
crate::catalog::SensitiveString::new(std::mem::take(&mut secret.private_key));
|
||||||
|
ssh.known_hosts_entry =
|
||||||
|
crate::catalog::SensitiveString::new(std::mem::take(&mut secret.known_hosts_entry));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn authorize_working_directory_repository_access_from_resource(
|
pub async fn authorize_working_directory_repository_access_from_resource(
|
||||||
&self,
|
&self,
|
||||||
mut request: WorkingDirectoryRepositoryAccessRequest,
|
mut request: WorkingDirectoryRepositoryAccessRequest,
|
||||||
@@ -404,51 +471,7 @@ impl Runtime {
|
|||||||
let ssh = request.materialization.ssh.as_mut().ok_or_else(|| {
|
let ssh = request.materialization.ssh.as_mut().ok_or_else(|| {
|
||||||
RuntimeError::InvalidRequest("Repository SSH access metadata is missing".to_string())
|
RuntimeError::InvalidRequest("Repository SSH access metadata is missing".to_string())
|
||||||
})?;
|
})?;
|
||||||
if ssh.private_key.expose().is_empty() || ssh.known_hosts_entry.expose().is_empty() {
|
self.resolve_repository_access_resource(ssh).await?;
|
||||||
let (client, runtime_id) = {
|
|
||||||
let state = self.lock()?;
|
|
||||||
let client = state.backend_resource_client.clone().ok_or_else(|| {
|
|
||||||
RuntimeError::InvalidRequest(
|
|
||||||
"Backend Repository access resource client is unavailable".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let runtime_id = state.runtime_identity.clone().ok_or_else(|| {
|
|
||||||
RuntimeError::InvalidRequest("Runtime identity is unavailable".to_string())
|
|
||||||
})?;
|
|
||||||
(client, runtime_id)
|
|
||||||
};
|
|
||||||
let mut response = client
|
|
||||||
.0
|
|
||||||
.fetch_resource(BackendResourceFetchRequest {
|
|
||||||
handle: ssh.secret_resource.clone(),
|
|
||||||
runtime_id,
|
|
||||||
worker_id: None,
|
|
||||||
audit_correlation_id: ssh.secret_resource.audit_correlation_id.clone(),
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.map_err(repository_resource_error)?;
|
|
||||||
if response.kind != BackendResourceKind::RepositorySshAccess
|
|
||||||
|| response.content_type != REPOSITORY_SSH_ACCESS_CONTENT_TYPE
|
|
||||||
|| response.resource_id != ssh.secret_resource.resource_id
|
|
||||||
|| response.digest != ssh.secret_resource.digest
|
|
||||||
|| response.bytes.len() as u64 > ssh.secret_resource.max_bytes
|
|
||||||
{
|
|
||||||
return Err(RuntimeError::InvalidRequest(
|
|
||||||
"Backend Repository SSH access resource response was invalid".to_string(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let secret = serde_json::from_slice::<RepositorySshAccessSecret>(&response.bytes);
|
|
||||||
response.bytes.fill(0);
|
|
||||||
let mut secret = secret.map_err(|_| {
|
|
||||||
RuntimeError::InvalidRequest(
|
|
||||||
"Backend Repository SSH access resource payload was invalid".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
ssh.private_key =
|
|
||||||
crate::catalog::SensitiveString::new(std::mem::take(&mut secret.private_key));
|
|
||||||
ssh.known_hosts_entry =
|
|
||||||
crate::catalog::SensitiveString::new(std::mem::take(&mut secret.known_hosts_entry));
|
|
||||||
}
|
|
||||||
self.authorize_working_directory_repository_access(request)
|
self.authorize_working_directory_repository_access(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3335,6 +3358,9 @@ mod tests {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn repository_access_resource_is_fetched_before_provider_authorization() {
|
async fn repository_access_resource_is_fetched_before_provider_authorization() {
|
||||||
let (runtime, backend) = runtime_and_backend();
|
let (runtime, backend) = runtime_and_backend();
|
||||||
|
backend
|
||||||
|
.repository_access_available
|
||||||
|
.store(true, Ordering::SeqCst);
|
||||||
runtime.bind_runtime_identity("runtime-1").unwrap();
|
runtime.bind_runtime_identity("runtime-1").unwrap();
|
||||||
let handle = repository_resource_handle();
|
let handle = repository_resource_handle();
|
||||||
runtime
|
runtime
|
||||||
@@ -3398,6 +3424,88 @@ mod tests {
|
|||||||
assert_eq!(access.known_hosts_entry.expose(), "known-hosts-entry");
|
assert_eq!(access.known_hosts_entry.expose(), "known-hosts-entry");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn working_directory_create_fetches_repository_access_before_provider_call() {
|
||||||
|
let (runtime, backend) = runtime_and_backend();
|
||||||
|
backend
|
||||||
|
.repository_access_available
|
||||||
|
.store(true, Ordering::SeqCst);
|
||||||
|
runtime.bind_runtime_identity("runtime-1").unwrap();
|
||||||
|
let handle = repository_resource_handle();
|
||||||
|
runtime
|
||||||
|
.install_backend_resource_client(Arc::new(TestRepositoryResourceClient {
|
||||||
|
response: Mutex::new(Some(crate::resource::BackendResourceFetchResponse {
|
||||||
|
kind: crate::resource::BackendResourceKind::RepositorySshAccess,
|
||||||
|
resource_id: handle.resource_id.clone(),
|
||||||
|
digest: handle.digest.clone(),
|
||||||
|
content_type: crate::resource::REPOSITORY_SSH_ACCESS_CONTENT_TYPE.to_string(),
|
||||||
|
bytes: serde_json::to_vec(&RepositorySshAccessSecret {
|
||||||
|
private_key: "create-private-key-bytes".to_string(),
|
||||||
|
known_hosts_entry: "create-known-hosts-entry".to_string(),
|
||||||
|
})
|
||||||
|
.unwrap(),
|
||||||
|
audit_correlation_id: handle.audit_correlation_id.clone(),
|
||||||
|
})),
|
||||||
|
}))
|
||||||
|
.unwrap();
|
||||||
|
let request = WorkingDirectoryRequest {
|
||||||
|
repository: WorkingDirectoryRepository {
|
||||||
|
id: "repository-1".to_string(),
|
||||||
|
provider: "git".to_string(),
|
||||||
|
source: workspace_api::RepositorySource {
|
||||||
|
kind: workspace_api::RepositorySourceKind::Ssh,
|
||||||
|
uri: "ssh://git@example.test/repo.git".to_string(),
|
||||||
|
},
|
||||||
|
source_revision: 1,
|
||||||
|
source_fingerprint: "sha256:source".to_string(),
|
||||||
|
selector: None,
|
||||||
|
},
|
||||||
|
materializer: MaterializerKind::RuntimeGitCache,
|
||||||
|
backend_workdir_id: Some("working-directory-1".to_string()),
|
||||||
|
materialization: Some(RepositoryMaterializationContext {
|
||||||
|
workspace_id: "workspace-1".to_string(),
|
||||||
|
runtime_id: "runtime-1".to_string(),
|
||||||
|
operation_id: "operation-create".to_string(),
|
||||||
|
config_revision: 1,
|
||||||
|
config_projection_digest: "sha256:projection".to_string(),
|
||||||
|
cache_generation: 0,
|
||||||
|
ssh: Some(RepositorySshMaterializationAccess {
|
||||||
|
credential_id: "credential-1".to_string(),
|
||||||
|
credential_revision: 1,
|
||||||
|
host_trust_id: "host-trust-1".to_string(),
|
||||||
|
host_trust_revision: 1,
|
||||||
|
access: workspace_api::RepositoryAccessMode::ReadOnly,
|
||||||
|
expires_at_epoch_seconds: u64::MAX,
|
||||||
|
repository_id: "repository-1".to_string(),
|
||||||
|
repository_source_fingerprint: "sha256:source".to_string(),
|
||||||
|
repository_uri: "ssh://git@example.test/repo.git".to_string(),
|
||||||
|
secret_resource: handle,
|
||||||
|
private_key: SensitiveString::default(),
|
||||||
|
known_hosts_entry: SensitiveString::default(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
runtime
|
||||||
|
.create_working_directory_from_resource(request)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
|
||||||
|
let requests = backend.working_directory_requests.lock().unwrap();
|
||||||
|
let access = requests[0]
|
||||||
|
.materialization
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|materialization| materialization.ssh.as_ref())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(access.private_key.expose(), "create-private-key-bytes");
|
||||||
|
assert_eq!(
|
||||||
|
access.known_hosts_entry.expose(),
|
||||||
|
"create-known-hosts-entry"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn scoped_task_request(objective: &str, workspace_id: &str) -> CreateWorkerRequest {
|
fn scoped_task_request(objective: &str, workspace_id: &str) -> CreateWorkerRequest {
|
||||||
let mut request = task_request(objective);
|
let mut request = task_request(objective);
|
||||||
request.workspace_api = Some(WorkspaceApiRef {
|
request.workspace_api = Some(WorkspaceApiRef {
|
||||||
@@ -3480,6 +3588,8 @@ mod tests {
|
|||||||
contexts: Mutex<BTreeMap<WorkerId, WorkerExecutionContext>>,
|
contexts: Mutex<BTreeMap<WorkerId, WorkerExecutionContext>>,
|
||||||
dispatched_inputs: Mutex<Vec<WorkerInput>>,
|
dispatched_inputs: Mutex<Vec<WorkerInput>>,
|
||||||
repository_accesses: Mutex<Vec<WorkingDirectoryRepositoryAccessRequest>>,
|
repository_accesses: Mutex<Vec<WorkingDirectoryRepositoryAccessRequest>>,
|
||||||
|
repository_access_available: AtomicBool,
|
||||||
|
working_directory_requests: Mutex<Vec<WorkingDirectoryRequest>>,
|
||||||
preserve_commit_ack_submission_id: AtomicBool,
|
preserve_commit_ack_submission_id: AtomicBool,
|
||||||
#[cfg(feature = "ws-server")]
|
#[cfg(feature = "ws-server")]
|
||||||
snapshots: Mutex<BTreeMap<WorkerId, protocol::Event>>,
|
snapshots: Mutex<BTreeMap<WorkerId, protocol::Event>>,
|
||||||
@@ -3520,6 +3630,20 @@ mod tests {
|
|||||||
"test-execution-backend"
|
"test-execution-backend"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_working_directory(
|
||||||
|
&self,
|
||||||
|
request: &WorkingDirectoryRequest,
|
||||||
|
) -> Result<CatalogWorkingDirectoryStatus, WorkingDirectoryDiagnostic> {
|
||||||
|
self.working_directory_requests
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push(request.clone());
|
||||||
|
Err(WorkingDirectoryDiagnostic::rejected(
|
||||||
|
"working_directory_unsupported",
|
||||||
|
"Worker execution backend does not support working directory materialization",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn authorize_working_directory_repository_access(
|
fn authorize_working_directory_repository_access(
|
||||||
&self,
|
&self,
|
||||||
request: &WorkingDirectoryRepositoryAccessRequest,
|
request: &WorkingDirectoryRepositoryAccessRequest,
|
||||||
@@ -3528,7 +3652,14 @@ mod tests {
|
|||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.push(request.clone());
|
.push(request.clone());
|
||||||
Ok(())
|
if self.repository_access_available.load(Ordering::SeqCst) {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(WorkingDirectoryDiagnostic::rejected(
|
||||||
|
"working_directory_repository_access_unsupported",
|
||||||
|
"Worker execution backend does not support Repository access authorization",
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult {
|
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult {
|
||||||
|
|||||||
@@ -488,12 +488,9 @@ impl RuntimeGitCacheMaterializer {
|
|||||||
access.stop();
|
access.stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if access.access == workspace_api::RepositoryAccessMode::ReadWrite {
|
binding
|
||||||
binding.command_environment.insert(
|
.command_environment
|
||||||
"SSH_AUTH_SOCK".to_string(),
|
.insert("SSH_AUTH_SOCK".to_string(), "/dev/null".to_string());
|
||||||
command_access.agent.socket.to_string_lossy().to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
binding.command_environment.insert(
|
binding.command_environment.insert(
|
||||||
"GIT_SSH_COMMAND".to_string(),
|
"GIT_SSH_COMMAND".to_string(),
|
||||||
command_access.ssh_command.to_string_lossy().to_string(),
|
command_access.ssh_command.to_string_lossy().to_string(),
|
||||||
@@ -703,14 +700,23 @@ impl RuntimeGitCacheMaterializer {
|
|||||||
let mut request = request.clone();
|
let mut request = request.clone();
|
||||||
if request.repository.provider != "git"
|
if request.repository.provider != "git"
|
||||||
|| request.repository.source.kind != workspace_api::RepositorySourceKind::Ssh
|
|| request.repository.source.kind != workspace_api::RepositorySourceKind::Ssh
|
||||||
|| request
|
|
||||||
.materialization
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|materialization| materialization.ssh.as_ref())
|
|
||||||
.is_some()
|
|
||||||
{
|
{
|
||||||
return Ok(request);
|
return Ok(request);
|
||||||
}
|
}
|
||||||
|
if let Some(access) = request
|
||||||
|
.materialization
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|materialization| materialization.ssh.as_ref())
|
||||||
|
{
|
||||||
|
validate_ssh_materialization_access(access)?;
|
||||||
|
validate_repository_access_binding(
|
||||||
|
access,
|
||||||
|
&request.repository.id,
|
||||||
|
&request.repository.source_fingerprint,
|
||||||
|
Some(request.repository.source.uri.as_str()),
|
||||||
|
)?;
|
||||||
|
return Ok(request);
|
||||||
|
}
|
||||||
let access = self
|
let access = self
|
||||||
.repository_access
|
.repository_access
|
||||||
.lock()
|
.lock()
|
||||||
@@ -1266,29 +1272,30 @@ impl std::fmt::Debug for PendingRepositorySshRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct RepositoryReadOnlySshBroker {
|
struct RepositorySshBroker {
|
||||||
socket: PathBuf,
|
socket: PathBuf,
|
||||||
stopped: Arc<AtomicBool>,
|
stopped: Arc<AtomicBool>,
|
||||||
thread: Mutex<Option<JoinHandle<()>>>,
|
thread: Mutex<Option<JoinHandle<()>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepositoryReadOnlySshBroker {
|
impl RepositorySshBroker {
|
||||||
fn start(
|
fn start(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
agent: Arc<RepositorySshAgent>,
|
agent: Arc<RepositorySshAgent>,
|
||||||
known_hosts: PathBuf,
|
known_hosts: PathBuf,
|
||||||
|
policy: RepositorySshCommandPolicy,
|
||||||
) -> Result<Self, WorkingDirectoryDiagnostic> {
|
) -> Result<Self, WorkingDirectoryDiagnostic> {
|
||||||
let socket = root.join("b.sock");
|
let socket = root.join("b.sock");
|
||||||
let listener = UnixListener::bind(&socket).map_err(|_| {
|
let listener = UnixListener::bind(&socket).map_err(|_| {
|
||||||
WorkingDirectoryDiagnostic::new(
|
WorkingDirectoryDiagnostic::new(
|
||||||
"working_directory_repository_access_setup_failed",
|
"working_directory_repository_access_setup_failed",
|
||||||
"read-only Repository SSH broker could not be prepared",
|
"Repository SSH broker could not be prepared",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
listener.set_nonblocking(true).map_err(|_| {
|
listener.set_nonblocking(true).map_err(|_| {
|
||||||
WorkingDirectoryDiagnostic::new(
|
WorkingDirectoryDiagnostic::new(
|
||||||
"working_directory_repository_access_setup_failed",
|
"working_directory_repository_access_setup_failed",
|
||||||
"read-only Repository SSH broker could not be prepared",
|
"Repository SSH broker could not be prepared",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let stopped = Arc::new(AtomicBool::new(false));
|
let stopped = Arc::new(AtomicBool::new(false));
|
||||||
@@ -1308,11 +1315,13 @@ impl RepositoryReadOnlySshBroker {
|
|||||||
{
|
{
|
||||||
let request_agent = Arc::clone(&agent);
|
let request_agent = Arc::clone(&agent);
|
||||||
let request_known_hosts = known_hosts.clone();
|
let request_known_hosts = known_hosts.clone();
|
||||||
|
let request_policy = policy.clone();
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
run_brokered_read_only_ssh(
|
run_brokered_repository_ssh(
|
||||||
request,
|
request,
|
||||||
&request_agent.socket,
|
&request_agent.socket,
|
||||||
&request_known_hosts,
|
&request_known_hosts,
|
||||||
|
&request_policy,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
pending.remove(&request_id);
|
pending.remove(&request_id);
|
||||||
@@ -1346,7 +1355,7 @@ impl RepositoryReadOnlySshBroker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for RepositoryReadOnlySshBroker {
|
impl Drop for RepositorySshBroker {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.stop();
|
self.stop();
|
||||||
}
|
}
|
||||||
@@ -1401,10 +1410,11 @@ fn receive_repository_ssh_channel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_brokered_read_only_ssh(
|
fn run_brokered_repository_ssh(
|
||||||
mut request: PendingRepositorySshRequest,
|
mut request: PendingRepositorySshRequest,
|
||||||
agent_socket: &Path,
|
agent_socket: &Path,
|
||||||
known_hosts: &Path,
|
known_hosts: &Path,
|
||||||
|
policy: &RepositorySshCommandPolicy,
|
||||||
) {
|
) {
|
||||||
let args = request.args.take().unwrap_or_default();
|
let args = request.args.take().unwrap_or_default();
|
||||||
let mut data = request.data.take().expect("complete broker request data");
|
let mut data = request.data.take().expect("complete broker request data");
|
||||||
@@ -1416,8 +1426,8 @@ fn run_brokered_read_only_ssh(
|
|||||||
.status
|
.status
|
||||||
.take()
|
.take()
|
||||||
.expect("complete broker request status");
|
.expect("complete broker request status");
|
||||||
let status = if !validate_read_only_ssh_args(&args) {
|
let status = if !validate_repository_ssh_args(&args, policy) {
|
||||||
let _ = stderr_stream.write_all(b"read-only Repository SSH operation denied\n");
|
let _ = stderr_stream.write_all(b"Repository SSH operation denied\n");
|
||||||
let _ = stderr_stream.shutdown(Shutdown::Write);
|
let _ = stderr_stream.shutdown(Shutdown::Write);
|
||||||
let _ = data.shutdown(Shutdown::Both);
|
let _ = data.shutdown(Shutdown::Both);
|
||||||
126
|
126
|
||||||
@@ -1480,28 +1490,114 @@ fn run_brokered_read_only_ssh(
|
|||||||
let _ = status_stream.shutdown(Shutdown::Write);
|
let _ = status_stream.shutdown(Shutdown::Write);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_read_only_ssh_args(args: &[String]) -> bool {
|
#[derive(Clone, Debug)]
|
||||||
|
struct RepositorySshCommandPolicy {
|
||||||
|
host: String,
|
||||||
|
username: Option<String>,
|
||||||
|
port: Option<u16>,
|
||||||
|
repository_path: String,
|
||||||
|
access: workspace_api::RepositoryAccessMode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RepositorySshCommandPolicy {
|
||||||
|
fn from_access(
|
||||||
|
access: &RepositorySshMaterializationAccess,
|
||||||
|
) -> Result<Self, WorkingDirectoryDiagnostic> {
|
||||||
|
let uri = url::Url::parse(&access.repository_uri).map_err(|_| {
|
||||||
|
WorkingDirectoryDiagnostic::new(
|
||||||
|
"working_directory_repository_access_binding_mismatch",
|
||||||
|
"Repository SSH access URI is invalid",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if uri.scheme() != "ssh"
|
||||||
|
|| uri.password().is_some()
|
||||||
|
|| uri.query().is_some()
|
||||||
|
|| uri.fragment().is_some()
|
||||||
|
{
|
||||||
|
return Err(WorkingDirectoryDiagnostic::new(
|
||||||
|
"working_directory_repository_access_binding_mismatch",
|
||||||
|
"Repository SSH access URI is not an authorized SSH endpoint",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let host = uri
|
||||||
|
.host_str()
|
||||||
|
.filter(|host| !host.is_empty())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
WorkingDirectoryDiagnostic::new(
|
||||||
|
"working_directory_repository_access_binding_mismatch",
|
||||||
|
"Repository SSH access host is missing",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let username = (!uri.username().is_empty()).then(|| uri.username().to_string());
|
||||||
|
if username
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|username| !is_safe_ssh_destination(username))
|
||||||
|
|| uri.path().is_empty()
|
||||||
|
|| !is_safe_repository_path(uri.path())
|
||||||
|
{
|
||||||
|
return Err(WorkingDirectoryDiagnostic::new(
|
||||||
|
"working_directory_repository_access_binding_mismatch",
|
||||||
|
"Repository SSH access endpoint or path is invalid",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
host: host.to_string(),
|
||||||
|
username,
|
||||||
|
port: uri.port(),
|
||||||
|
repository_path: uri.path().to_string(),
|
||||||
|
access: access.access,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destination_matches(&self, destination: &str, login: Option<&str>) -> bool {
|
||||||
|
let host = if self.host.contains(':') {
|
||||||
|
format!("[{}]", self.host)
|
||||||
|
} else {
|
||||||
|
self.host.clone()
|
||||||
|
};
|
||||||
|
match self.username.as_deref() {
|
||||||
|
Some(username) => {
|
||||||
|
(login.is_none() && destination == format!("{username}@{host}"))
|
||||||
|
|| (login == Some(username) && destination == host)
|
||||||
|
}
|
||||||
|
None => login.is_none() && destination == host,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_repository_ssh_args(args: &[String], policy: &RepositorySshCommandPolicy) -> bool {
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
let mut probe = false;
|
let mut probe = false;
|
||||||
|
let mut port = None;
|
||||||
|
let mut login = None;
|
||||||
let mut positional = Vec::new();
|
let mut positional = Vec::new();
|
||||||
while index < args.len() {
|
while index < args.len() {
|
||||||
match args[index].as_str() {
|
match args[index].as_str() {
|
||||||
"-G" => probe = true,
|
"-G" => probe = true,
|
||||||
"-4" | "-6" | "-v" | "-vv" | "-vvv" => {}
|
"-4" | "-6" | "-v" | "-vv" | "-vvv" => {}
|
||||||
"-p" => {
|
"-p" => {
|
||||||
index += 1;
|
if port.is_some() {
|
||||||
if index >= args.len()
|
|
||||||
|| args[index].is_empty()
|
|
||||||
|| !args[index].bytes().all(|byte| byte.is_ascii_digit())
|
|
||||||
{
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
index += 1;
|
||||||
|
let Some(value) = args.get(index).and_then(|value| value.parse::<u16>().ok())
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
port = Some(value);
|
||||||
}
|
}
|
||||||
"-l" => {
|
"-l" => {
|
||||||
index += 1;
|
if login.is_some() {
|
||||||
if index >= args.len() || !is_safe_ssh_destination(&args[index]) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
index += 1;
|
||||||
|
let Some(value) = args
|
||||||
|
.get(index)
|
||||||
|
.filter(|value| is_safe_ssh_destination(value))
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
login = Some(value.as_str());
|
||||||
}
|
}
|
||||||
"-o" => {
|
"-o" => {
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -1519,12 +1615,16 @@ fn validate_read_only_ssh_args(args: &[String]) -> bool {
|
|||||||
}
|
}
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
if port != policy.port
|
||||||
|
|| positional.is_empty()
|
||||||
|
|| !policy.destination_matches(positional[0], login)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if probe {
|
if probe {
|
||||||
positional.len() == 1 && is_safe_ssh_destination(positional[0])
|
positional.len() == 1
|
||||||
} else {
|
} else {
|
||||||
positional.len() == 2
|
positional.len() == 2 && repository_command_matches(positional[1], policy)
|
||||||
&& is_safe_ssh_destination(positional[0])
|
|
||||||
&& is_safe_read_only_git_command(positional[1])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1548,31 +1648,43 @@ fn is_safe_ssh_destination(value: &str) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_safe_read_only_git_command(command: &str) -> bool {
|
fn is_safe_repository_path(path: &str) -> bool {
|
||||||
let Some(argument) = command
|
!path.is_empty()
|
||||||
.strip_prefix("git-upload-pack ")
|
&& path.bytes().all(|byte| {
|
||||||
.or_else(|| command.strip_prefix("git-upload-archive "))
|
|
||||||
else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
let argument = argument
|
|
||||||
.strip_prefix('\'')
|
|
||||||
.and_then(|argument| argument.strip_suffix('\''))
|
|
||||||
.unwrap_or(argument);
|
|
||||||
!argument.is_empty()
|
|
||||||
&& argument.bytes().all(|byte| {
|
|
||||||
byte.is_ascii_alphanumeric()
|
byte.is_ascii_alphanumeric()
|
||||||
|| matches!(
|
|| matches!(
|
||||||
byte,
|
byte,
|
||||||
b'/' | b'.' | b'-' | b'_' | b'@' | b':' | b'+' | b'~' | b' '
|
b'/' | b'.' | b'-' | b'_' | b'@' | b':' | b'+' | b'~' | b' ' | b'%'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run_repository_read_only_ssh_client(arguments: &[String]) -> Result<i32, String> {
|
fn repository_command_matches(command: &str, policy: &RepositorySshCommandPolicy) -> bool {
|
||||||
|
let (operation, argument) = if let Some(argument) = command.strip_prefix("git-upload-pack ") {
|
||||||
|
("upload-pack", argument)
|
||||||
|
} else if let Some(argument) = command.strip_prefix("git-upload-archive ") {
|
||||||
|
("upload-archive", argument)
|
||||||
|
} else if let Some(argument) = command.strip_prefix("git-receive-pack ") {
|
||||||
|
("receive-pack", argument)
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
if operation == "receive-pack"
|
||||||
|
&& policy.access != workspace_api::RepositoryAccessMode::ReadWrite
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let argument = argument
|
||||||
|
.strip_prefix('\'')
|
||||||
|
.and_then(|argument| argument.strip_suffix('\''))
|
||||||
|
.unwrap_or(argument);
|
||||||
|
is_safe_repository_path(argument) && argument == policy.repository_path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_repository_ssh_client(arguments: &[String]) -> Result<i32, String> {
|
||||||
let (socket, ssh_args) = arguments
|
let (socket, ssh_args) = arguments
|
||||||
.split_first()
|
.split_first()
|
||||||
.ok_or_else(|| "read-only Repository SSH broker socket is missing".to_string())?;
|
.ok_or_else(|| "Repository SSH broker socket is missing".to_string())?;
|
||||||
let request_id = format!(
|
let request_id = format!(
|
||||||
"{}-{}",
|
"{}-{}",
|
||||||
std::process::id(),
|
std::process::id(),
|
||||||
@@ -1600,7 +1712,7 @@ pub fn run_repository_read_only_ssh_client(arguments: &[String]) -> Result<i32,
|
|||||||
)?;
|
)?;
|
||||||
let mut input = data
|
let mut input = data
|
||||||
.try_clone()
|
.try_clone()
|
||||||
.map_err(|_| "read-only Repository SSH broker input failed".to_string())?;
|
.map_err(|_| "Repository SSH broker input failed".to_string())?;
|
||||||
let input_thread = std::thread::spawn(move || {
|
let input_thread = std::thread::spawn(move || {
|
||||||
let _ = std::io::copy(&mut std::io::stdin(), &mut input);
|
let _ = std::io::copy(&mut std::io::stdin(), &mut input);
|
||||||
let _ = input.shutdown(Shutdown::Write);
|
let _ = input.shutdown(Shutdown::Write);
|
||||||
@@ -1609,17 +1721,17 @@ pub fn run_repository_read_only_ssh_client(arguments: &[String]) -> Result<i32,
|
|||||||
let _ = std::io::copy(&mut stderr_stream, &mut std::io::stderr());
|
let _ = std::io::copy(&mut stderr_stream, &mut std::io::stderr());
|
||||||
});
|
});
|
||||||
std::io::copy(&mut data, &mut std::io::stdout())
|
std::io::copy(&mut data, &mut std::io::stdout())
|
||||||
.map_err(|_| "read-only Repository SSH broker output failed".to_string())?;
|
.map_err(|_| "Repository SSH broker output failed".to_string())?;
|
||||||
let _ = input_thread.join();
|
let _ = input_thread.join();
|
||||||
let _ = error_thread.join();
|
let _ = error_thread.join();
|
||||||
let mut status = String::new();
|
let mut status = String::new();
|
||||||
status_stream
|
status_stream
|
||||||
.read_to_string(&mut status)
|
.read_to_string(&mut status)
|
||||||
.map_err(|_| "read-only Repository SSH broker status failed".to_string())?;
|
.map_err(|_| "Repository SSH broker status failed".to_string())?;
|
||||||
status
|
status
|
||||||
.trim()
|
.trim()
|
||||||
.parse::<i32>()
|
.parse::<i32>()
|
||||||
.map_err(|_| "read-only Repository SSH broker status was invalid".to_string())
|
.map_err(|_| "Repository SSH broker status was invalid".to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn connect_repository_ssh_broker_channel(
|
fn connect_repository_ssh_broker_channel(
|
||||||
@@ -1627,12 +1739,12 @@ fn connect_repository_ssh_broker_channel(
|
|||||||
header: &RepositorySshBrokerHeader,
|
header: &RepositorySshBrokerHeader,
|
||||||
) -> Result<UnixStream, String> {
|
) -> Result<UnixStream, String> {
|
||||||
let mut stream = UnixStream::connect(socket)
|
let mut stream = UnixStream::connect(socket)
|
||||||
.map_err(|_| "read-only Repository SSH broker is unavailable".to_string())?;
|
.map_err(|_| "Repository SSH broker is unavailable".to_string())?;
|
||||||
serde_json::to_writer(&mut stream, header)
|
serde_json::to_writer(&mut stream, header)
|
||||||
.map_err(|_| "read-only Repository SSH broker request failed".to_string())?;
|
.map_err(|_| "Repository SSH broker request failed".to_string())?;
|
||||||
stream
|
stream
|
||||||
.write_all(b"\n")
|
.write_all(b"\n")
|
||||||
.map_err(|_| "read-only Repository SSH broker request failed".to_string())?;
|
.map_err(|_| "Repository SSH broker request failed".to_string())?;
|
||||||
Ok(stream)
|
Ok(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1641,7 +1753,7 @@ struct RepositoryCommandAccess {
|
|||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
ssh_command: PathBuf,
|
ssh_command: PathBuf,
|
||||||
agent: Arc<RepositorySshAgent>,
|
agent: Arc<RepositorySshAgent>,
|
||||||
read_only_broker: Option<RepositoryReadOnlySshBroker>,
|
ssh_broker: Option<RepositorySshBroker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RepositoryCommandAccess {
|
impl RepositoryCommandAccess {
|
||||||
@@ -1694,46 +1806,37 @@ impl RepositoryCommandAccess {
|
|||||||
let ssh_command = root.join("ssh-command");
|
let ssh_command = root.join("ssh-command");
|
||||||
write_owner_only(&known_hosts, ssh.known_hosts_entry.expose().as_bytes())?;
|
write_owner_only(&known_hosts, ssh.known_hosts_entry.expose().as_bytes())?;
|
||||||
let agent = Arc::new(RepositorySshAgent::start(runtime_root, operation_id, ssh)?);
|
let agent = Arc::new(RepositorySshAgent::start(runtime_root, operation_id, ssh)?);
|
||||||
let read_only_broker = if ssh.access == workspace_api::RepositoryAccessMode::ReadOnly {
|
let policy = RepositorySshCommandPolicy::from_access(ssh)?;
|
||||||
Some(RepositoryReadOnlySshBroker::start(
|
let ssh_broker = Some(RepositorySshBroker::start(
|
||||||
&root,
|
&root,
|
||||||
Arc::clone(&agent),
|
Arc::clone(&agent),
|
||||||
known_hosts.clone(),
|
known_hosts.clone(),
|
||||||
)?)
|
policy,
|
||||||
} else {
|
)?);
|
||||||
None
|
let broker = ssh_broker.as_ref().expect("Repository SSH broker");
|
||||||
};
|
let executable = std::env::current_exe().map_err(|_| {
|
||||||
let script = if let Some(broker) = read_only_broker.as_ref() {
|
WorkingDirectoryDiagnostic::new(
|
||||||
let executable = std::env::current_exe().map_err(|_| {
|
"working_directory_repository_access_setup_failed",
|
||||||
WorkingDirectoryDiagnostic::new(
|
"Repository SSH broker client could not be resolved",
|
||||||
"working_directory_repository_access_setup_failed",
|
|
||||||
"read-only Repository SSH broker client could not be resolved",
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
format!(
|
|
||||||
"#!/bin/sh\nexec {} __repository-read-only-ssh {} \"$@\"\n",
|
|
||||||
shell_quote_path(&executable)?,
|
|
||||||
shell_quote_path(&broker.socket)?,
|
|
||||||
)
|
)
|
||||||
} else {
|
})?;
|
||||||
format!(
|
let script = format!(
|
||||||
"#!/bin/sh\nexport SSH_AUTH_SOCK={}\nexec ssh -F /dev/null -o BatchMode=yes -o IdentitiesOnly=no -o IdentityFile=/dev/null -o StrictHostKeyChecking=yes -o UserKnownHostsFile={} \"$@\"\n",
|
"#!/bin/sh\nexec {} __repository-ssh {} \"$@\"\n",
|
||||||
shell_quote_path(&agent.socket)?,
|
shell_quote_path(&executable)?,
|
||||||
shell_quote_path(&known_hosts)?,
|
shell_quote_path(&broker.socket)?,
|
||||||
)
|
);
|
||||||
};
|
|
||||||
write_owner_only(&ssh_command, script.as_bytes())?;
|
write_owner_only(&ssh_command, script.as_bytes())?;
|
||||||
set_file_owner_executable(&ssh_command)?;
|
set_file_owner_executable(&ssh_command)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
root,
|
root,
|
||||||
ssh_command,
|
ssh_command,
|
||||||
agent,
|
agent,
|
||||||
read_only_broker,
|
ssh_broker,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn stop(&self) {
|
fn stop(&self) {
|
||||||
if let Some(broker) = self.read_only_broker.as_ref() {
|
if let Some(broker) = self.ssh_broker.as_ref() {
|
||||||
broker.stop();
|
broker.stop();
|
||||||
}
|
}
|
||||||
self.agent.stop();
|
self.agent.stop();
|
||||||
@@ -2727,17 +2830,27 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let binding = materializer.bind_working_directory(&id, None).unwrap();
|
let binding = materializer.bind_working_directory(&id, None).unwrap();
|
||||||
let environment = binding.command_environment();
|
let environment = binding.command_environment();
|
||||||
let socket = PathBuf::from(environment["SSH_AUTH_SOCK"].clone());
|
assert_eq!(environment["SSH_AUTH_SOCK"], "/dev/null");
|
||||||
let ssh_command = PathBuf::from(environment["GIT_SSH_COMMAND"].clone());
|
let ssh_command = PathBuf::from(environment["GIT_SSH_COMMAND"].clone());
|
||||||
assert!(socket.exists());
|
|
||||||
assert!(ssh_command.exists());
|
assert!(ssh_command.exists());
|
||||||
let ssh_policy = fs::read_to_string(&ssh_command).unwrap();
|
let ssh_policy = fs::read_to_string(&ssh_command).unwrap();
|
||||||
assert!(ssh_policy.contains("StrictHostKeyChecking=yes"));
|
assert!(ssh_policy.contains("__repository-ssh"));
|
||||||
assert!(ssh_policy.contains("UserKnownHostsFile="));
|
assert!(!ssh_policy.contains("SSH_AUTH_SOCK"));
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_dir(runtime_root.path().join(".repository-agents"))
|
||||||
|
.map(|entries| entries.count())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
1
|
||||||
|
);
|
||||||
assert_eq!(environment["YOI_REPOSITORY_ACCESS"], "read_write");
|
assert_eq!(environment["YOI_REPOSITORY_ACCESS"], "read_write");
|
||||||
drop(binding);
|
drop(binding);
|
||||||
assert!(!socket.exists());
|
|
||||||
assert!(!ssh_command.exists());
|
assert!(!ssh_command.exists());
|
||||||
|
assert_eq!(
|
||||||
|
fs::read_dir(runtime_root.path().join(".repository-agents"))
|
||||||
|
.map(|entries| entries.count())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
0
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
materializer
|
materializer
|
||||||
.bind_working_directory(&id, None)
|
.bind_working_directory(&id, None)
|
||||||
@@ -2762,17 +2875,22 @@ mod tests {
|
|||||||
);
|
);
|
||||||
let rebound_environment = rebound.command_environment();
|
let rebound_environment = rebound.command_environment();
|
||||||
assert_eq!(rebound_environment["YOI_REPOSITORY_ACCESS"], "read_only");
|
assert_eq!(rebound_environment["YOI_REPOSITORY_ACCESS"], "read_only");
|
||||||
assert!(!rebound_environment.contains_key("SSH_AUTH_SOCK"));
|
assert_eq!(rebound_environment["SSH_AUTH_SOCK"], "/dev/null");
|
||||||
let read_only_ssh = &rebound_environment["GIT_SSH_COMMAND"];
|
let read_only_ssh = &rebound_environment["GIT_SSH_COMMAND"];
|
||||||
let read_only_policy = fs::read_to_string(read_only_ssh).unwrap();
|
let read_only_policy = fs::read_to_string(read_only_ssh).unwrap();
|
||||||
assert!(read_only_policy.contains("__repository-read-only-ssh"));
|
assert!(read_only_policy.contains("__repository-ssh"));
|
||||||
assert!(!read_only_policy.contains("SSH_AUTH_SOCK"));
|
assert!(!read_only_policy.contains("SSH_AUTH_SOCK"));
|
||||||
assert!(!read_only_policy.contains(".repository-agents"));
|
assert!(!read_only_policy.contains(".repository-agents"));
|
||||||
assert!(!read_only_policy.contains("known_hosts"));
|
assert!(!read_only_policy.contains("known_hosts"));
|
||||||
assert!(!validate_read_only_ssh_args(&[
|
let read_only_command_policy =
|
||||||
"example.test".to_string(),
|
RepositorySshCommandPolicy::from_access(rotated.ssh.as_ref().unwrap()).unwrap();
|
||||||
"git-receive-pack 'repo.git'".to_string(),
|
assert!(!validate_repository_ssh_args(
|
||||||
]));
|
&[
|
||||||
|
"git@example.test".to_string(),
|
||||||
|
"git-receive-pack '/repo.git'".to_string(),
|
||||||
|
],
|
||||||
|
&read_only_command_policy,
|
||||||
|
));
|
||||||
let broker_socket = fs::read_dir(runtime_root.path().join(REPOSITORY_ACCESS_DIR))
|
let broker_socket = fs::read_dir(runtime_root.path().join(REPOSITORY_ACCESS_DIR))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
@@ -2780,35 +2898,77 @@ mod tests {
|
|||||||
.find(|path| path.exists())
|
.find(|path| path.exists())
|
||||||
.expect("read-only broker socket");
|
.expect("read-only broker socket");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
run_repository_read_only_ssh_client(&[
|
run_repository_ssh_client(&[
|
||||||
broker_socket.to_string_lossy().to_string(),
|
broker_socket.to_string_lossy().to_string(),
|
||||||
"-G".to_string(),
|
"-G".to_string(),
|
||||||
"example.test".to_string(),
|
"git@example.test".to_string(),
|
||||||
])
|
])
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
run_repository_read_only_ssh_client(&[
|
run_repository_ssh_client(&[
|
||||||
broker_socket.to_string_lossy().to_string(),
|
broker_socket.to_string_lossy().to_string(),
|
||||||
"example.test".to_string(),
|
"git@example.test".to_string(),
|
||||||
"git-receive-pack 'repo.git'".to_string(),
|
"git-receive-pack '/repo.git'".to_string(),
|
||||||
])
|
])
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
126
|
126
|
||||||
);
|
);
|
||||||
assert!(!validate_read_only_ssh_args(&[
|
assert!(!validate_repository_ssh_args(
|
||||||
"-o".to_string(),
|
&[
|
||||||
"ProxyCommand=sh -c exploit".to_string(),
|
"-o".to_string(),
|
||||||
"example.test".to_string(),
|
"ProxyCommand=sh -c exploit".to_string(),
|
||||||
"git-upload-pack 'repo.git'".to_string(),
|
"git@example.test".to_string(),
|
||||||
]));
|
"git-upload-pack '/repo.git'".to_string(),
|
||||||
assert!(validate_read_only_ssh_args(&[
|
],
|
||||||
"-o".to_string(),
|
&read_only_command_policy,
|
||||||
"SendEnv=GIT_PROTOCOL".to_string(),
|
));
|
||||||
"example.test".to_string(),
|
assert!(validate_repository_ssh_args(
|
||||||
"git-upload-pack 'repo.git'".to_string(),
|
&[
|
||||||
]));
|
"-o".to_string(),
|
||||||
|
"SendEnv=GIT_PROTOCOL".to_string(),
|
||||||
|
"git@example.test".to_string(),
|
||||||
|
"git-upload-pack '/repo.git'".to_string(),
|
||||||
|
],
|
||||||
|
&read_only_command_policy,
|
||||||
|
));
|
||||||
|
assert!(!validate_repository_ssh_args(
|
||||||
|
&[
|
||||||
|
"git@other.test".to_string(),
|
||||||
|
"git-upload-pack '/repo.git'".to_string(),
|
||||||
|
],
|
||||||
|
&read_only_command_policy,
|
||||||
|
));
|
||||||
|
let mut port_bound_access = rotated.ssh.as_ref().unwrap().clone();
|
||||||
|
port_bound_access.repository_uri = "ssh://git@example.test:2222/repo.git".to_string();
|
||||||
|
let port_bound_policy =
|
||||||
|
RepositorySshCommandPolicy::from_access(&port_bound_access).unwrap();
|
||||||
|
assert!(validate_repository_ssh_args(
|
||||||
|
&[
|
||||||
|
"-p".to_string(),
|
||||||
|
"2222".to_string(),
|
||||||
|
"git@example.test".to_string(),
|
||||||
|
"git-upload-pack '/repo.git'".to_string(),
|
||||||
|
],
|
||||||
|
&port_bound_policy,
|
||||||
|
));
|
||||||
|
assert!(!validate_repository_ssh_args(
|
||||||
|
&[
|
||||||
|
"-p".to_string(),
|
||||||
|
"22".to_string(),
|
||||||
|
"git@example.test".to_string(),
|
||||||
|
"git-upload-pack '/repo.git'".to_string(),
|
||||||
|
],
|
||||||
|
&port_bound_policy,
|
||||||
|
));
|
||||||
|
assert!(!validate_repository_ssh_args(
|
||||||
|
&[
|
||||||
|
"git@example.test".to_string(),
|
||||||
|
"git-upload-pack '/other.git'".to_string(),
|
||||||
|
],
|
||||||
|
&read_only_command_policy,
|
||||||
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
git_stdout(
|
git_stdout(
|
||||||
rebound.root(),
|
rebound.root(),
|
||||||
@@ -2824,6 +2984,8 @@ mod tests {
|
|||||||
read_write.operation_id = "operation-agent-read-write".to_string();
|
read_write.operation_id = "operation-agent-read-write".to_string();
|
||||||
read_write.ssh.as_mut().unwrap().credential_revision = 3;
|
read_write.ssh.as_mut().unwrap().credential_revision = 3;
|
||||||
read_write.ssh.as_mut().unwrap().access = workspace_api::RepositoryAccessMode::ReadWrite;
|
read_write.ssh.as_mut().unwrap().access = workspace_api::RepositoryAccessMode::ReadWrite;
|
||||||
|
let read_write_command_policy =
|
||||||
|
RepositorySshCommandPolicy::from_access(read_write.ssh.as_ref().unwrap()).unwrap();
|
||||||
materializer
|
materializer
|
||||||
.authorize_repository_access(&WorkingDirectoryRepositoryAccessRequest {
|
.authorize_repository_access(&WorkingDirectoryRepositoryAccessRequest {
|
||||||
working_directory_id: id.clone(),
|
working_directory_id: id.clone(),
|
||||||
@@ -2835,7 +2997,21 @@ mod tests {
|
|||||||
rebound.command_environment()["YOI_REPOSITORY_ACCESS"],
|
rebound.command_environment()["YOI_REPOSITORY_ACCESS"],
|
||||||
"read_write"
|
"read_write"
|
||||||
);
|
);
|
||||||
assert!(rebound.command_environment().contains_key("SSH_AUTH_SOCK"));
|
assert_eq!(rebound.command_environment()["SSH_AUTH_SOCK"], "/dev/null");
|
||||||
|
assert!(validate_repository_ssh_args(
|
||||||
|
&[
|
||||||
|
"git@example.test".to_string(),
|
||||||
|
"git-receive-pack '/repo.git'".to_string(),
|
||||||
|
],
|
||||||
|
&read_write_command_policy,
|
||||||
|
));
|
||||||
|
assert!(!validate_repository_ssh_args(
|
||||||
|
&[
|
||||||
|
"git@example.test".to_string(),
|
||||||
|
"git-receive-pack '/other.git'".to_string(),
|
||||||
|
],
|
||||||
|
&read_write_command_policy,
|
||||||
|
));
|
||||||
assert!(
|
assert!(
|
||||||
git_stdout(
|
git_stdout(
|
||||||
rebound.root(),
|
rebound.root(),
|
||||||
|
|||||||
Reference in New Issue
Block a user