Add operation approval policy editor

This commit is contained in:
github-ops
2026-06-24 10:45:09 +00:00
parent 5922aea68f
commit d83ab541d9
20 changed files with 451 additions and 11 deletions
+1
View File
@@ -175,6 +175,7 @@ mod tests {
response_cache: None,
idempotency: None,
safety: None,
approval_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
},
+4 -1
View File
@@ -38,7 +38,8 @@ mod workspaces;
use crate::{auth::AuthSettings, error::ApiError, storage::LocalArtifactStorage};
use operation_validation::{
validate_idempotency_policy, validate_protocol_target, validate_response_cache_policy,
validate_approval_policy, validate_idempotency_policy, validate_protocol_target,
validate_response_cache_policy,
};
#[derive(Clone)]
@@ -298,6 +299,7 @@ impl AdminService {
validate_protocol_target(payload.protocol, &payload.target)?;
validate_response_cache_policy(&payload.target, &payload.execution_config)?;
validate_idempotency_policy(&payload.target, &payload.execution_config)?;
validate_approval_policy(&payload.execution_config)?;
payload.input_mapping.validate_paths()?;
payload.output_mapping.validate_paths()?;
Ok(())
@@ -308,6 +310,7 @@ impl AdminService {
validate_protocol_target(operation.protocol, &operation.target)?;
validate_response_cache_policy(&operation.target, &operation.execution_config)?;
validate_idempotency_policy(&operation.target, &operation.execution_config)?;
validate_approval_policy(&operation.execution_config)?;
operation.input_mapping.validate_paths()?;
operation.output_mapping.validate_paths()?;
Ok(())
+1
View File
@@ -387,6 +387,7 @@ fn demo_rest_operation_payload() -> OperationPayload {
response_cache: None,
idempotency: None,
safety: None,
approval_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
},
+1
View File
@@ -191,6 +191,7 @@ impl AdminService {
response_cache: None,
idempotency: None,
safety: None,
approval_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
},
@@ -105,16 +105,60 @@ pub(super) fn validate_idempotency_policy(
Ok(())
}
pub(super) fn validate_approval_policy(
execution_config: &crank_core::ExecutionConfig,
) -> Result<(), ApiError> {
let Some(policy) = execution_config.approval_policy.as_ref() else {
return Ok(());
};
if !policy.required {
return Ok(());
}
if policy.ttl_seconds == 0 || policy.ttl_seconds > 300 {
return Err(ApiError::validation_with_context(
"approval ttl must be between 1 and 300 seconds".to_owned(),
json!({
"field": "execution_config.approval_policy.ttl_seconds",
}),
));
}
if policy.confirmation_title.trim().is_empty() {
return Err(ApiError::validation_with_context(
"approval confirmation title is required".to_owned(),
json!({
"field": "execution_config.approval_policy.confirmation_title",
}),
));
}
if policy.confirmation_body_template.trim().is_empty() {
return Err(ApiError::validation_with_context(
"approval confirmation body is required".to_owned(),
json!({
"field": "execution_config.approval_policy.confirmation_body_template",
}),
));
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use crank_core::{
ExecutionConfig, HttpMethod, IdempotencyMode, IdempotencyPolicy, ResponseCachePolicy,
RestTarget, Target,
ExecutionConfig, HttpMethod, IdempotencyMode, IdempotencyPolicy,
OperationApprovalPayloadPreviewMode, OperationApprovalPolicy, OperationApprovalRiskLevel,
ResponseCachePolicy, RestTarget, Target,
};
use super::{validate_idempotency_policy, validate_response_cache_policy};
use super::{
validate_approval_policy, validate_idempotency_policy, validate_response_cache_policy,
};
fn cacheable_execution_config() -> ExecutionConfig {
ExecutionConfig {
@@ -123,6 +167,7 @@ mod tests {
response_cache: Some(ResponseCachePolicy { ttl_ms: 5_000 }),
idempotency: None,
safety: None,
approval_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
}
@@ -140,6 +185,7 @@ mod tests {
header_name: Some("Idempotency-Key".to_owned()),
}),
safety: None,
approval_policy: None,
auth_profile_ref: None,
headers: BTreeMap::new(),
}
@@ -238,4 +284,42 @@ mod tests {
"required idempotency needs input_field or header_name"
);
}
#[test]
fn accepts_valid_approval_policy() {
let mut config = cacheable_execution_config();
config.approval_policy = Some(OperationApprovalPolicy {
required: true,
risk_level: OperationApprovalRiskLevel::Dangerous,
confirmation_title: "Подтвердите действие".to_owned(),
confirmation_body_template: "Выполнить действие?".to_owned(),
ttl_seconds: 300,
show_payload_preview: true,
payload_preview_mode: OperationApprovalPayloadPreviewMode::MaskedJson,
});
validate_approval_policy(&config).unwrap();
}
#[test]
fn rejects_invalid_approval_policy() {
let mut config = cacheable_execution_config();
config.approval_policy = Some(OperationApprovalPolicy {
required: true,
risk_level: OperationApprovalRiskLevel::Dangerous,
confirmation_title: "".to_owned(),
confirmation_body_template: "Выполнить действие?".to_owned(),
ttl_seconds: 0,
show_payload_preview: true,
payload_preview_mode: OperationApprovalPayloadPreviewMode::MaskedJson,
});
let error = validate_approval_policy(&config).unwrap_err();
assert!(matches!(error, crate::error::ApiError::Validation { .. }));
assert_eq!(
error.to_string(),
"approval ttl must be between 1 and 300 seconds"
);
}
}