728 lines
24 KiB
Rust
728 lines
24 KiB
Rust
//! Rule management API routes
|
|
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
http::StatusCode,
|
|
response::IntoResponse,
|
|
routing::{get, post},
|
|
Json, Router,
|
|
};
|
|
use std::sync::Arc;
|
|
use tracing::{info, warn};
|
|
use validator::Validate;
|
|
|
|
use attune_common::mq::{
|
|
MessageEnvelope, MessageType, RuleCreatedPayload, RuleDisabledPayload, RuleEnabledPayload,
|
|
};
|
|
use attune_common::rbac::{Action, AuthorizationContext, Resource};
|
|
use attune_common::repositories::{
|
|
action::ActionRepository,
|
|
pack::PackRepository,
|
|
rule::{CreateRuleInput, RuleRepository, RuleSearchFilters, UpdateRuleInput},
|
|
trigger::TriggerRepository,
|
|
Create, Delete, FindByRef, Patch, Update,
|
|
};
|
|
|
|
use crate::{
|
|
auth::middleware::RequireAuth,
|
|
authz::{AuthorizationCheck, AuthorizationService},
|
|
dto::{
|
|
common::{PaginatedResponse, PaginationParams},
|
|
rule::{CreateRuleRequest, RuleResponse, RuleSummary, UpdateRuleRequest},
|
|
ApiResponse, SuccessResponse,
|
|
},
|
|
middleware::{ApiError, ApiResult},
|
|
state::AppState,
|
|
validation::{validate_action_params, validate_trigger_params},
|
|
};
|
|
|
|
/// List all rules with pagination
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/rules",
|
|
tag = "rules",
|
|
params(PaginationParams),
|
|
responses(
|
|
(status = 200, description = "List of rules", body = PaginatedResponse<RuleSummary>),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn list_rules(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(_user): RequireAuth,
|
|
Query(pagination): Query<PaginationParams>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
let filters = RuleSearchFilters {
|
|
pack: None,
|
|
action: None,
|
|
trigger: None,
|
|
enabled: None,
|
|
limit: pagination.limit(),
|
|
offset: pagination.offset(),
|
|
};
|
|
|
|
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
|
|
|
let paginated_rules: Vec<RuleSummary> =
|
|
result.rows.into_iter().map(RuleSummary::from).collect();
|
|
|
|
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// List enabled rules
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/rules/enabled",
|
|
tag = "rules",
|
|
params(PaginationParams),
|
|
responses(
|
|
(status = 200, description = "List of enabled rules", body = PaginatedResponse<RuleSummary>),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn list_enabled_rules(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(_user): RequireAuth,
|
|
Query(pagination): Query<PaginationParams>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
let filters = RuleSearchFilters {
|
|
pack: None,
|
|
action: None,
|
|
trigger: None,
|
|
enabled: Some(true),
|
|
limit: pagination.limit(),
|
|
offset: pagination.offset(),
|
|
};
|
|
|
|
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
|
|
|
let paginated_rules: Vec<RuleSummary> =
|
|
result.rows.into_iter().map(RuleSummary::from).collect();
|
|
|
|
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// List rules by pack reference
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/packs/{pack_ref}/rules",
|
|
tag = "rules",
|
|
params(
|
|
("pack_ref" = String, Path, description = "Pack reference"),
|
|
PaginationParams
|
|
),
|
|
responses(
|
|
(status = 200, description = "List of rules in pack", body = PaginatedResponse<RuleSummary>),
|
|
(status = 404, description = "Pack not found"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn list_rules_by_pack(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(_user): RequireAuth,
|
|
Path(pack_ref): Path<String>,
|
|
Query(pagination): Query<PaginationParams>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
// Verify pack exists
|
|
let pack = PackRepository::find_by_ref(&state.db, &pack_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", pack_ref)))?;
|
|
|
|
let filters = RuleSearchFilters {
|
|
pack: Some(pack.id),
|
|
action: None,
|
|
trigger: None,
|
|
enabled: None,
|
|
limit: pagination.limit(),
|
|
offset: pagination.offset(),
|
|
};
|
|
|
|
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
|
|
|
let paginated_rules: Vec<RuleSummary> =
|
|
result.rows.into_iter().map(RuleSummary::from).collect();
|
|
|
|
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// List rules by action reference
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/actions/{action_ref}/rules",
|
|
tag = "rules",
|
|
params(
|
|
("action_ref" = String, Path, description = "Action reference"),
|
|
PaginationParams
|
|
),
|
|
responses(
|
|
(status = 200, description = "List of rules using this action", body = PaginatedResponse<RuleSummary>),
|
|
(status = 404, description = "Action not found"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn list_rules_by_action(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(_user): RequireAuth,
|
|
Path(action_ref): Path<String>,
|
|
Query(pagination): Query<PaginationParams>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
// Verify action exists
|
|
let action = ActionRepository::find_by_ref(&state.db, &action_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Action '{}' not found", action_ref)))?;
|
|
|
|
let filters = RuleSearchFilters {
|
|
pack: None,
|
|
action: Some(action.id),
|
|
trigger: None,
|
|
enabled: None,
|
|
limit: pagination.limit(),
|
|
offset: pagination.offset(),
|
|
};
|
|
|
|
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
|
|
|
let paginated_rules: Vec<RuleSummary> =
|
|
result.rows.into_iter().map(RuleSummary::from).collect();
|
|
|
|
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// List rules by trigger reference
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/triggers/{trigger_ref}/rules",
|
|
tag = "rules",
|
|
params(
|
|
("trigger_ref" = String, Path, description = "Trigger reference"),
|
|
PaginationParams
|
|
),
|
|
responses(
|
|
(status = 200, description = "List of rules using this trigger", body = PaginatedResponse<RuleSummary>),
|
|
(status = 404, description = "Trigger not found"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn list_rules_by_trigger(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(_user): RequireAuth,
|
|
Path(trigger_ref): Path<String>,
|
|
Query(pagination): Query<PaginationParams>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
// Verify trigger exists
|
|
let trigger = TriggerRepository::find_by_ref(&state.db, &trigger_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Trigger '{}' not found", trigger_ref)))?;
|
|
|
|
let filters = RuleSearchFilters {
|
|
pack: None,
|
|
action: None,
|
|
trigger: Some(trigger.id),
|
|
enabled: None,
|
|
limit: pagination.limit(),
|
|
offset: pagination.offset(),
|
|
};
|
|
|
|
let result = RuleRepository::list_search(&state.db, &filters).await?;
|
|
|
|
let paginated_rules: Vec<RuleSummary> =
|
|
result.rows.into_iter().map(RuleSummary::from).collect();
|
|
|
|
let response = PaginatedResponse::new(paginated_rules, &pagination, result.total);
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// Get a single rule by reference
|
|
#[utoipa::path(
|
|
get,
|
|
path = "/api/v1/rules/{ref}",
|
|
tag = "rules",
|
|
params(
|
|
("ref" = String, Path, description = "Rule reference")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Rule details", body = ApiResponse<RuleResponse>),
|
|
(status = 404, description = "Rule not found"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn get_rule(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(_user): RequireAuth,
|
|
Path(rule_ref): Path<String>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
let rule = RuleRepository::find_by_ref(&state.db, &rule_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Rule '{}' not found", rule_ref)))?;
|
|
|
|
let response = ApiResponse::new(RuleResponse::from(rule));
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// Create a new rule
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/v1/rules",
|
|
tag = "rules",
|
|
request_body = CreateRuleRequest,
|
|
responses(
|
|
(status = 201, description = "Rule created successfully", body = ApiResponse<RuleResponse>),
|
|
(status = 400, description = "Invalid request"),
|
|
(status = 404, description = "Pack, action, or trigger not found"),
|
|
(status = 409, description = "Rule with same ref already exists"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn create_rule(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(user): RequireAuth,
|
|
Json(request): Json<CreateRuleRequest>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
// Validate request
|
|
request.validate()?;
|
|
|
|
// Check if rule with same ref already exists
|
|
if RuleRepository::find_by_ref(&state.db, &request.r#ref)
|
|
.await?
|
|
.is_some()
|
|
{
|
|
return Err(ApiError::Conflict(format!(
|
|
"Rule with ref '{}' already exists",
|
|
request.r#ref
|
|
)));
|
|
}
|
|
|
|
// Verify pack exists and get its ID
|
|
let pack = PackRepository::find_by_ref(&state.db, &request.pack_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Pack '{}' not found", request.pack_ref)))?;
|
|
|
|
// Verify action exists and get its ID
|
|
let action = ActionRepository::find_by_ref(&state.db, &request.action_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Action '{}' not found", request.action_ref)))?;
|
|
|
|
// Verify trigger exists and get its ID
|
|
let trigger = TriggerRepository::find_by_ref(&state.db, &request.trigger_ref)
|
|
.await?
|
|
.ok_or_else(|| {
|
|
ApiError::NotFound(format!("Trigger '{}' not found", request.trigger_ref))
|
|
})?;
|
|
|
|
if user.claims.token_type == crate::auth::jwt::TokenType::Access {
|
|
let identity_id = user
|
|
.identity_id()
|
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
|
let authz = AuthorizationService::new(state.db.clone());
|
|
let mut ctx = AuthorizationContext::new(identity_id);
|
|
ctx.pack_ref = Some(pack.r#ref.clone());
|
|
ctx.target_ref = Some(request.r#ref.clone());
|
|
authz
|
|
.authorize(
|
|
&user,
|
|
AuthorizationCheck {
|
|
resource: Resource::Rules,
|
|
action: Action::Create,
|
|
context: ctx,
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
// Validate trigger parameters against schema
|
|
validate_trigger_params(&trigger, &request.trigger_params)?;
|
|
|
|
// Validate action parameters against schema
|
|
validate_action_params(&action, &request.action_params)?;
|
|
|
|
// Create rule input
|
|
let rule_input = CreateRuleInput {
|
|
r#ref: request.r#ref,
|
|
pack: pack.id,
|
|
pack_ref: pack.r#ref.clone(),
|
|
label: request.label,
|
|
description: request.description,
|
|
action: action.id,
|
|
action_ref: action.r#ref.clone(),
|
|
trigger: trigger.id,
|
|
trigger_ref: trigger.r#ref.clone(),
|
|
conditions: request.conditions,
|
|
action_params: request.action_params,
|
|
trigger_params: request.trigger_params,
|
|
enabled: request.enabled,
|
|
is_adhoc: true, // Rules created via API are ad-hoc (not from pack installation)
|
|
};
|
|
|
|
let rule = RuleRepository::create(&state.db, rule_input).await?;
|
|
|
|
// Publish RuleCreated message to notify sensor service
|
|
if let Some(publisher) = state.get_publisher().await {
|
|
let payload = RuleCreatedPayload {
|
|
rule_id: rule.id,
|
|
rule_ref: rule.r#ref.clone(),
|
|
trigger_id: rule.trigger,
|
|
trigger_ref: rule.trigger_ref.clone(),
|
|
action_id: rule.action,
|
|
action_ref: rule.action_ref.clone(),
|
|
trigger_params: Some(rule.trigger_params.clone()),
|
|
enabled: rule.enabled,
|
|
};
|
|
|
|
let envelope =
|
|
MessageEnvelope::new(MessageType::RuleCreated, payload).with_source("api-service");
|
|
|
|
if let Err(e) = publisher.publish_envelope(&envelope).await {
|
|
warn!(
|
|
"Failed to publish RuleCreated message for rule {}: {}",
|
|
rule.r#ref, e
|
|
);
|
|
} else {
|
|
info!("Published RuleCreated message for rule {}", rule.r#ref);
|
|
}
|
|
}
|
|
|
|
let response = ApiResponse::with_message(RuleResponse::from(rule), "Rule created successfully");
|
|
|
|
Ok((StatusCode::CREATED, Json(response)))
|
|
}
|
|
|
|
/// Update an existing rule
|
|
#[utoipa::path(
|
|
put,
|
|
path = "/api/v1/rules/{ref}",
|
|
tag = "rules",
|
|
params(
|
|
("ref" = String, Path, description = "Rule reference")
|
|
),
|
|
request_body = UpdateRuleRequest,
|
|
responses(
|
|
(status = 200, description = "Rule updated successfully", body = ApiResponse<RuleResponse>),
|
|
(status = 400, description = "Invalid request"),
|
|
(status = 404, description = "Rule not found"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn update_rule(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(user): RequireAuth,
|
|
Path(rule_ref): Path<String>,
|
|
Json(request): Json<UpdateRuleRequest>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
// Validate request
|
|
request.validate()?;
|
|
|
|
// Check if rule exists
|
|
let existing_rule = RuleRepository::find_by_ref(&state.db, &rule_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Rule '{}' not found", rule_ref)))?;
|
|
|
|
if user.claims.token_type == crate::auth::jwt::TokenType::Access {
|
|
let identity_id = user
|
|
.identity_id()
|
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
|
let authz = AuthorizationService::new(state.db.clone());
|
|
let mut ctx = AuthorizationContext::new(identity_id);
|
|
ctx.target_id = Some(existing_rule.id);
|
|
ctx.target_ref = Some(existing_rule.r#ref.clone());
|
|
ctx.pack_ref = Some(existing_rule.pack_ref.clone());
|
|
authz
|
|
.authorize(
|
|
&user,
|
|
AuthorizationCheck {
|
|
resource: Resource::Rules,
|
|
action: Action::Update,
|
|
context: ctx,
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
// If action parameters are being updated, validate against the action's schema
|
|
if let Some(ref action_params) = request.action_params {
|
|
let action = ActionRepository::find_by_ref(&state.db, &existing_rule.action_ref)
|
|
.await?
|
|
.ok_or_else(|| {
|
|
ApiError::NotFound(format!("Action '{}' not found", existing_rule.action_ref))
|
|
})?;
|
|
validate_action_params(&action, action_params)?;
|
|
}
|
|
|
|
// If trigger parameters are being updated, validate against the trigger's schema
|
|
if let Some(ref trigger_params) = request.trigger_params {
|
|
let trigger = TriggerRepository::find_by_ref(&state.db, &existing_rule.trigger_ref)
|
|
.await?
|
|
.ok_or_else(|| {
|
|
ApiError::NotFound(format!("Trigger '{}' not found", existing_rule.trigger_ref))
|
|
})?;
|
|
validate_trigger_params(&trigger, trigger_params)?;
|
|
}
|
|
|
|
// Track if trigger params changed
|
|
let trigger_params_changed = request.trigger_params.is_some()
|
|
&& request.trigger_params != Some(existing_rule.trigger_params.clone());
|
|
|
|
// Create update input
|
|
let update_input = UpdateRuleInput {
|
|
label: request.label,
|
|
description: request.description.map(Patch::Set),
|
|
conditions: request.conditions,
|
|
action_params: request.action_params,
|
|
trigger_params: request.trigger_params,
|
|
enabled: request.enabled,
|
|
};
|
|
|
|
let rule = RuleRepository::update(&state.db, existing_rule.id, update_input).await?;
|
|
|
|
// If the rule is enabled and trigger params changed, publish RuleEnabled message
|
|
// to notify sensors to restart with new parameters
|
|
if rule.enabled && trigger_params_changed {
|
|
if let Some(publisher) = state.get_publisher().await {
|
|
let payload = RuleEnabledPayload {
|
|
rule_id: rule.id,
|
|
rule_ref: rule.r#ref.clone(),
|
|
trigger_ref: rule.trigger_ref.clone(),
|
|
trigger_params: Some(rule.trigger_params.clone()),
|
|
};
|
|
|
|
let envelope =
|
|
MessageEnvelope::new(MessageType::RuleEnabled, payload).with_source("api-service");
|
|
|
|
if let Err(e) = publisher.publish_envelope(&envelope).await {
|
|
warn!(
|
|
"Failed to publish RuleEnabled message for updated rule {}: {}",
|
|
rule.r#ref, e
|
|
);
|
|
} else {
|
|
info!(
|
|
"Published RuleEnabled message for updated rule {} (trigger params changed)",
|
|
rule.r#ref
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
let response = ApiResponse::with_message(RuleResponse::from(rule), "Rule updated successfully");
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// Delete a rule
|
|
#[utoipa::path(
|
|
delete,
|
|
path = "/api/v1/rules/{ref}",
|
|
tag = "rules",
|
|
params(
|
|
("ref" = String, Path, description = "Rule reference")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Rule deleted successfully", body = SuccessResponse),
|
|
(status = 404, description = "Rule not found"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn delete_rule(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(user): RequireAuth,
|
|
Path(rule_ref): Path<String>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
// Check if rule exists
|
|
let rule = RuleRepository::find_by_ref(&state.db, &rule_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Rule '{}' not found", rule_ref)))?;
|
|
|
|
if user.claims.token_type == crate::auth::jwt::TokenType::Access {
|
|
let identity_id = user
|
|
.identity_id()
|
|
.map_err(|_| ApiError::Unauthorized("Invalid user identity".to_string()))?;
|
|
let authz = AuthorizationService::new(state.db.clone());
|
|
let mut ctx = AuthorizationContext::new(identity_id);
|
|
ctx.target_id = Some(rule.id);
|
|
ctx.target_ref = Some(rule.r#ref.clone());
|
|
ctx.pack_ref = Some(rule.pack_ref.clone());
|
|
authz
|
|
.authorize(
|
|
&user,
|
|
AuthorizationCheck {
|
|
resource: Resource::Rules,
|
|
action: Action::Delete,
|
|
context: ctx,
|
|
},
|
|
)
|
|
.await?;
|
|
}
|
|
|
|
// Delete the rule
|
|
let deleted = RuleRepository::delete(&state.db, rule.id).await?;
|
|
|
|
if !deleted {
|
|
return Err(ApiError::NotFound(format!("Rule '{}' not found", rule_ref)));
|
|
}
|
|
|
|
let response = SuccessResponse::new(format!("Rule '{}' deleted successfully", rule_ref));
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// Enable a rule
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/v1/rules/{ref}/enable",
|
|
tag = "rules",
|
|
params(
|
|
("ref" = String, Path, description = "Rule reference")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Rule enabled successfully", body = ApiResponse<RuleResponse>),
|
|
(status = 404, description = "Rule not found"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn enable_rule(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(_user): RequireAuth,
|
|
Path(rule_ref): Path<String>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
// Check if rule exists
|
|
let existing_rule = RuleRepository::find_by_ref(&state.db, &rule_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Rule '{}' not found", rule_ref)))?;
|
|
|
|
// Update rule to enabled
|
|
let update_input = UpdateRuleInput {
|
|
label: None,
|
|
description: None,
|
|
conditions: None,
|
|
action_params: None,
|
|
trigger_params: None,
|
|
enabled: Some(true),
|
|
};
|
|
|
|
let rule = RuleRepository::update(&state.db, existing_rule.id, update_input).await?;
|
|
|
|
// Publish RuleEnabled message to notify sensor service
|
|
if let Some(publisher) = state.get_publisher().await {
|
|
let payload = RuleEnabledPayload {
|
|
rule_id: rule.id,
|
|
rule_ref: rule.r#ref.clone(),
|
|
trigger_ref: rule.trigger_ref.clone(),
|
|
trigger_params: Some(rule.trigger_params.clone()),
|
|
};
|
|
|
|
let envelope =
|
|
MessageEnvelope::new(MessageType::RuleEnabled, payload).with_source("api-service");
|
|
|
|
if let Err(e) = publisher.publish_envelope(&envelope).await {
|
|
warn!(
|
|
"Failed to publish RuleEnabled message for rule {}: {}",
|
|
rule.r#ref, e
|
|
);
|
|
} else {
|
|
info!("Published RuleEnabled message for rule {}", rule.r#ref);
|
|
}
|
|
}
|
|
|
|
let response = ApiResponse::with_message(RuleResponse::from(rule), "Rule enabled successfully");
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// Disable a rule
|
|
#[utoipa::path(
|
|
post,
|
|
path = "/api/v1/rules/{ref}/disable",
|
|
tag = "rules",
|
|
params(
|
|
("ref" = String, Path, description = "Rule reference")
|
|
),
|
|
responses(
|
|
(status = 200, description = "Rule disabled successfully", body = ApiResponse<RuleResponse>),
|
|
(status = 404, description = "Rule not found"),
|
|
(status = 500, description = "Internal server error")
|
|
)
|
|
)]
|
|
pub async fn disable_rule(
|
|
State(state): State<Arc<AppState>>,
|
|
RequireAuth(_user): RequireAuth,
|
|
Path(rule_ref): Path<String>,
|
|
) -> ApiResult<impl IntoResponse> {
|
|
// Check if rule exists
|
|
let existing_rule = RuleRepository::find_by_ref(&state.db, &rule_ref)
|
|
.await?
|
|
.ok_or_else(|| ApiError::NotFound(format!("Rule '{}' not found", rule_ref)))?;
|
|
|
|
// Update rule to disabled
|
|
let update_input = UpdateRuleInput {
|
|
label: None,
|
|
description: None,
|
|
conditions: None,
|
|
action_params: None,
|
|
trigger_params: None,
|
|
enabled: Some(false),
|
|
};
|
|
|
|
let rule = RuleRepository::update(&state.db, existing_rule.id, update_input).await?;
|
|
|
|
// Publish RuleDisabled message to notify sensor service
|
|
if let Some(publisher) = state.get_publisher().await {
|
|
let payload = RuleDisabledPayload {
|
|
rule_id: rule.id,
|
|
rule_ref: rule.r#ref.clone(),
|
|
trigger_ref: rule.trigger_ref.clone(),
|
|
};
|
|
|
|
let envelope =
|
|
MessageEnvelope::new(MessageType::RuleDisabled, payload).with_source("api-service");
|
|
|
|
if let Err(e) = publisher.publish_envelope(&envelope).await {
|
|
warn!(
|
|
"Failed to publish RuleDisabled message for rule {}: {}",
|
|
rule.r#ref, e
|
|
);
|
|
} else {
|
|
info!("Published RuleDisabled message for rule {}", rule.r#ref);
|
|
}
|
|
}
|
|
|
|
let response =
|
|
ApiResponse::with_message(RuleResponse::from(rule), "Rule disabled successfully");
|
|
|
|
Ok((StatusCode::OK, Json(response)))
|
|
}
|
|
|
|
/// Create rule routes
|
|
pub fn routes() -> Router<Arc<AppState>> {
|
|
Router::new()
|
|
.route("/rules", get(list_rules).post(create_rule))
|
|
.route("/rules/enabled", get(list_enabled_rules))
|
|
.route(
|
|
"/rules/{ref}",
|
|
get(get_rule).put(update_rule).delete(delete_rule),
|
|
)
|
|
.route("/rules/{ref}/enable", post(enable_rule))
|
|
.route("/rules/{ref}/disable", post(disable_rule))
|
|
.route("/packs/{pack_ref}/rules", get(list_rules_by_pack))
|
|
.route("/actions/{action_ref}/rules", get(list_rules_by_action))
|
|
.route("/triggers/{trigger_ref}/rules", get(list_rules_by_trigger))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_rule_routes_structure() {
|
|
// Just verify the router can be constructed
|
|
let _router = routes();
|
|
}
|
|
}
|