fix: allow workspace clients through ticket REST operations
This commit is contained in:
@@ -3842,7 +3842,7 @@ fn reject_unguarded_ticket_completion(operation: &TicketBackendOperation) -> Res
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn execute_worker_ticket_rest_operation(
|
async fn execute_ticket_rest_operation(
|
||||||
api: &WorkspaceApi,
|
api: &WorkspaceApi,
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
@@ -3863,7 +3863,10 @@ async fn execute_worker_ticket_rest_operation(
|
|||||||
let operation_kind = ticket_mutation_operation_kind(&operation);
|
let operation_kind = ticket_mutation_operation_kind(&operation);
|
||||||
let is_mutation = operation_kind != "read";
|
let is_mutation = operation_kind != "read";
|
||||||
let target = ticket_mutation_target(&operation).cloned();
|
let target = ticket_mutation_target(&operation).cloned();
|
||||||
let source = authenticate_worker_mutation_source(api, workspace_id, &headers)?;
|
// Human clients are authorized by the Workspace route boundary. Runtime-forwarded Worker
|
||||||
|
// calls carry source headers; when either header is present, the complete pair is required
|
||||||
|
// and authenticated before source attribution is attached.
|
||||||
|
let source = optional_worker_mutation_source(api, workspace_id, &headers)?;
|
||||||
reject_unguarded_ticket_completion(&operation)?;
|
reject_unguarded_ticket_completion(&operation)?;
|
||||||
validate_ticket_repository_operation(api, &operation)?;
|
validate_ticket_repository_operation(api, &operation)?;
|
||||||
let before = target.as_ref().and_then(|id| backend.show(id.clone()).ok());
|
let before = target.as_ref().and_then(|id| backend.show(id.clone()).ok());
|
||||||
@@ -3871,9 +3874,12 @@ async fn execute_worker_ticket_rest_operation(
|
|||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|ticket| ticket.meta.workflow_state.as_str().to_string())
|
.map(|ticket| ticket.meta.workflow_state.as_str().to_string())
|
||||||
.unwrap_or_else(|| ticket_operation_initial_state(&operation));
|
.unwrap_or_else(|| ticket_operation_initial_state(&operation));
|
||||||
bind_worker_ticket_operation_source(&source, &mut operation);
|
if let Some(source) = source.as_ref() {
|
||||||
let source_context = worker_ticket_source_context(api, workspace_id, &source, before.as_ref());
|
bind_worker_ticket_operation_source(source, &mut operation);
|
||||||
backend = backend.with_event_attributes(source_context.attributes(operation_kind));
|
let source_context =
|
||||||
|
worker_ticket_source_context(api, workspace_id, source, before.as_ref());
|
||||||
|
backend = backend.with_event_attributes(source_context.attributes(operation_kind));
|
||||||
|
}
|
||||||
|
|
||||||
let result = execute_ticket_backend_operation(&backend, operation).map_err(Error::from)?;
|
let result = execute_ticket_backend_operation(&backend, operation).map_err(Error::from)?;
|
||||||
if is_mutation
|
if is_mutation
|
||||||
@@ -3886,7 +3892,7 @@ async fn execute_worker_ticket_rest_operation(
|
|||||||
&ticket.meta.id,
|
&ticket.meta.id,
|
||||||
&previous_state,
|
&previous_state,
|
||||||
ticket.meta.workflow_state.as_str(),
|
ticket.meta.workflow_state.as_str(),
|
||||||
Some(source),
|
source,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
@@ -3899,7 +3905,7 @@ async fn execute_worker_ticket_test_operation(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(operation): Json<TicketBackendOperation>,
|
Json(operation): Json<TicketBackendOperation>,
|
||||||
) -> ApiResult<Json<TicketBackendOperationResult>> {
|
) -> ApiResult<Json<TicketBackendOperationResult>> {
|
||||||
execute_worker_ticket_rest_operation(&api, &path.workspace_id, headers, operation)
|
execute_ticket_rest_operation(&api, &path.workspace_id, headers, operation)
|
||||||
.await
|
.await
|
||||||
.map(Json)
|
.map(Json)
|
||||||
}
|
}
|
||||||
@@ -3935,7 +3941,7 @@ async fn scoped_default_intake_ready_body(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(request): Json<DefaultIntakeReadyBodyRequest>,
|
Json(request): Json<DefaultIntakeReadyBodyRequest>,
|
||||||
) -> ApiResult<Json<String>> {
|
) -> ApiResult<Json<String>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -3972,7 +3978,7 @@ async fn scoped_list_ticket_summaries(
|
|||||||
ticket::TicketListQuery::states(selected)
|
ticket::TicketListQuery::states(selected)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -3990,7 +3996,7 @@ async fn scoped_get_ticket_record(
|
|||||||
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
|
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> ApiResult<Json<ticket::Ticket>> {
|
) -> ApiResult<Json<ticket::Ticket>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4020,7 +4026,7 @@ async fn scoped_create_ticket_record(
|
|||||||
"Ticket creation must start in planning; use guarded workflow operations for later states",
|
"Ticket creation must start in planning; use guarded workflow operations for later states",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4039,7 +4045,7 @@ async fn scoped_edit_ticket_record_item(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(edit): Json<TicketItemEdit>,
|
Json(edit): Json<TicketItemEdit>,
|
||||||
) -> ApiResult<Json<ticket::Ticket>> {
|
) -> ApiResult<Json<ticket::Ticket>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4060,7 +4066,7 @@ async fn scoped_ticket_dependency_check(
|
|||||||
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
|
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> ApiResult<Json<ticket::TicketDependencyCheck>> {
|
) -> ApiResult<Json<ticket::TicketDependencyCheck>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4081,7 +4087,7 @@ async fn scoped_add_ticket_thread_event(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(event): Json<NewTicketEvent>,
|
Json(event): Json<NewTicketEvent>,
|
||||||
) -> ApiResult<StatusCode> {
|
) -> ApiResult<StatusCode> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4100,7 +4106,7 @@ async fn scoped_add_ticket_state_change(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(change): Json<TicketStateChange>,
|
Json(change): Json<TicketStateChange>,
|
||||||
) -> ApiResult<StatusCode> {
|
) -> ApiResult<StatusCode> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4119,7 +4125,7 @@ async fn scoped_add_ticket_intake_summary(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(summary): Json<ticket::TicketIntakeSummary>,
|
Json(summary): Json<ticket::TicketIntakeSummary>,
|
||||||
) -> ApiResult<StatusCode> {
|
) -> ApiResult<StatusCode> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4147,7 +4153,7 @@ async fn scoped_set_ticket_state_field(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(change): Json<TicketStateChange>,
|
Json(change): Json<TicketStateChange>,
|
||||||
) -> ApiResult<StatusCode> {
|
) -> ApiResult<StatusCode> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4167,7 +4173,7 @@ async fn scoped_set_ticket_workflow_state(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(change): Json<TicketStateChange>,
|
Json(change): Json<TicketStateChange>,
|
||||||
) -> ApiResult<StatusCode> {
|
) -> ApiResult<StatusCode> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4186,7 +4192,7 @@ async fn scoped_mark_ticket_ready(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(request): Json<TicketMarkReadyRequest>,
|
Json(request): Json<TicketMarkReadyRequest>,
|
||||||
) -> ApiResult<Json<ticket::Ticket>> {
|
) -> ApiResult<Json<ticket::Ticket>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4212,7 +4218,7 @@ async fn scoped_queue_ticket_record(
|
|||||||
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
|
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> ApiResult<StatusCode> {
|
) -> ApiResult<StatusCode> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4975,7 +4981,7 @@ async fn scoped_close_ticket_record(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(resolution): Json<MarkdownText>,
|
Json(resolution): Json<MarkdownText>,
|
||||||
) -> ApiResult<StatusCode> {
|
) -> ApiResult<StatusCode> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -4994,7 +5000,7 @@ async fn scoped_record_ticket_relation(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(relation): Json<ticket::NewTicketRelation>,
|
Json(relation): Json<ticket::NewTicketRelation>,
|
||||||
) -> ApiResult<Json<ticket::TicketRelation>> {
|
) -> ApiResult<Json<ticket::TicketRelation>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -5022,7 +5028,7 @@ async fn scoped_remove_ticket_relation(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(relation): Json<TicketRelationRemoveRequest>,
|
Json(relation): Json<TicketRelationRemoveRequest>,
|
||||||
) -> ApiResult<Json<ticket::TicketRelation>> {
|
) -> ApiResult<Json<ticket::TicketRelation>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -5051,7 +5057,7 @@ async fn scoped_query_ticket_relations(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(query): Json<TicketRelationSearchRequest>,
|
Json(query): Json<TicketRelationSearchRequest>,
|
||||||
) -> ApiResult<Json<Vec<ticket::TicketRelation>>> {
|
) -> ApiResult<Json<Vec<ticket::TicketRelation>>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -5072,7 +5078,7 @@ async fn scoped_ticket_relation_view(
|
|||||||
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
|
AxumPath((workspace_id, id)): AxumPath<(String, String)>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> ApiResult<Json<ticket::TicketRelationView>> {
|
) -> ApiResult<Json<ticket::TicketRelationView>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -5093,7 +5099,7 @@ async fn scoped_record_ticket_orchestration_plan(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(record): Json<ticket::NewOrchestrationPlanRecord>,
|
Json(record): Json<ticket::NewOrchestrationPlanRecord>,
|
||||||
) -> ApiResult<Json<ticket::OrchestrationPlanRecord>> {
|
) -> ApiResult<Json<ticket::OrchestrationPlanRecord>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&workspace_id,
|
&workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -5121,7 +5127,7 @@ async fn scoped_query_ticket_orchestration_plans(
|
|||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
Json(query): Json<TicketOrchestrationPlanSearchRequest>,
|
Json(query): Json<TicketOrchestrationPlanSearchRequest>,
|
||||||
) -> ApiResult<Json<Vec<ticket::OrchestrationPlanRecord>>> {
|
) -> ApiResult<Json<Vec<ticket::OrchestrationPlanRecord>>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -5142,7 +5148,7 @@ async fn scoped_ticket_doctor(
|
|||||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
) -> ApiResult<Json<ticket::TicketDoctorReport>> {
|
) -> ApiResult<Json<ticket::TicketDoctorReport>> {
|
||||||
let result = execute_worker_ticket_rest_operation(
|
let result = execute_ticket_rest_operation(
|
||||||
&api,
|
&api,
|
||||||
&path.workspace_id,
|
&path.workspace_id,
|
||||||
headers,
|
headers,
|
||||||
@@ -15390,14 +15396,9 @@ mod tests {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
for operation in operations {
|
for operation in operations {
|
||||||
execute_worker_ticket_rest_operation(
|
execute_ticket_rest_operation(&api, TEST_WORKSPACE_ID, source_headers(), operation)
|
||||||
&api,
|
.await
|
||||||
TEST_WORKSPACE_ID,
|
.unwrap();
|
||||||
source_headers(),
|
|
||||||
operation,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let inputs = execution.take_inputs();
|
let inputs = execution.take_inputs();
|
||||||
@@ -18165,7 +18166,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn ticket_rest_search_requires_worker_source_identity_and_rpc_route_is_removed() {
|
async fn ticket_rest_search_allows_workspace_product_clients_and_rejects_invalid_worker_source()
|
||||||
|
{
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
let api = test_api(dir.path()).await;
|
let api = test_api(dir.path()).await;
|
||||||
let app = build_router(api);
|
let app = build_router(api);
|
||||||
@@ -18183,7 +18185,23 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let response = app
|
||||||
|
.clone()
|
||||||
|
.oneshot(
|
||||||
|
Request::builder()
|
||||||
|
.method("POST")
|
||||||
|
.uri(format!("/api/w/{TEST_WORKSPACE_ID}/tickets"))
|
||||||
|
.header("content-type", "application/json")
|
||||||
|
.body(Body::from(
|
||||||
|
serde_json::to_vec(&ticket::NewTicket::new("CLI Ticket")).unwrap(),
|
||||||
|
))
|
||||||
|
.unwrap(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
|
|
||||||
let response = app
|
let response = app
|
||||||
.clone()
|
.clone()
|
||||||
|
|||||||
Reference in New Issue
Block a user