operations: enforce community capability limits
This commit is contained in:
+286
-3
@@ -233,9 +233,9 @@ mod tests {
|
||||
use crank_core::{
|
||||
AsyncJobHandle, AsyncJobId, DescriptorId, ExecutionConfig, ExecutionMode,
|
||||
GraphqlOperationType, GraphqlTarget, GrpcTarget, HttpMethod, JobStatus, MembershipRole,
|
||||
OperationId, Protocol, RestTarget, SecretKind, SoapBindingStyle, SoapOperationMetadata,
|
||||
SoapTarget, SoapVersion, StreamSession, StreamStatus, Target, ToolDescription,
|
||||
TransportBehavior, WorkspaceId,
|
||||
OperationId, OperationSecurityLevel, Protocol, RestTarget, SecretKind, SoapBindingStyle,
|
||||
SoapOperationMetadata, SoapTarget, SoapVersion, StreamSession, StreamStatus, Target,
|
||||
ToolDescription, TransportBehavior, WebsocketTarget, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_registry::{CreateAsyncJobRequest, CreateStreamSessionRequest, PostgresRegistry};
|
||||
@@ -1067,6 +1067,173 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_unsupported_protocol_for_community_operation_create() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("community_protocol_reject");
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let response = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_websocket_operation_payload(
|
||||
"wss://example.com/stream",
|
||||
"telemetry_ws",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(body["error"]["code"], "validation_error");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
"protocol websocket is not supported in Community"
|
||||
);
|
||||
assert_eq!(
|
||||
body["error"]["context"],
|
||||
json!({
|
||||
"protocol": "websocket",
|
||||
"edition": "community",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_premium_security_level_for_community_operation_create() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("community_security_reject_create");
|
||||
let upstream_base_url = spawn_upstream_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let mut payload = test_operation_payload(&upstream_base_url, "crm_elevated");
|
||||
payload.security_level = OperationSecurityLevel::Elevated;
|
||||
|
||||
let response = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&payload)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(body["error"]["code"], "validation_error");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
"security level elevated is not supported in Community"
|
||||
);
|
||||
assert_eq!(
|
||||
body["error"]["context"],
|
||||
json!({
|
||||
"security_level": "elevated",
|
||||
"edition": "community",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_premium_security_level_for_community_operation_update() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("community_security_reject_update");
|
||||
let upstream_base_url = spawn_upstream_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let created = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_operation_payload(
|
||||
&upstream_base_url,
|
||||
"crm_update_elevated",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
|
||||
|
||||
let response = client
|
||||
.patch(format!("{base_url}/operations/{operation_id}"))
|
||||
.json(&json!({
|
||||
"display_name": "Create Lead",
|
||||
"category": "sales",
|
||||
"security_level": "elevated",
|
||||
"target": {
|
||||
"kind": "rest",
|
||||
"base_url": upstream_base_url,
|
||||
"method": "POST",
|
||||
"path_template": "/crm/leads",
|
||||
"static_headers": {}
|
||||
},
|
||||
"input_schema": object_schema("email"),
|
||||
"output_schema": object_schema("id"),
|
||||
"input_mapping": {
|
||||
"rules": [{
|
||||
"source": "$.mcp.email",
|
||||
"target": "$.request.body.email",
|
||||
"required": true,
|
||||
"default_value": null,
|
||||
"transform": null,
|
||||
"condition": null,
|
||||
"notes": null
|
||||
}]
|
||||
},
|
||||
"output_mapping": {
|
||||
"rules": [{
|
||||
"source": "$.response.body.id",
|
||||
"target": "$.output.id",
|
||||
"required": true,
|
||||
"default_value": null,
|
||||
"transform": null,
|
||||
"condition": null,
|
||||
"notes": null
|
||||
}]
|
||||
},
|
||||
"execution_config": {
|
||||
"timeout_ms": 1000,
|
||||
"retry_policy": null,
|
||||
"auth_profile_ref": null,
|
||||
"headers": {},
|
||||
"protocol_options": null,
|
||||
"streaming": null
|
||||
},
|
||||
"tool_description": {
|
||||
"title": "Create Lead",
|
||||
"description": "Creates a CRM lead",
|
||||
"tags": ["crm"],
|
||||
"examples": []
|
||||
}
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(body["error"]["code"], "validation_error");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
"security level elevated is not supported in Community"
|
||||
);
|
||||
assert_eq!(
|
||||
body["error"]["context"],
|
||||
json!({
|
||||
"security_level": "elevated",
|
||||
"edition": "community",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn manages_workspace_access_lifecycle() {
|
||||
@@ -2375,6 +2542,63 @@ mod tests {
|
||||
assert_eq!(imported["import_mode"], "upsert");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn rejects_premium_security_level_for_community_yaml_upsert() {
|
||||
let registry = test_registry().await;
|
||||
let storage_root = test_storage_root("community_yaml_security_reject");
|
||||
let upstream_base_url = spawn_upstream_server().await;
|
||||
let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await;
|
||||
let client = authorized_client(&base_url).await;
|
||||
|
||||
let created = client
|
||||
.post(format!("{base_url}/operations"))
|
||||
.json(&test_operation_payload(
|
||||
&upstream_base_url,
|
||||
"crm_yaml_standard",
|
||||
))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.json::<Value>()
|
||||
.await
|
||||
.unwrap();
|
||||
let operation_id = created["operation_id"].as_str().unwrap().to_owned();
|
||||
|
||||
let mut yaml = client
|
||||
.get(format!("{base_url}/operations/{operation_id}/export"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.text()
|
||||
.await
|
||||
.unwrap();
|
||||
yaml = yaml.replace("security_level: standard", "security_level: elevated");
|
||||
|
||||
let response = client
|
||||
.post(format!("{base_url}/operations/import?mode=upsert"))
|
||||
.body(yaml)
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
let status = response.status();
|
||||
let body = response.json::<Value>().await.unwrap();
|
||||
|
||||
assert_eq!(status, reqwest::StatusCode::BAD_REQUEST);
|
||||
assert_eq!(body["error"]["code"], "validation_error");
|
||||
assert_eq!(
|
||||
body["error"]["message"],
|
||||
"security level elevated is not supported in Community"
|
||||
);
|
||||
assert_eq!(
|
||||
body["error"]["context"],
|
||||
json!({
|
||||
"security_level": "elevated",
|
||||
"edition": "community",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
#[serial]
|
||||
async fn manages_workspace_secrets_without_exposing_plaintext() {
|
||||
@@ -3069,6 +3293,7 @@ mod tests {
|
||||
display_name: "Create Lead".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Rest,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Rest(RestTarget {
|
||||
base_url: base_url.to_owned(),
|
||||
method: HttpMethod::Post,
|
||||
@@ -3122,6 +3347,7 @@ mod tests {
|
||||
display_name: "Create Lead GraphQL".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Graphql,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Graphql(GraphqlTarget {
|
||||
endpoint: endpoint.to_owned(),
|
||||
operation_type: GraphqlOperationType::Mutation,
|
||||
@@ -3178,6 +3404,7 @@ mod tests {
|
||||
display_name: "Unary Echo gRPC".to_owned(),
|
||||
category: "support".to_owned(),
|
||||
protocol: Protocol::Grpc,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Grpc(GrpcTarget {
|
||||
server_addr: server_addr.to_owned(),
|
||||
package: "echo".to_owned(),
|
||||
@@ -3325,6 +3552,7 @@ mod tests {
|
||||
display_name: "Create Lead SOAP".to_owned(),
|
||||
category: "sales".to_owned(),
|
||||
protocol: Protocol::Soap,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Soap(SoapTarget {
|
||||
wsdl_ref: "sample_wsdl".into(),
|
||||
service_name: "LeadService".to_owned(),
|
||||
@@ -3383,6 +3611,61 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn test_websocket_operation_payload(url: &str, name: &str) -> OperationPayload {
|
||||
OperationPayload {
|
||||
name: name.to_owned(),
|
||||
display_name: "Telemetry WebSocket".to_owned(),
|
||||
category: "telemetry".to_owned(),
|
||||
protocol: Protocol::Websocket,
|
||||
security_level: OperationSecurityLevel::Standard,
|
||||
target: Target::Websocket(WebsocketTarget {
|
||||
url: url.to_owned(),
|
||||
subprotocols: Vec::new(),
|
||||
subscribe_message_template: Some(json!({ "subscribe": true })),
|
||||
unsubscribe_message_template: None,
|
||||
static_headers: BTreeMap::new(),
|
||||
}),
|
||||
input_schema: object_schema("channel"),
|
||||
output_schema: object_schema("event"),
|
||||
input_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.mcp.channel".to_owned(),
|
||||
target: "$.request.body.channel".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
output_mapping: MappingSet {
|
||||
rules: vec![MappingRule {
|
||||
source: "$.response.body.event".to_owned(),
|
||||
target: "$.output.event".to_owned(),
|
||||
required: true,
|
||||
default_value: None,
|
||||
transform: None,
|
||||
condition: None,
|
||||
notes: None,
|
||||
}],
|
||||
},
|
||||
execution_config: ExecutionConfig {
|
||||
timeout_ms: 1_000,
|
||||
retry_policy: None,
|
||||
auth_profile_ref: None,
|
||||
headers: BTreeMap::new(),
|
||||
protocol_options: None,
|
||||
streaming: None,
|
||||
},
|
||||
tool_description: ToolDescription {
|
||||
title: "Telemetry WebSocket".to_owned(),
|
||||
description: "Streams telemetry events".to_owned(),
|
||||
tags: vec!["telemetry".to_owned(), "websocket".to_owned()],
|
||||
examples: Vec::new(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const SOAP_TEST_WSDL: &str = r#"<?xml version="1.0"?>
|
||||
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/"
|
||||
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
||||
|
||||
Reference in New Issue
Block a user