fix: retain byte command transport and explicit authorization fixtures
This commit is contained in:
@@ -5047,6 +5047,65 @@ mod tests {
|
||||
|
||||
const SAVED_SUBJEKTIV_SYSTEM_PROMPT: &str = "Committed saved Subject Worker system prompt";
|
||||
|
||||
// Substituted Host transport for restore/controller component tests. This
|
||||
// fixture has exactly the two activation grants requested by the saved
|
||||
// Manifest; it grants no protected operations or resources. It does not
|
||||
// exercise authenticated source derivation, Subject policy, or revocation
|
||||
// in the real Workspace Host.
|
||||
fn saved_subjektiv_authorization_fixture(
|
||||
input: server_api::authorization::OperationAuthorizationRequest,
|
||||
) -> Result<server_api::authorization::OperationAuthorizationResponse, &'static str> {
|
||||
use server_api::authorization::{OperationAuthorizationResponse, PermissionResource};
|
||||
if input.validate().is_ok()
|
||||
&& matches!(input.operation.as_str(), "subjektiv" | "workdir_catalog")
|
||||
&& input.resource == PermissionResource::Feature
|
||||
{
|
||||
Ok(OperationAuthorizationResponse {
|
||||
scope: Some(PermissionResource::Feature),
|
||||
})
|
||||
} else {
|
||||
Err("saved Subject fixture has no matching exact Feature grant")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_subjektiv_authorization_fixture_grants_only_exact_feature_activation() {
|
||||
use server_api::authorization::{OperationAuthorizationRequest, PermissionResource};
|
||||
for operation in ["subjektiv", "workdir_catalog"] {
|
||||
let response = saved_subjektiv_authorization_fixture(OperationAuthorizationRequest {
|
||||
operation: operation.into(),
|
||||
resource: PermissionResource::Feature,
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(response.scope, Some(PermissionResource::Feature));
|
||||
}
|
||||
for (operation, resource) in [
|
||||
("ticket", PermissionResource::Feature),
|
||||
("subjektiv.journal", PermissionResource::Feature),
|
||||
("subjektiv.*", PermissionResource::Feature),
|
||||
(
|
||||
"subjektiv",
|
||||
PermissionResource::Workspace {
|
||||
workspace_id: "workspace-saved-subjektiv".into(),
|
||||
},
|
||||
),
|
||||
(
|
||||
"subjektiv.journal",
|
||||
PermissionResource::Workspace {
|
||||
workspace_id: "workspace-saved-subjektiv".into(),
|
||||
},
|
||||
),
|
||||
] {
|
||||
assert!(
|
||||
saved_subjektiv_authorization_fixture(OperationAuthorizationRequest {
|
||||
operation: operation.into(),
|
||||
resource,
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn saved_subjektiv_policy(
|
||||
root: &Path,
|
||||
worker_ref: &WorkerRef,
|
||||
@@ -5211,6 +5270,25 @@ mod tests {
|
||||
}))
|
||||
}),
|
||||
);
|
||||
let app = app.route(
|
||||
"/api/w/workspace-saved-subjektiv/authorization/check",
|
||||
axum::routing::post(
|
||||
|axum::Json(input): axum::Json<
|
||||
server_api::authorization::OperationAuthorizationRequest,
|
||||
>| async move {
|
||||
match saved_subjektiv_authorization_fixture(input) {
|
||||
Ok(response) => (
|
||||
axum::http::StatusCode::OK,
|
||||
axum::Json(serde_json::to_value(response).unwrap()),
|
||||
),
|
||||
Err(message) => (
|
||||
axum::http::StatusCode::FORBIDDEN,
|
||||
axum::Json(serde_json::json!({ "message": message })),
|
||||
),
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
@@ -6865,10 +6943,10 @@ mod tests {
|
||||
working_directory_id: workdir_id,
|
||||
capabilities: WorkdirSessionCapabilities::READ_ONLY,
|
||||
}];
|
||||
let client = WorkerWorkspaceContext::local_filesystem(Some(
|
||||
WorkspaceId::new("workspace-a").unwrap(),
|
||||
))
|
||||
.client_handle();
|
||||
// Direct local-router capability test: no Workspace Host transport is
|
||||
// installed. Use an explicit standalone binding, not an unavailable
|
||||
// Workspace-bound client pretending to authorize operations.
|
||||
let client = WorkerWorkspaceContext::no_workspace().client_handle();
|
||||
|
||||
let router = restored_workdir_router(&attachments, &logical, &[], client).unwrap();
|
||||
|
||||
@@ -7473,6 +7551,91 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// Generic controller tests explicitly substitute the Host's non-Subject
|
||||
// legacy decision. No Subject authority is modeled here; all other requests
|
||||
// still use the original transport and its Backend/provider checks.
|
||||
#[derive(Debug)]
|
||||
struct LegacyNonSubjectAuthorizationClient(Arc<dyn WorkspaceClient>);
|
||||
|
||||
impl WorkspaceClient for LegacyNonSubjectAuthorizationClient {
|
||||
fn workspace_id(&self) -> Option<&str> {
|
||||
self.0.workspace_id()
|
||||
}
|
||||
|
||||
fn kind(&self) -> &str {
|
||||
self.0.kind()
|
||||
}
|
||||
|
||||
fn is_available(&self) -> bool {
|
||||
self.0.is_available()
|
||||
}
|
||||
|
||||
fn execute(
|
||||
&self,
|
||||
request: worker::WorkspaceRequest,
|
||||
) -> Result<worker::WorkspaceResponse, worker::WorkspaceClientError> {
|
||||
if request.method == worker::WorkspaceRequestMethod::Post
|
||||
&& request.path
|
||||
== format!(
|
||||
"/api/w/{}/authorization/check",
|
||||
self.workspace_id()
|
||||
.expect("legacy fixture needs Workspace identity")
|
||||
)
|
||||
{
|
||||
let input: server_api::authorization::OperationAuthorizationRequest =
|
||||
serde_json::from_str(request.body.as_deref().unwrap_or_default()).map_err(
|
||||
|error| worker::WorkspaceClientError::Request(error.to_string()),
|
||||
)?;
|
||||
input
|
||||
.validate()
|
||||
.map_err(worker::WorkspaceClientError::Request)?;
|
||||
return Ok(worker::WorkspaceResponse {
|
||||
status: 200,
|
||||
body: serde_json::to_string(
|
||||
&server_api::authorization::OperationAuthorizationResponse { scope: None },
|
||||
)
|
||||
.unwrap(),
|
||||
});
|
||||
}
|
||||
self.0.execute(request)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_non_subject_authorization_fixture_preserves_other_transport_failures() {
|
||||
use server_api::authorization::PermissionResource;
|
||||
let client = LegacyNonSubjectAuthorizationClient(
|
||||
WorkerWorkspaceContext::local_filesystem(Some(WorkspaceId::new("ws-test").unwrap()))
|
||||
.client_handle(),
|
||||
);
|
||||
assert_eq!(
|
||||
client
|
||||
.authorize_operation_scope("workdir_catalog", &PermissionResource::Feature)
|
||||
.unwrap(),
|
||||
None,
|
||||
);
|
||||
assert_eq!(
|
||||
client
|
||||
.authorize_operation_scope(
|
||||
"ticket.create",
|
||||
&PermissionResource::Workspace {
|
||||
workspace_id: "ws-test".into(),
|
||||
}
|
||||
)
|
||||
.unwrap(),
|
||||
None,
|
||||
);
|
||||
assert!(
|
||||
client
|
||||
.authorize_operation("ticket.*", &PermissionResource::Feature)
|
||||
.is_err()
|
||||
);
|
||||
assert!(matches!(
|
||||
client.execute(worker::WorkspaceRequest::get("/api/w/ws-test/tickets")),
|
||||
Err(worker::WorkspaceClientError::Unavailable(_)),
|
||||
));
|
||||
}
|
||||
|
||||
struct MockFactory {
|
||||
client: MockClient,
|
||||
runtime_base: PathBuf,
|
||||
@@ -7538,6 +7701,22 @@ mod tests {
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let workspace_context = if workspace_context.client().is_available()
|
||||
&& workspace_context.workspace_id().is_some()
|
||||
{
|
||||
assert!(
|
||||
!request.request.subjektiv_attached,
|
||||
"generic legacy fixture must never authorize a Subject Worker"
|
||||
);
|
||||
WorkerWorkspaceContext::with_client(
|
||||
workspace_context.workspace_id().cloned(),
|
||||
Arc::new(LegacyNonSubjectAuthorizationClient(
|
||||
workspace_context.client_handle(),
|
||||
)),
|
||||
)
|
||||
} else {
|
||||
workspace_context
|
||||
};
|
||||
let workspace_client = workspace_context.client_handle();
|
||||
self.observed_workspace_clients.lock().unwrap().push((
|
||||
workspace_client.kind().to_string(),
|
||||
|
||||
@@ -417,11 +417,6 @@ impl WorkdirSession for AuthorizedWorkdirSession {
|
||||
async fn start_command(&self, request: CommandRequest) -> Result<CommandHandle, WorkdirError> {
|
||||
// Validate coordinates without treating cwd as the command's access scope.
|
||||
WorkdirPath::new(request.cwd.as_str()).map_err(WorkdirError::from)?;
|
||||
if request.spill_dir.is_some() {
|
||||
return Err(WorkdirError::Denied(
|
||||
"provider-local command spill paths have no attachment resource mapping".into(),
|
||||
));
|
||||
}
|
||||
self.authorize_command()?;
|
||||
self.session.start_command(request).await
|
||||
}
|
||||
@@ -609,9 +604,8 @@ mod tests {
|
||||
CommandRequest {
|
||||
command: command.into(),
|
||||
timeout_secs: 10,
|
||||
output_limit: 4096,
|
||||
transport_byte_budget: 4096,
|
||||
cwd: WorkdirPath::root(),
|
||||
spill_dir: None,
|
||||
tool_call_id: None,
|
||||
}
|
||||
}
|
||||
@@ -786,7 +780,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unmappable_absolute_or_spill_paths_are_denied_before_local_effects() {
|
||||
async fn unmappable_absolute_paths_are_denied_before_local_effects() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let client = AuthorizationClient::new(vec![
|
||||
permission(WRITE, ".", true),
|
||||
@@ -804,15 +798,7 @@ mod tests {
|
||||
.await,
|
||||
Err(WorkdirError::Denied(_))
|
||||
));
|
||||
let mut request = command("printf bad > escaped");
|
||||
request.spill_dir = Some(root.path().join("spill"));
|
||||
assert!(matches!(
|
||||
session.start_command(request).await,
|
||||
Err(WorkdirError::Denied(_))
|
||||
));
|
||||
assert!(!root.path().join("absolute").exists());
|
||||
assert!(!root.path().join("escaped").exists());
|
||||
assert!(!root.path().join("spill").exists());
|
||||
assert!(client.requests.lock().unwrap().is_empty());
|
||||
session.close().await.unwrap();
|
||||
}
|
||||
|
||||
@@ -134,13 +134,6 @@ fn authorize_permissions(
|
||||
// Validating cwd is not a shell confinement mechanism. The command can cd elsewhere,
|
||||
// so a prefix grant (even one covering cwd recursively) is never enough.
|
||||
resource(working_directory_id, COMMAND, &request.cwd, false)?;
|
||||
if request.spill_dir.is_some() {
|
||||
// This is a provider-local PathBuf, not an attachment-relative WorkdirPath.
|
||||
// Do not authorize an unrepresentable write destination as attachment scope.
|
||||
return Err(denied(
|
||||
"caller-selected provider-local command spill paths are unsupported",
|
||||
));
|
||||
}
|
||||
check(COMMAND, &WorkdirPath::root(), true)
|
||||
}
|
||||
WorkdirSessionOperation::CommandOutput(_) => {
|
||||
@@ -233,9 +226,8 @@ mod tests {
|
||||
WorkdirSessionOperation::CommandStart(CommandRequest {
|
||||
command: "cd ..; cat secret".into(),
|
||||
timeout_secs: 1,
|
||||
output_limit: 1024,
|
||||
transport_byte_budget: 1024,
|
||||
cwd: path(cwd),
|
||||
spill_dir: None,
|
||||
tool_call_id: None,
|
||||
})
|
||||
}
|
||||
@@ -427,14 +419,6 @@ mod tests {
|
||||
assert!(!allowed(&[grant(COMMAND, ".", false)], &command("src")));
|
||||
assert!(!allowed(&[grant(WRITE, ".", true)], &command("src")));
|
||||
assert!(allowed(&[grant(COMMAND, ".", true)], &command("src")));
|
||||
let WorkdirSessionOperation::CommandStart(mut request) = command("src") else {
|
||||
unreachable!()
|
||||
};
|
||||
request.spill_dir = Some("/provider/private".into());
|
||||
assert!(!allowed(
|
||||
&[grant(COMMAND, ".", true)],
|
||||
&WorkdirSessionOperation::CommandStart(request)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user