fix: bind repository SSH secrets to one-shot resources

This commit is contained in:
2026-08-26 18:28:22 +09:00
parent b644971d45
commit 4b132a21e9
9 changed files with 699 additions and 61 deletions
+12
View File
@@ -116,6 +116,12 @@ impl Drop for SensitiveString {
}
}
impl Default for SensitiveString {
fn default() -> Self {
Self(String::new())
}
}
impl std::fmt::Debug for SensitiveString {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("[REDACTED]")
@@ -130,7 +136,13 @@ pub struct RepositorySshMaterializationAccess {
pub host_trust_revision: u64,
pub access: workspace_api::RepositoryAccessMode,
pub expires_at_epoch_seconds: u64,
pub repository_id: String,
pub repository_source_fingerprint: String,
pub repository_uri: String,
pub secret_resource: crate::resource::BackendResourceHandle,
#[serde(skip, default)]
pub private_key: SensitiveString,
#[serde(skip, default)]
pub known_hosts_entry: SensitiveString,
}
+2 -1
View File
@@ -538,7 +538,8 @@ async fn authorize_working_directory_repository_access(
}
state
.runtime
.authorize_working_directory_repository_access(request)
.authorize_working_directory_repository_access_from_resource(request)
.await
.map_err(RuntimeHttpRestError::runtime)?;
Ok(Json(RuntimeHttpRepositoryAccessResponse {
authorized: true,
+21 -8
View File
@@ -182,6 +182,9 @@ fn build_runtime(config: &ProcessConfig) -> Result<Runtime, ProcessError> {
factory = factory.with_remote_worker_mutation_identity(identity);
}
}
let mut backend_resource_client: Option<
Arc<dyn worker_runtime::resource::BackendResourceClient>,
> = None;
if let Some(endpoint) = config.backend_resource_endpoint.clone() {
let identity = runtime_auth.identity.as_ref().ok_or_else(|| {
ProcessError::Auth(
@@ -194,13 +197,15 @@ fn build_runtime(config: &ProcessConfig) -> Result<Runtime, ProcessError> {
.to_owned(),
));
};
factory = factory.with_resource_client(Arc::new(
let client = Arc::new(
worker_runtime::resource::HttpBackendResourceClient::new(
endpoint,
config.backend_resource_token.clone(),
)
.with_runtime_request_source(identity, trusted_server.server_id.clone()),
));
);
factory = factory.with_resource_client(client.clone());
backend_resource_client = Some(client);
}
let backend = Arc::new(
WorkerRuntimeExecutionBackend::new(factory)
@@ -210,10 +215,10 @@ fn build_runtime(config: &ProcessConfig) -> Result<Runtime, ProcessError> {
)),
);
match &config.http.store {
let runtime = match &config.http.store {
RuntimeHttpStoreSelection::Memory => {
Runtime::with_execution_backend(runtime_options_from_http(&config.http), backend)
.map_err(ProcessError::Runtime)
.map_err(ProcessError::Runtime)?
}
RuntimeHttpStoreSelection::Fs { root } => {
let mut options = FsRuntimeStoreOptions::new(root.clone()).with_runtime_id(
@@ -226,12 +231,20 @@ fn build_runtime(config: &ProcessConfig) -> Result<Runtime, ProcessError> {
);
options.display_name = config.http.display_name.clone();
Runtime::with_fs_store_and_execution_backend(options, backend)
.map_err(ProcessError::Runtime)
.map_err(ProcessError::Runtime)?
}
_ => Err(ProcessError::usage(
"unsupported Runtime catalog store selection".to_string(),
)),
_ => {
return Err(ProcessError::usage(
"unsupported Runtime catalog store selection".to_string(),
));
}
};
if let Some(client) = backend_resource_client {
runtime
.install_backend_resource_client(client)
.map_err(ProcessError::Runtime)?;
}
Ok(runtime)
}
fn runtime_options_from_http(config: &RuntimeHttpServerConfig) -> RuntimeOptions {
+56 -5
View File
@@ -11,18 +11,46 @@ use std::sync::Mutex;
pub const PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE: &str =
"application/vnd.yoi.profile-source-archive+tar";
pub const REPOSITORY_SSH_ACCESS_CONTENT_TYPE: &str =
"application/vnd.yoi.repository-ssh-access+json";
pub const DEFAULT_PROFILE_SOURCE_ARCHIVE_MAX_BYTES: u64 = 2 * 1024 * 1024;
pub const DEFAULT_REPOSITORY_SSH_ACCESS_MAX_BYTES: u64 = 64 * 1024;
#[derive(Clone, Serialize, Deserialize)]
pub struct RepositorySshAccessSecret {
pub private_key: String,
pub known_hosts_entry: String,
}
impl Drop for RepositorySshAccessSecret {
fn drop(&mut self) {
zeroize::Zeroize::zeroize(&mut self.private_key);
zeroize::Zeroize::zeroize(&mut self.known_hosts_entry);
}
}
impl std::fmt::Debug for RepositorySshAccessSecret {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("RepositorySshAccessSecret")
.field("private_key", &"[REDACTED]")
.field("known_hosts_entry", &"[REDACTED]")
.finish()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackendResourceKind {
ProfileSourceArchive,
RepositorySshAccess,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum BackendResourceOperation {
FetchArchive,
FetchOnce,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -66,7 +94,7 @@ pub struct BackendResourceFetchRequest {
pub audit_correlation_id: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BackendResourceFetchResponse {
pub kind: BackendResourceKind,
pub resource_id: String,
@@ -76,6 +104,29 @@ pub struct BackendResourceFetchResponse {
pub audit_correlation_id: String,
}
impl std::fmt::Debug for BackendResourceFetchResponse {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("BackendResourceFetchResponse")
.field("kind", &self.kind)
.field("resource_id", &self.resource_id)
.field("digest", &self.digest)
.field("content_type", &self.content_type)
.field(
"bytes",
&format_args!("[REDACTED; {} bytes]", self.bytes.len()),
)
.field("audit_correlation_id", &self.audit_correlation_id)
.finish()
}
}
impl Drop for BackendResourceFetchResponse {
fn drop(&mut self) {
self.bytes.fill(0);
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
#[serde(tag = "code", rename_all = "snake_case")]
pub enum BackendResourceError {
@@ -248,7 +299,7 @@ pub fn build_profile_source_archive_fetch_request(
pub fn profile_source_archive_from_response(
handle: &BackendResourceHandle,
response: BackendResourceFetchResponse,
mut response: BackendResourceFetchResponse,
) -> Result<ProfileSourceArchive, BackendResourceError> {
if handle.kind != BackendResourceKind::ProfileSourceArchive
|| response.kind != BackendResourceKind::ProfileSourceArchive
@@ -263,7 +314,7 @@ pub fn profile_source_archive_from_response(
if response.content_type != handle.content_type {
return Err(BackendResourceError::ContentTypeMismatch {
expected: handle.content_type.clone(),
actual: response.content_type,
actual: response.content_type.clone(),
});
}
let actual_bytes = response.bytes.len() as u64;
@@ -278,7 +329,7 @@ pub fn profile_source_archive_from_response(
return Err(BackendResourceError::DigestMismatch {
expected: handle.digest.clone(),
actual: if response.digest != handle.digest {
response.digest
response.digest.clone()
} else {
actual_digest
},
@@ -296,7 +347,7 @@ pub fn profile_source_archive_from_response(
}
})?,
},
content: response.bytes,
content: std::mem::take(&mut response.bytes),
})
}
+221
View File
@@ -26,6 +26,10 @@ use crate::management::{
};
#[cfg(feature = "ws-server")]
use crate::observation::{WorkerObservationCursor, WorkerObservationEvent};
use crate::resource::{
BackendResourceClient, BackendResourceError, BackendResourceFetchRequest, BackendResourceKind,
REPOSITORY_SSH_ACCESS_CONTENT_TYPE, RepositorySshAccessSecret,
};
#[cfg(feature = "fs-store")]
use crate::retention::{
FsWorkerRetentionProvider, WorkerRetentionExecutionRequest, WorkerRetentionExecutionResult,
@@ -172,6 +176,14 @@ impl Runtime {
Ok(runtime)
}
pub fn install_backend_resource_client(
&self,
client: Arc<dyn BackendResourceClient>,
) -> Result<(), RuntimeError> {
self.lock()?.backend_resource_client = Some(BackendResourceClientRef(client));
Ok(())
}
/// Create or restore a filesystem-backed Runtime.
///
/// The store is scoped by `options.root`; if the directory already exists,
@@ -385,6 +397,61 @@ impl Runtime {
.map_err(RuntimeError::from)
}
pub async fn authorize_working_directory_repository_access_from_resource(
&self,
mut request: WorkingDirectoryRepositoryAccessRequest,
) -> Result<(), RuntimeError> {
let ssh = request.materialization.ssh.as_mut().ok_or_else(|| {
RuntimeError::InvalidRequest("Repository SSH access metadata is missing".to_string())
})?;
if ssh.private_key.expose().is_empty() || ssh.known_hosts_entry.expose().is_empty() {
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)
}
/// List Runtime-owned working directories through the attached execution backend.
pub fn list_working_directories(
&self,
@@ -1862,6 +1929,15 @@ struct SubscriptionSink {
lagged: Arc<AtomicBool>,
}
#[derive(Clone)]
struct BackendResourceClientRef(Arc<dyn BackendResourceClient>);
impl std::fmt::Debug for BackendResourceClientRef {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str("BackendResourceClientRef(..)")
}
}
#[derive(Debug)]
struct RuntimeState {
display_name: Option<String>,
@@ -1873,6 +1949,7 @@ struct RuntimeState {
persistence: RuntimePersistence,
status: RuntimeStatus,
execution_backend: Option<WorkerExecutionBackendRef>,
backend_resource_client: Option<BackendResourceClientRef>,
#[cfg(feature = "fs-store")]
next_diagnostic_id: u64,
workers: BTreeMap<WorkerId, WorkerRecord>,
@@ -1900,6 +1977,7 @@ impl RuntimeState {
persistence: RuntimePersistence::Memory,
status: RuntimeStatus::Running,
execution_backend: None,
backend_resource_client: None,
#[cfg(feature = "fs-store")]
next_diagnostic_id: 1,
workers: BTreeMap::new(),
@@ -1928,6 +2006,7 @@ impl RuntimeState {
persistence: RuntimePersistence::Fs(store),
status: RuntimeStatus::Running,
execution_backend: None,
backend_resource_client: None,
#[cfg(feature = "fs-store")]
next_diagnostic_id: 1,
workers: BTreeMap::new(),
@@ -1979,6 +2058,7 @@ impl RuntimeState {
persistence: RuntimePersistence::Fs(store),
status: persisted.status,
execution_backend: None,
backend_resource_client: None,
next_diagnostic_id,
workers,
config_bundles: BTreeMap::new(),
@@ -2734,6 +2814,23 @@ fn worker_status_from_run_state(run_state: WorkerExecutionRunState) -> WorkerSta
}
}
fn repository_resource_error(error: BackendResourceError) -> RuntimeError {
let category = match error {
BackendResourceError::Expired => "expired",
BackendResourceError::Unauthorized { .. } => "unauthorized",
BackendResourceError::UnsupportedKind => "unsupported_kind",
BackendResourceError::MissingResource => "missing_resource",
BackendResourceError::Oversized { .. } => "oversized",
BackendResourceError::DigestMismatch { .. } => "digest_mismatch",
BackendResourceError::ContentTypeMismatch { .. } => "content_type_mismatch",
BackendResourceError::InvalidResponse { .. } => "invalid_response",
BackendResourceError::Transport { .. } => "transport",
};
RuntimeError::InvalidRequest(format!(
"Backend Repository SSH access resource fetch failed: {category}"
))
}
fn durable_create_worker_request(request: &CreateWorkerRequest) -> CreateWorkerRequest {
let mut durable = request.clone();
if let Some(working_directory) = durable.working_directory_request.as_mut()
@@ -2926,6 +3023,8 @@ mod tests {
WorkerExecutionBackend, WorkerExecutionContext, WorkerExecutionHandle,
WorkerExecutionRestoreRequest, WorkerExecutionRunState,
};
use crate::working_directory::WorkingDirectoryDiagnostic;
use async_trait::async_trait;
use std::collections::BTreeMap;
#[cfg(feature = "fs-store")]
use std::sync::atomic::{AtomicU64, Ordering};
@@ -3178,6 +3277,10 @@ mod tests {
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: repository_resource_handle(),
private_key: SensitiveString::new("private-key-bytes"),
known_hosts_entry: SensitiveString::new("known-hosts-entry"),
}),
@@ -3207,6 +3310,94 @@ mod tests {
assert!(!serialized.contains("known-hosts-entry"));
}
fn repository_resource_handle() -> crate::resource::BackendResourceHandle {
crate::resource::BackendResourceHandle {
kind: crate::resource::BackendResourceKind::RepositorySshAccess,
workspace_id: "workspace-1".to_string(),
scope_id: Some("repository-ssh-access".to_string()),
runtime_id: Some("runtime-1".to_string()),
worker_id: None,
resource_id: "repository-access-1".to_string(),
digest: "opaque:repository-access-1".to_string(),
operation: crate::resource::BackendResourceOperation::FetchOnce,
expires_at_unix_seconds: i64::MAX,
nonce: "repository-access-1".to_string(),
revision: "1".to_string(),
generation: None,
max_bytes: crate::resource::DEFAULT_REPOSITORY_SSH_ACCESS_MAX_BYTES,
content_type: crate::resource::REPOSITORY_SSH_ACCESS_CONTENT_TYPE.to_string(),
redaction: crate::resource::ResourceRedactionPolicy::RuntimeInternalOnly,
audit_correlation_id: "repository-access-1".to_string(),
profile_source_graph: None,
}
}
#[tokio::test]
async fn repository_access_resource_is_fetched_before_provider_authorization() {
let (runtime, backend) = runtime_and_backend();
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: "private-key-bytes".to_string(),
known_hosts_entry: "known-hosts-entry".to_string(),
})
.unwrap(),
audit_correlation_id: handle.audit_correlation_id.clone(),
})),
}))
.unwrap();
let request = WorkingDirectoryRepositoryAccessRequest {
working_directory_id: "working-directory-1".to_string(),
materialization: RepositoryMaterializationContext {
workspace_id: "workspace-1".to_string(),
runtime_id: "runtime-1".to_string(),
operation_id: "operation-1".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(),
}),
},
};
let replay = request.clone();
runtime
.authorize_working_directory_repository_access_from_resource(request)
.await
.unwrap();
assert!(
runtime
.authorize_working_directory_repository_access_from_resource(replay)
.await
.is_err()
);
let accesses = backend.repository_accesses.lock().unwrap();
assert_eq!(accesses.len(), 1);
let access = accesses[0].materialization.ssh.as_ref().unwrap();
assert_eq!(access.private_key.expose(), "private-key-bytes");
assert_eq!(access.known_hosts_entry.expose(), "known-hosts-entry");
}
fn scoped_task_request(objective: &str, workspace_id: &str) -> CreateWorkerRequest {
let mut request = task_request(objective);
request.workspace_api = Some(WorkspaceApiRef {
@@ -3288,6 +3479,7 @@ mod tests {
config_bundles: Mutex<Vec<Option<ConfigBundle>>>,
contexts: Mutex<BTreeMap<WorkerId, WorkerExecutionContext>>,
dispatched_inputs: Mutex<Vec<WorkerInput>>,
repository_accesses: Mutex<Vec<WorkingDirectoryRepositoryAccessRequest>>,
preserve_commit_ack_submission_id: AtomicBool,
#[cfg(feature = "ws-server")]
snapshots: Mutex<BTreeMap<WorkerId, protocol::Event>>,
@@ -3328,6 +3520,17 @@ mod tests {
"test-execution-backend"
}
fn authorize_working_directory_repository_access(
&self,
request: &WorkingDirectoryRepositoryAccessRequest,
) -> Result<(), WorkingDirectoryDiagnostic> {
self.repository_accesses
.lock()
.unwrap()
.push(request.clone());
Ok(())
}
fn spawn_worker(&self, request: WorkerExecutionSpawnRequest) -> WorkerExecutionSpawnResult {
self.run_generations
.lock()
@@ -3435,6 +3638,24 @@ mod tests {
}
}
struct TestRepositoryResourceClient {
response: Mutex<Option<crate::resource::BackendResourceFetchResponse>>,
}
#[async_trait]
impl BackendResourceClient for TestRepositoryResourceClient {
async fn fetch_resource(
&self,
_request: BackendResourceFetchRequest,
) -> Result<crate::resource::BackendResourceFetchResponse, BackendResourceError> {
self.response
.lock()
.unwrap()
.take()
.ok_or(BackendResourceError::MissingResource)
}
}
fn runtime_with_backend() -> Runtime {
let runtime = Runtime::with_execution_backend(
RuntimeOptions::default(),
@@ -729,6 +729,12 @@ impl RuntimeGitCacheMaterializer {
)
})?;
validate_ssh_materialization_access(&access)?;
validate_repository_access_binding(
&access,
&request.repository.id,
&request.repository.source_fingerprint,
Some(request.repository.source.uri.as_str()),
)?;
request
.materialization
.as_mut()
@@ -912,6 +918,17 @@ impl WorkingDirectoryMaterializer for RuntimeGitCacheMaterializer {
"Workdir is not backed by an SSH Repository",
));
}
validate_repository_access_binding(
ssh,
&binding.working_directory.evidence.repository_id,
binding
.working_directory
.evidence
.repository_source_fingerprint
.as_deref()
.unwrap_or_default(),
None,
)?;
binding.working_directory.evidence.operation_id =
Some(request.materialization.operation_id.clone());
binding.working_directory.evidence.credential_revision = Some(ssh.credential_revision);
@@ -1730,6 +1747,24 @@ impl Drop for RepositoryCommandAccess {
}
}
fn validate_repository_access_binding(
access: &RepositorySshMaterializationAccess,
repository_id: &str,
repository_source_fingerprint: &str,
repository_uri: Option<&str>,
) -> Result<(), WorkingDirectoryDiagnostic> {
if access.repository_id != repository_id
|| access.repository_source_fingerprint != repository_source_fingerprint
|| repository_uri.is_some_and(|repository_uri| access.repository_uri != repository_uri)
{
return Err(WorkingDirectoryDiagnostic::new(
"working_directory_repository_access_binding_mismatch",
"Repository SSH access authority does not match the requested Repository source",
));
}
Ok(())
}
fn validate_ssh_materialization_access(
access: &RepositorySshMaterializationAccess,
) -> Result<(), WorkingDirectoryDiagnostic> {
@@ -2286,6 +2321,28 @@ mod tests {
use crate::catalog::{RepositorySelector, WorkingDirectoryRepository};
use crate::identity::{WorkerId, WorkerRef};
fn repository_resource_handle() -> crate::resource::BackendResourceHandle {
crate::resource::BackendResourceHandle {
kind: crate::resource::BackendResourceKind::RepositorySshAccess,
workspace_id: "workspace-1".to_string(),
scope_id: Some("repository-ssh-access".to_string()),
runtime_id: Some("runtime-1".to_string()),
worker_id: None,
resource_id: "repository-access-1".to_string(),
digest: "opaque:repository-access-1".to_string(),
operation: crate::resource::BackendResourceOperation::FetchOnce,
expires_at_unix_seconds: i64::MAX,
nonce: "repository-access-1".to_string(),
revision: "1".to_string(),
generation: None,
max_bytes: crate::resource::DEFAULT_REPOSITORY_SSH_ACCESS_MAX_BYTES,
content_type: crate::resource::REPOSITORY_SSH_ACCESS_CONTENT_TYPE.to_string(),
redaction: crate::resource::ResourceRedactionPolicy::RuntimeInternalOnly,
audit_correlation_id: "repository-access-1".to_string(),
profile_source_graph: None,
}
}
fn git(path: &Path, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
@@ -2523,6 +2580,10 @@ mod tests {
host_trust_revision: 4,
access: workspace_api::RepositoryAccessMode::ReadOnly,
expires_at_epoch_seconds: u64::MAX,
repository_id: "repo-main".to_string(),
repository_source_fingerprint: "sha256:test".to_string(),
repository_uri: "ssh://git@example.test/repo.git".to_string(),
secret_resource: repository_resource_handle(),
private_key: crate::catalog::SensitiveString::new("PRIVATE KEY secret bytes"),
known_hosts_entry: crate::catalog::SensitiveString::new("host key secret bytes"),
};
@@ -2560,6 +2621,10 @@ mod tests {
host_trust_revision: 1,
access: workspace_api::RepositoryAccessMode::ReadWrite,
expires_at_epoch_seconds: u64::MAX,
repository_id: "repo-main".to_string(),
repository_source_fingerprint: "sha256:test".to_string(),
repository_uri: "ssh://git@example.test/repo.git".to_string(),
secret_resource: repository_resource_handle(),
private_key: crate::catalog::SensitiveString::new(
fs::read_to_string(&key_path).unwrap(),
),
@@ -2609,6 +2674,31 @@ mod tests {
.ssh
.is_some()
);
let mut mismatched_source_request = authorized_ssh_request.clone();
mismatched_source_request.repository.source.uri =
"ssh://git@example.test/other.git".to_string();
assert_eq!(
materializer
.request_with_authorized_repository_access(
&working_directory_id,
&mismatched_source_request,
)
.unwrap_err()
.code,
"working_directory_repository_access_binding_mismatch"
);
let mut mismatched_repository_request = authorized_ssh_request.clone();
mismatched_repository_request.repository.id = "repo-other".to_string();
assert_eq!(
materializer
.request_with_authorized_repository_access(
&working_directory_id,
&mismatched_repository_request,
)
.unwrap_err()
.code,
"working_directory_repository_access_binding_mismatch"
);
let created = materializer.create(&request).unwrap();
let id = created.working_directory.id;
assert_eq!(id, working_directory_id);
@@ -2794,6 +2884,10 @@ mod tests {
host_trust_revision: 1,
access: workspace_api::RepositoryAccessMode::ReadOnly,
expires_at_epoch_seconds: u64::MAX,
repository_id: "repo-main".to_string(),
repository_source_fingerprint: "sha256:test".to_string(),
repository_uri: "ssh://git@example.test/repo.git".to_string(),
secret_resource: repository_resource_handle(),
private_key: crate::catalog::SensitiveString::new("PRIVATE KEY placeholder"),
known_hosts_entry: crate::catalog::SensitiveString::new(
"example.test ssh-ed25519 placeholder",
@@ -2884,6 +2978,10 @@ mod tests {
host_trust_revision: 1,
access: workspace_api::RepositoryAccessMode::ReadOnly,
expires_at_epoch_seconds: u64::MAX,
repository_id: "repo-main".to_string(),
repository_source_fingerprint: "sha256:test".to_string(),
repository_uri: "ssh://git@example.test/repo.git".to_string(),
secret_resource: repository_resource_handle(),
private_key: crate::catalog::SensitiveString::new("PRIVATE KEY placeholder"),
known_hosts_entry: crate::catalog::SensitiveString::new(
"other.test ssh-ed25519 placeholder",