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
+1 -1
View File
@@ -4500,7 +4500,7 @@ mod tests {
let handle = bundle.profile_source_archive_handle.as_ref().unwrap();
assert!(bundle.profile_source_archive.is_none());
let response = broker
.fetch_profile_source_archive(worker_runtime::resource::BackendResourceFetchRequest {
.fetch_resource(worker_runtime::resource::BackendResourceFetchRequest {
handle: handle.clone(),
runtime_id: runtime_id.to_string(),
worker_id: None,
+199 -33
View File
@@ -9,7 +9,8 @@ use worker_runtime::resource::{
BackendResourceClient, BackendResourceError, BackendResourceFetchRequest,
BackendResourceFetchResponse, BackendResourceHandle, BackendResourceKind,
BackendResourceOperation, DEFAULT_PROFILE_SOURCE_ARCHIVE_MAX_BYTES,
PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE, ResourceRedactionPolicy,
DEFAULT_REPOSITORY_SSH_ACCESS_MAX_BYTES, PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE,
REPOSITORY_SSH_ACCESS_CONTENT_TYPE, RepositorySshAccessSecret, ResourceRedactionPolicy,
};
#[derive(Clone, Default)]
@@ -29,7 +30,34 @@ struct StoredResource {
runtime_id: Option<String>,
worker: Option<RuntimeWorkerRef>,
handle: BackendResourceHandle,
archive: ProfileSourceArchive,
bytes: Vec<u8>,
archive: Option<ProfileSourceArchive>,
one_shot: bool,
}
impl StoredResource {
fn byte_len(&self) -> usize {
self.archive
.as_ref()
.map(|archive| archive.content.len())
.unwrap_or_else(|| self.bytes.len())
}
fn take_bytes(&mut self) -> Vec<u8> {
self.archive
.as_mut()
.map(|archive| std::mem::take(&mut archive.content))
.unwrap_or_else(|| std::mem::take(&mut self.bytes))
}
}
impl Drop for StoredResource {
fn drop(&mut self) {
self.bytes.fill(0);
if let Some(archive) = self.archive.as_mut() {
archive.content.fill(0);
}
}
}
impl BackendResourceBroker {
@@ -73,7 +101,9 @@ impl BackendResourceBroker {
runtime_id,
worker,
handle: handle.clone(),
archive,
bytes: Vec::new(),
archive: Some(archive),
one_shot: false,
};
if let Ok(mut resources) = self.resources.lock() {
resources.insert(nonce, stored);
@@ -81,6 +111,84 @@ impl BackendResourceBroker {
handle
}
pub fn issue_repository_ssh_access_handle(
&self,
workspace_id: impl Into<String>,
runtime_id: &str,
resource_id: impl Into<String>,
revision: impl Into<String>,
expires_at_unix_seconds: i64,
secret: RepositorySshAccessSecret,
) -> Result<BackendResourceHandle, BackendResourceError> {
let bytes =
serde_json::to_vec(&secret).map_err(|error| BackendResourceError::InvalidResponse {
message: error.to_string(),
})?;
if bytes.len() as u64 > DEFAULT_REPOSITORY_SSH_ACCESS_MAX_BYTES {
return Err(BackendResourceError::Oversized {
max_bytes: DEFAULT_REPOSITORY_SSH_ACCESS_MAX_BYTES,
actual_bytes: bytes.len() as u64,
});
}
let workspace_id = workspace_id.into();
let resource_id = resource_id.into();
let revision = revision.into();
let nonce = Uuid::now_v7().to_string();
let handle = BackendResourceHandle {
kind: BackendResourceKind::RepositorySshAccess,
workspace_id,
scope_id: Some("repository-ssh-access".to_string()),
runtime_id: Some(runtime_id.to_string()),
worker_id: None,
resource_id,
digest: format!("opaque:{nonce}"),
operation: BackendResourceOperation::FetchOnce,
expires_at_unix_seconds,
nonce: nonce.clone(),
revision,
generation: None,
max_bytes: DEFAULT_REPOSITORY_SSH_ACCESS_MAX_BYTES,
content_type: REPOSITORY_SSH_ACCESS_CONTENT_TYPE.to_string(),
redaction: ResourceRedactionPolicy::RuntimeInternalOnly,
audit_correlation_id: format!("repository-ssh-access-{nonce}"),
profile_source_graph: None,
};
let stored = StoredResource {
runtime_id: Some(runtime_id.to_string()),
worker: None,
handle: handle.clone(),
bytes,
archive: None,
one_shot: true,
};
let resource_key = nonce.clone();
self.resources
.lock()
.map_err(|_| BackendResourceError::Transport {
message: "resource broker lock poisoned".to_string(),
})?
.insert(resource_key.clone(), stored);
if expires_at_unix_seconds != i64::MAX {
let resources = self.resources.clone();
std::thread::spawn(move || {
let now = Utc::now().timestamp();
if expires_at_unix_seconds > now {
std::thread::sleep(std::time::Duration::from_secs(
(expires_at_unix_seconds - now) as u64,
));
}
if let Ok(mut resources) = resources.lock()
&& resources
.get(&resource_key)
.is_some_and(|stored| stored.handle.nonce == resource_key)
{
resources.remove(&resource_key);
}
});
}
Ok(handle)
}
pub fn profile_source_archive(
&self,
digest: &str,
@@ -90,20 +198,21 @@ impl BackendResourceBroker {
.ok()?
.values()
.find(|resource| resource.handle.digest == digest)
.map(|resource| resource.archive.clone())
.and_then(|resource| resource.archive.clone())
}
pub fn fetch_profile_source_archive(
pub fn fetch_resource(
&self,
request: BackendResourceFetchRequest,
) -> Result<BackendResourceFetchResponse, BackendResourceError> {
verify_handle_shape(&request.handle)?;
let stored = self
let mut resources = self
.resources
.lock()
.map_err(|_| BackendResourceError::Transport {
message: "resource broker lock poisoned".to_string(),
})?
})?;
let mut stored = resources
.get(&request.handle.nonce)
.cloned()
.ok_or(BackendResourceError::MissingResource)?;
@@ -111,7 +220,7 @@ impl BackendResourceBroker {
if stored.handle.expires_at_unix_seconds < Utc::now().timestamp() {
return Err(BackendResourceError::Expired);
}
let actual_bytes = stored.archive.content.len() as u64;
let actual_bytes = stored.byte_len() as u64;
if actual_bytes > stored.handle.max_bytes {
return Err(BackendResourceError::Oversized {
max_bytes: stored.handle.max_bytes,
@@ -139,12 +248,15 @@ impl BackendResourceBroker {
});
}
}
if stored.one_shot {
resources.remove(&request.handle.nonce);
}
Ok(BackendResourceFetchResponse {
kind: BackendResourceKind::ProfileSourceArchive,
resource_id: stored.archive.reference.id,
digest: stored.archive.reference.digest,
content_type: PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE.to_string(),
bytes: stored.archive.content,
kind: stored.handle.kind.clone(),
resource_id: stored.handle.resource_id.clone(),
digest: stored.handle.digest.clone(),
content_type: stored.handle.content_type.clone(),
bytes: stored.take_bytes(),
audit_correlation_id: request.audit_correlation_id,
})
}
@@ -156,24 +268,38 @@ impl BackendResourceClient for BackendResourceBroker {
&self,
request: BackendResourceFetchRequest,
) -> Result<BackendResourceFetchResponse, BackendResourceError> {
self.fetch_profile_source_archive(request)
self.fetch_resource(request)
}
}
fn verify_handle_shape(handle: &BackendResourceHandle) -> Result<(), BackendResourceError> {
if handle.kind != BackendResourceKind::ProfileSourceArchive {
return Err(BackendResourceError::UnsupportedKind);
}
if handle.operation != BackendResourceOperation::FetchArchive {
return Err(BackendResourceError::Unauthorized {
message: "resource handle operation is not fetch_archive".to_string(),
});
}
if handle.content_type != PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE {
return Err(BackendResourceError::ContentTypeMismatch {
expected: PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE.to_string(),
actual: handle.content_type.clone(),
});
match handle.kind {
BackendResourceKind::ProfileSourceArchive => {
if handle.operation != BackendResourceOperation::FetchArchive {
return Err(BackendResourceError::Unauthorized {
message: "resource handle operation is not fetch_archive".to_string(),
});
}
if handle.content_type != PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE {
return Err(BackendResourceError::ContentTypeMismatch {
expected: PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE.to_string(),
actual: handle.content_type.clone(),
});
}
}
BackendResourceKind::RepositorySshAccess => {
if handle.operation != BackendResourceOperation::FetchOnce {
return Err(BackendResourceError::Unauthorized {
message: "resource handle operation is not fetch_once".to_string(),
});
}
if handle.content_type != REPOSITORY_SSH_ACCESS_CONTENT_TYPE {
return Err(BackendResourceError::ContentTypeMismatch {
expected: REPOSITORY_SSH_ACCESS_CONTENT_TYPE.to_string(),
actual: handle.content_type.clone(),
});
}
}
}
Ok(())
}
@@ -249,7 +375,7 @@ mod tests {
archive(),
);
let response = broker
.fetch_profile_source_archive(BackendResourceFetchRequest {
.fetch_resource(BackendResourceFetchRequest {
handle: handle.clone(),
runtime_id: runtime_id.to_string(),
worker_id: None,
@@ -260,6 +386,46 @@ mod tests {
assert_eq!(response.content_type, PROFILE_SOURCE_ARCHIVE_CONTENT_TYPE);
}
#[test]
fn repository_ssh_access_resource_is_runtime_bound_and_one_shot() {
let broker = BackendResourceBroker::default();
let handle = broker
.issue_repository_ssh_access_handle(
"workspace-test",
"runtime-test",
"repository-access-test",
"1",
i64::MAX,
RepositorySshAccessSecret {
private_key: "private-key-bytes".to_string(),
known_hosts_entry: "known-hosts-entry".to_string(),
},
)
.unwrap();
let unauthorized = broker
.fetch_resource(request(handle.clone(), "runtime-other", None))
.unwrap_err();
assert!(matches!(
unauthorized,
BackendResourceError::Unauthorized { .. }
));
let response = broker
.fetch_resource(request(handle.clone(), "runtime-test", None))
.unwrap();
assert_eq!(response.kind, BackendResourceKind::RepositorySshAccess);
assert_eq!(response.content_type, REPOSITORY_SSH_ACCESS_CONTENT_TYPE);
let debug = format!("{response:?}");
assert!(!debug.contains("private-key-bytes"));
assert!(debug.contains("REDACTED"));
let secret: RepositorySshAccessSecret = serde_json::from_slice(&response.bytes).unwrap();
assert_eq!(secret.private_key, "private-key-bytes");
assert!(matches!(
broker.fetch_resource(request(handle, "runtime-test", None)),
Err(BackendResourceError::MissingResource)
));
}
#[test]
fn broker_rejects_runtime_mismatch() {
let broker = BackendResourceBroker::default();
@@ -270,7 +436,7 @@ mod tests {
archive(),
);
let err = broker
.fetch_profile_source_archive(request(handle, "runtime-b", None))
.fetch_resource(request(handle, "runtime-b", None))
.unwrap_err();
assert!(matches!(err, BackendResourceError::Unauthorized { .. }));
}
@@ -287,7 +453,7 @@ mod tests {
archive(),
);
let err = broker
.fetch_profile_source_archive(request(handle, runtime_id, Some(&worker_b.worker_id)))
.fetch_resource(request(handle, runtime_id, Some(&worker_b.worker_id)))
.unwrap_err();
assert!(matches!(err, BackendResourceError::Unauthorized { .. }));
}
@@ -312,7 +478,7 @@ mod tests {
let mut extended = handle;
extended.expires_at_unix_seconds = 4_102_444_800;
let err = broker
.fetch_profile_source_archive(request(extended, &runtime_id, None))
.fetch_resource(request(extended, &runtime_id, None))
.unwrap_err();
assert!(matches!(err, BackendResourceError::Expired));
}
@@ -328,7 +494,7 @@ mod tests {
);
handle.scope_id = Some("tampered-scope".to_string());
let err = broker
.fetch_profile_source_archive(request(handle, &runtime_id, None))
.fetch_resource(request(handle, &runtime_id, None))
.unwrap_err();
assert!(matches!(err, BackendResourceError::Unauthorized { .. }));
}
@@ -345,7 +511,7 @@ mod tests {
);
handle.max_bytes = DEFAULT_PROFILE_SOURCE_ARCHIVE_MAX_BYTES + 1024;
let err = broker
.fetch_profile_source_archive(request(handle, &runtime_id, None))
.fetch_resource(request(handle, &runtime_id, None))
.unwrap_err();
assert!(matches!(err, BackendResourceError::Oversized { .. }));
}
+89 -13
View File
@@ -11504,7 +11504,7 @@ async fn scoped_post_internal_runtime_resource_fetch(
));
}
api.resource_broker
.fetch_profile_source_archive(request)
.fetch_resource(request)
.map(Json)
.map_err(|error| (backend_resource_error_status(&error), Json(error)))
}
@@ -14122,6 +14122,29 @@ fn authorize_repository_materialization_operation(
));
}
};
let expires_at_epoch_seconds = repository_access_expiry();
let secret_resource = api
.resource_broker
.issue_repository_ssh_access_handle(
&api.config.workspace_id,
&operation.resolved_runtime_id,
format!("repository-ssh-access:{}", operation.operation_id),
format!(
"credential:{}:host-trust:{}",
lease.credential_revision, lease.host_trust_revision
),
i64::try_from(expires_at_epoch_seconds).unwrap_or(i64::MAX),
worker_runtime::resource::RepositorySshAccessSecret {
private_key: lease.private_key.as_str().to_string(),
known_hosts_entry: lease.known_hosts_entry.clone(),
},
)
.map_err(|_| {
settings_bad_request(
"working_directory_repository_access_resource_failed",
"Repository SSH access resource could not be issued",
)
})?;
RepositoryMaterializationContext {
workspace_id: api.config.workspace_id.clone(),
runtime_id: operation.resolved_runtime_id.clone(),
@@ -14135,9 +14158,13 @@ fn authorize_repository_materialization_operation(
host_trust_id: lease.host_trust_id,
host_trust_revision: lease.host_trust_revision,
access,
expires_at_epoch_seconds: repository_access_expiry(),
private_key: SensitiveString::new(lease.private_key.as_str()),
known_hosts_entry: SensitiveString::new(lease.known_hosts_entry),
expires_at_epoch_seconds,
repository_id: request.repository.id.clone(),
repository_source_fingerprint: request.repository.source_fingerprint.clone(),
repository_uri: request.repository.source.uri.clone(),
secret_resource,
private_key: SensitiveString::default(),
known_hosts_entry: SensitiveString::default(),
}),
}
} else {
@@ -14230,15 +14257,42 @@ fn authorize_repository_materialization(
let lease = api
.repository_secrets
.lease_ssh_materialization_access(&api.config.workspace_id, binding)?;
let expires_at_epoch_seconds = repository_access_expiry();
let secret_resource = api
.resource_broker
.issue_repository_ssh_access_handle(
&api.config.workspace_id,
runtime_id,
format!("repository-ssh-access:{operation_id}"),
format!(
"credential:{}:host-trust:{}",
lease.credential_revision, lease.host_trust_revision
),
i64::try_from(expires_at_epoch_seconds).unwrap_or(i64::MAX),
worker_runtime::resource::RepositorySshAccessSecret {
private_key: lease.private_key.as_str().to_string(),
known_hosts_entry: lease.known_hosts_entry.clone(),
},
)
.map_err(|_| {
settings_bad_request(
"working_directory_repository_access_resource_failed",
"Repository SSH access resource could not be issued",
)
})?;
Some(RepositorySshMaterializationAccess {
credential_id: lease.credential_id,
credential_revision: lease.credential_revision,
host_trust_id: lease.host_trust_id,
host_trust_revision: lease.host_trust_revision,
access: binding.access,
expires_at_epoch_seconds: repository_access_expiry(),
private_key: SensitiveString::new(lease.private_key.as_str()),
known_hosts_entry: SensitiveString::new(lease.known_hosts_entry),
expires_at_epoch_seconds,
repository_id: request.repository.id.clone(),
repository_source_fingerprint: request.repository.source_fingerprint.clone(),
repository_uri: request.repository.source.uri.clone(),
secret_resource,
private_key: SensitiveString::default(),
known_hosts_entry: SensitiveString::default(),
})
} else {
None
@@ -15423,6 +15477,20 @@ mod tests {
);
let mut repository_access_launch = workdir_flow_launch;
let secret_resource = api
.resource_broker
.issue_repository_ssh_access_handle(
&api.config.workspace_id,
"runtime-1",
"repository-access-test",
"1",
i64::MAX,
worker_runtime::resource::RepositorySshAccessSecret {
private_key: "private-key-bytes".to_string(),
known_hosts_entry: "known-hosts-entry".to_string(),
},
)
.unwrap();
let working_directory = repository_access_launch
.resolved_working_directory_request
.as_mut()
@@ -15444,12 +15512,15 @@ mod tests {
host_trust_revision: 1,
access: workspace_api::RepositoryAccessMode::ReadOnly,
expires_at_epoch_seconds: u64::MAX,
private_key: worker_runtime::catalog::SensitiveString::new(
"private-key-bytes",
),
known_hosts_entry: worker_runtime::catalog::SensitiveString::new(
"known-hosts-entry",
),
repository_id: working_directory.repository.id.clone(),
repository_source_fingerprint: working_directory
.repository
.source_fingerprint
.clone(),
repository_uri: working_directory.repository.source.uri.clone(),
secret_resource,
private_key: worker_runtime::catalog::SensitiveString::default(),
known_hosts_entry: worker_runtime::catalog::SensitiveString::default(),
},
),
});
@@ -15460,6 +15531,11 @@ mod tests {
assert_eq!(access.working_directory_id, "working-directory-1");
assert!(access.materialization.ssh.is_some());
let serialized_access = serde_json::to_string(&access).unwrap();
assert!(!serialized_access.contains("private-key-bytes"));
assert!(!serialized_access.contains("known-hosts-entry"));
assert!(!serialized_access.contains("private_key"));
assert!(!serialized_access.contains("known_hosts_entry"));
assert!(
repository_access_launch
.resolved_working_directory_request