Усилить безопасность и надёжность выполнения операций
CI / Rust Checks (push) Successful in 5m7s
CI / UI Checks (push) Successful in 4s
CI / Deployment Manifests (push) Successful in 3s
CI / Frontend E2E (push) Successful in 3m9s
CI / Deploy (push) Successful in 1m41s

This commit is contained in:
2026-07-11 14:08:07 +03:00
parent 626f2845e2
commit 8318e4b560
40 changed files with 1343 additions and 185 deletions
+4 -2
View File
@@ -10,7 +10,7 @@ use crank_community_auth::PasswordIdentityProvider;
use crank_registry::{PostgresPoolConfig, PostgresRegistry};
use crank_runtime::{
RequestRateLimitConfig, RequestRateLimiter, RuntimeCacheConfig, RuntimeCacheStores,
RuntimeLimits, SecretCrypto, community_default,
RuntimeLimits, SecretCrypto,
};
use sqlx::postgres::PgConnectOptions;
use tokio::net::TcpListener;
@@ -56,7 +56,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let cache_stores = RuntimeCacheStores::from_config(&cache_config).await?;
let api_rate_limit = admin_api_rate_limit_config_from_env()?;
let secret_crypto = SecretCrypto::new(&env::var("CRANK_MASTER_KEY")?)?;
let runtime = community_default()
let outbound_http_policy = crank_runtime::OutboundHttpPolicy::from_env()?;
let runtime = crank_runtime::community_with_outbound_policy(outbound_http_policy.clone())
.with_limits(runtime_limits)
.with_response_cache(cache_stores.response.clone())
.with_coordination_store(cache_stores.coordination.clone())
@@ -70,6 +71,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
secret_crypto,
runtime,
)
.with_outbound_http_policy(outbound_http_policy)
.with_identity_provider(std::sync::Arc::new(identity_provider))
.build();
service.bootstrap_admin_user().await?;
+27 -3
View File
@@ -15,7 +15,9 @@ use crank_registry::{
AgentSummary, CreateInvocationLogRequest, OperationAgentRef, OperationSummary,
OperationUsageSummary, PostgresRegistry, RegistryOperation, UsageBucket,
};
use crank_runtime::{PreparedRequest, ResolvedAuth, RuntimeError, RuntimeExecutor, SecretCrypto};
use crank_runtime::{
OutboundHttpPolicy, PreparedRequest, ResolvedAuth, RuntimeError, RuntimeExecutor, SecretCrypto,
};
use crank_schema::{Schema, SchemaKind};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
@@ -38,8 +40,8 @@ mod workspaces;
use crate::{auth::AuthSettings, error::ApiError, storage::LocalArtifactStorage};
use operation_validation::{
validate_approval_policy, validate_idempotency_policy, validate_protocol_target,
validate_response_cache_policy,
validate_approval_policy, validate_execution_timeout, validate_idempotency_policy,
validate_protocol_target, validate_response_cache_policy,
};
#[derive(Clone)]
@@ -53,6 +55,7 @@ pub struct AdminService {
policy_engine: Arc<dyn PolicyEngine>,
audit_sink: Arc<dyn AuditSink>,
capability_profile: Arc<dyn CapabilityProfile>,
outbound_http_policy: OutboundHttpPolicy,
}
pub struct AdminServiceBuilder {
@@ -65,6 +68,7 @@ pub struct AdminServiceBuilder {
policy_engine: Option<Arc<dyn PolicyEngine>>,
audit_sink: Option<Arc<dyn AuditSink>>,
capability_profile: Option<Arc<dyn CapabilityProfile>>,
outbound_http_policy: OutboundHttpPolicy,
}
pub use crate::dto::*;
@@ -139,6 +143,7 @@ impl AdminServiceBuilder {
policy_engine: None,
audit_sink: None,
capability_profile: None,
outbound_http_policy: OutboundHttpPolicy::default(),
}
}
@@ -147,6 +152,11 @@ impl AdminServiceBuilder {
self
}
pub fn with_outbound_http_policy(mut self, policy: OutboundHttpPolicy) -> Self {
self.outbound_http_policy = policy;
self
}
#[allow(dead_code)]
pub fn with_policy_engine(mut self, policy_engine: Arc<dyn PolicyEngine>) -> Self {
self.policy_engine = Some(policy_engine);
@@ -183,6 +193,7 @@ impl AdminServiceBuilder {
capability_profile: self
.capability_profile
.unwrap_or_else(|| Arc::new(CommunityCapabilityProfile)),
outbound_http_policy: self.outbound_http_policy,
}
}
}
@@ -297,6 +308,8 @@ impl AdminService {
fn validate_operation_payload(&self, payload: &OperationPayload) -> Result<(), ApiError> {
self.validate_operation_capabilities(payload.protocol, payload.security_level)?;
validate_protocol_target(payload.protocol, &payload.target)?;
self.validate_outbound_target(&payload.target)?;
validate_execution_timeout(&payload.execution_config)?;
validate_response_cache_policy(&payload.target, &payload.execution_config)?;
validate_idempotency_policy(&payload.target, &payload.execution_config)?;
validate_approval_policy(&payload.execution_config)?;
@@ -308,6 +321,8 @@ impl AdminService {
fn validate_registry_operation(&self, operation: &RegistryOperation) -> Result<(), ApiError> {
self.validate_operation_capabilities(operation.protocol, operation.security_level)?;
validate_protocol_target(operation.protocol, &operation.target)?;
self.validate_outbound_target(&operation.target)?;
validate_execution_timeout(&operation.execution_config)?;
validate_response_cache_policy(&operation.target, &operation.execution_config)?;
validate_idempotency_policy(&operation.target, &operation.execution_config)?;
validate_approval_policy(&operation.execution_config)?;
@@ -316,6 +331,15 @@ impl AdminService {
Ok(())
}
fn validate_outbound_target(&self, target: &crank_core::Target) -> Result<(), ApiError> {
match target {
crank_core::Target::Rest(rest) => self
.outbound_http_policy
.validate_base_url(&rest.base_url)
.map_err(|error| ApiError::validation(error.to_string())),
}
}
fn validate_operation_capabilities(
&self,
protocol: Protocol,
@@ -3,6 +3,8 @@ use serde_json::json;
use crate::error::ApiError;
const MAX_OPERATION_TIMEOUT_MS: u64 = 300_000;
pub(super) fn validate_protocol_target(
protocol: Protocol,
target: &Target,
@@ -16,6 +18,18 @@ pub(super) fn validate_protocol_target(
Err(ApiError::validation("protocol and target kind must match"))
}
pub(super) fn validate_execution_timeout(
execution_config: &crank_core::ExecutionConfig,
) -> Result<(), ApiError> {
if !(1..=MAX_OPERATION_TIMEOUT_MS).contains(&execution_config.timeout_ms) {
return Err(ApiError::validation_with_context(
format!("operation timeout must be between 1 and {MAX_OPERATION_TIMEOUT_MS} ms"),
json!({ "field": "execution_config.timeout_ms" }),
));
}
Ok(())
}
pub(super) fn validate_response_cache_policy(
target: &Target,
execution_config: &crank_core::ExecutionConfig,
@@ -150,7 +164,8 @@ mod tests {
};
use super::{
validate_approval_policy, validate_idempotency_policy, validate_response_cache_policy,
validate_approval_policy, validate_execution_timeout, validate_idempotency_policy,
validate_response_cache_policy,
};
fn cacheable_execution_config() -> ExecutionConfig {
@@ -198,6 +213,19 @@ mod tests {
assert!(result.is_ok());
}
#[test]
fn rejects_zero_and_excessive_execution_timeouts() {
let mut config = cacheable_execution_config();
config.timeout_ms = 0;
assert!(validate_execution_timeout(&config).is_err());
config.timeout_ms = 300_001;
assert!(validate_execution_timeout(&config).is_err());
config.timeout_ms = 300_000;
assert!(validate_execution_timeout(&config).is_ok());
}
#[test]
fn rejects_response_cache_for_non_get_rest_operation() {
let target = Target::Rest(RestTarget {
+4 -1
View File
@@ -114,13 +114,16 @@ pub(super) fn test_service(
auth_settings: AuthSettings,
secret_crypto: SecretCrypto,
) -> AdminService {
let outbound_policy = crank_runtime::OutboundHttpPolicy::allowing_hosts(["127.0.0.1"]);
let runtime = crank_runtime::community_with_outbound_policy(outbound_policy.clone()).build();
AdminServiceBuilder::new(
registry,
storage_root,
auth_settings,
secret_crypto,
crank_runtime::RuntimeExecutor::new(),
runtime,
)
.with_outbound_http_policy(outbound_policy)
.build()
}