re-uploading work
This commit is contained in:
363
crates/api/src/routes/keys.rs
Normal file
363
crates/api/src/routes/keys.rs
Normal file
@@ -0,0 +1,363 @@
|
||||
//! Key/Secret management API routes
|
||||
|
||||
use axum::{
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Json, Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
use attune_common::repositories::{
|
||||
key::{CreateKeyInput, KeyRepository, UpdateKeyInput},
|
||||
Create, Delete, List, Update,
|
||||
};
|
||||
|
||||
use crate::auth::RequireAuth;
|
||||
use crate::{
|
||||
dto::{
|
||||
common::{PaginatedResponse, PaginationParams},
|
||||
key::{CreateKeyRequest, KeyQueryParams, KeyResponse, KeySummary, UpdateKeyRequest},
|
||||
ApiResponse, SuccessResponse,
|
||||
},
|
||||
middleware::{ApiError, ApiResult},
|
||||
state::AppState,
|
||||
};
|
||||
|
||||
/// List all keys with pagination and optional filters (values redacted)
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/keys",
|
||||
tag = "secrets",
|
||||
params(KeyQueryParams),
|
||||
responses(
|
||||
(status = 200, description = "List of keys (values redacted)", body = PaginatedResponse<KeySummary>),
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn list_keys(
|
||||
_user: RequireAuth,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Query(query): Query<KeyQueryParams>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Get keys based on filters
|
||||
let keys = if let Some(owner_type) = query.owner_type {
|
||||
// Filter by owner type
|
||||
KeyRepository::find_by_owner_type(&state.db, owner_type).await?
|
||||
} else {
|
||||
// Get all keys
|
||||
KeyRepository::list(&state.db).await?
|
||||
};
|
||||
|
||||
// Apply additional filters in memory
|
||||
let mut filtered_keys = keys;
|
||||
|
||||
if let Some(owner) = &query.owner {
|
||||
filtered_keys.retain(|k| k.owner.as_ref() == Some(owner));
|
||||
}
|
||||
|
||||
// Calculate pagination
|
||||
let total = filtered_keys.len() as u64;
|
||||
let start = query.offset() as usize;
|
||||
let end = (start + query.limit() as usize).min(filtered_keys.len());
|
||||
|
||||
// Get paginated slice (values redacted in summary)
|
||||
let paginated_keys: Vec<KeySummary> = filtered_keys[start..end]
|
||||
.iter()
|
||||
.map(|key| KeySummary::from(key.clone()))
|
||||
.collect();
|
||||
|
||||
// Convert query params to pagination params for response
|
||||
let pagination_params = PaginationParams {
|
||||
page: query.page,
|
||||
page_size: query.per_page,
|
||||
};
|
||||
|
||||
let response = PaginatedResponse::new(paginated_keys, &pagination_params, total);
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
/// Get a single key by reference (includes decrypted value)
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/api/v1/keys/{ref}",
|
||||
tag = "secrets",
|
||||
params(
|
||||
("ref" = String, Path, description = "Key reference identifier")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Key details with decrypted value", body = inline(ApiResponse<KeyResponse>)),
|
||||
(status = 404, description = "Key not found")
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn get_key(
|
||||
_user: RequireAuth,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(key_ref): Path<String>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
let mut key = KeyRepository::find_by_ref(&state.db, &key_ref)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Key '{}' not found", key_ref)))?;
|
||||
|
||||
// Decrypt value if encrypted
|
||||
if key.encrypted {
|
||||
let encryption_key = state
|
||||
.config
|
||||
.security
|
||||
.encryption_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
ApiError::InternalServerError("Encryption key not configured on server".to_string())
|
||||
})?;
|
||||
|
||||
let decrypted_value =
|
||||
attune_common::crypto::decrypt(&key.value, encryption_key).map_err(|e| {
|
||||
tracing::error!("Failed to decrypt key '{}': {}", key_ref, e);
|
||||
ApiError::InternalServerError(format!("Failed to decrypt key: {}", e))
|
||||
})?;
|
||||
|
||||
key.value = decrypted_value;
|
||||
}
|
||||
|
||||
let response = ApiResponse::new(KeyResponse::from(key));
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
/// Create a new key/secret
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/api/v1/keys",
|
||||
tag = "secrets",
|
||||
request_body = CreateKeyRequest,
|
||||
responses(
|
||||
(status = 201, description = "Key created successfully", body = inline(ApiResponse<KeyResponse>)),
|
||||
(status = 400, description = "Validation error"),
|
||||
(status = 409, description = "Key with same ref already exists")
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn create_key(
|
||||
_user: RequireAuth,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(request): Json<CreateKeyRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Validate request
|
||||
request.validate()?;
|
||||
|
||||
// Check if key with same ref already exists
|
||||
if let Some(_) = KeyRepository::find_by_ref(&state.db, &request.r#ref).await? {
|
||||
return Err(ApiError::Conflict(format!(
|
||||
"Key with ref '{}' already exists",
|
||||
request.r#ref
|
||||
)));
|
||||
}
|
||||
|
||||
// Encrypt value if requested
|
||||
let (value, encryption_key_hash) = if request.encrypted {
|
||||
let encryption_key = state
|
||||
.config
|
||||
.security
|
||||
.encryption_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
ApiError::BadRequest(
|
||||
"Cannot encrypt: encryption key not configured on server".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let encrypted_value = attune_common::crypto::encrypt(&request.value, encryption_key)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to encrypt key value: {}", e);
|
||||
ApiError::InternalServerError(format!("Failed to encrypt value: {}", e))
|
||||
})?;
|
||||
|
||||
let key_hash = attune_common::crypto::hash_encryption_key(encryption_key);
|
||||
|
||||
(encrypted_value, Some(key_hash))
|
||||
} else {
|
||||
// Store in plaintext (not recommended for sensitive data)
|
||||
(request.value.clone(), None)
|
||||
};
|
||||
|
||||
// Create key input
|
||||
let key_input = CreateKeyInput {
|
||||
r#ref: request.r#ref,
|
||||
owner_type: request.owner_type,
|
||||
owner: request.owner,
|
||||
owner_identity: request.owner_identity,
|
||||
owner_pack: request.owner_pack,
|
||||
owner_pack_ref: request.owner_pack_ref,
|
||||
owner_action: request.owner_action,
|
||||
owner_action_ref: request.owner_action_ref,
|
||||
owner_sensor: request.owner_sensor,
|
||||
owner_sensor_ref: request.owner_sensor_ref,
|
||||
name: request.name,
|
||||
encrypted: request.encrypted,
|
||||
encryption_key_hash,
|
||||
value,
|
||||
};
|
||||
|
||||
let mut key = KeyRepository::create(&state.db, key_input).await?;
|
||||
|
||||
// Return decrypted value in response
|
||||
if key.encrypted {
|
||||
let encryption_key = state.config.security.encryption_key.as_ref().unwrap();
|
||||
key.value = attune_common::crypto::decrypt(&key.value, encryption_key).map_err(|e| {
|
||||
tracing::error!("Failed to decrypt newly created key: {}", e);
|
||||
ApiError::InternalServerError(format!("Failed to decrypt value: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
let response = ApiResponse::with_message(KeyResponse::from(key), "Key created successfully");
|
||||
|
||||
Ok((StatusCode::CREATED, Json(response)))
|
||||
}
|
||||
|
||||
/// Update an existing key/secret
|
||||
#[utoipa::path(
|
||||
put,
|
||||
path = "/api/v1/keys/{ref}",
|
||||
tag = "secrets",
|
||||
params(
|
||||
("ref" = String, Path, description = "Key reference identifier")
|
||||
),
|
||||
request_body = UpdateKeyRequest,
|
||||
responses(
|
||||
(status = 200, description = "Key updated successfully", body = inline(ApiResponse<KeyResponse>)),
|
||||
(status = 400, description = "Validation error"),
|
||||
(status = 404, description = "Key not found")
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn update_key(
|
||||
_user: RequireAuth,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(key_ref): Path<String>,
|
||||
Json(request): Json<UpdateKeyRequest>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Validate request
|
||||
request.validate()?;
|
||||
|
||||
// Verify key exists
|
||||
let existing = KeyRepository::find_by_ref(&state.db, &key_ref)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Key '{}' not found", key_ref)))?;
|
||||
|
||||
// Handle value update with encryption
|
||||
let (value, encrypted, encryption_key_hash) = if let Some(new_value) = request.value {
|
||||
let should_encrypt = request.encrypted.unwrap_or(existing.encrypted);
|
||||
|
||||
if should_encrypt {
|
||||
let encryption_key =
|
||||
state
|
||||
.config
|
||||
.security
|
||||
.encryption_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
ApiError::BadRequest(
|
||||
"Cannot encrypt: encryption key not configured on server".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
let encrypted_value = attune_common::crypto::encrypt(&new_value, encryption_key)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to encrypt key value: {}", e);
|
||||
ApiError::InternalServerError(format!("Failed to encrypt value: {}", e))
|
||||
})?;
|
||||
|
||||
let key_hash = attune_common::crypto::hash_encryption_key(encryption_key);
|
||||
|
||||
(Some(encrypted_value), Some(should_encrypt), Some(key_hash))
|
||||
} else {
|
||||
(Some(new_value), Some(false), None)
|
||||
}
|
||||
} else {
|
||||
// No value update, but might be changing encryption status
|
||||
(None, request.encrypted, None)
|
||||
};
|
||||
|
||||
// Create update input
|
||||
let update_input = UpdateKeyInput {
|
||||
name: request.name,
|
||||
value,
|
||||
encrypted,
|
||||
encryption_key_hash,
|
||||
};
|
||||
|
||||
let mut updated_key = KeyRepository::update(&state.db, existing.id, update_input).await?;
|
||||
|
||||
// Return decrypted value in response
|
||||
if updated_key.encrypted {
|
||||
let encryption_key = state
|
||||
.config
|
||||
.security
|
||||
.encryption_key
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
ApiError::InternalServerError("Encryption key not configured on server".to_string())
|
||||
})?;
|
||||
|
||||
updated_key.value = attune_common::crypto::decrypt(&updated_key.value, encryption_key)
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to decrypt updated key '{}': {}", key_ref, e);
|
||||
ApiError::InternalServerError(format!("Failed to decrypt value: {}", e))
|
||||
})?;
|
||||
}
|
||||
|
||||
let response =
|
||||
ApiResponse::with_message(KeyResponse::from(updated_key), "Key updated successfully");
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
/// Delete a key/secret
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/api/v1/keys/{ref}",
|
||||
tag = "secrets",
|
||||
params(
|
||||
("ref" = String, Path, description = "Key reference identifier")
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Key deleted successfully", body = SuccessResponse),
|
||||
(status = 404, description = "Key not found")
|
||||
),
|
||||
security(("bearer_auth" = []))
|
||||
)]
|
||||
pub async fn delete_key(
|
||||
_user: RequireAuth,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Path(key_ref): Path<String>,
|
||||
) -> ApiResult<impl IntoResponse> {
|
||||
// Verify key exists
|
||||
let key = KeyRepository::find_by_ref(&state.db, &key_ref)
|
||||
.await?
|
||||
.ok_or_else(|| ApiError::NotFound(format!("Key '{}' not found", key_ref)))?;
|
||||
|
||||
// Delete the key
|
||||
let deleted = KeyRepository::delete(&state.db, key.id).await?;
|
||||
|
||||
if !deleted {
|
||||
return Err(ApiError::NotFound(format!("Key '{}' not found", key_ref)));
|
||||
}
|
||||
|
||||
let response = SuccessResponse::new("Key deleted successfully");
|
||||
|
||||
Ok((StatusCode::OK, Json(response)))
|
||||
}
|
||||
|
||||
/// Register key/secret routes
|
||||
pub fn routes() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/keys", get(list_keys).post(create_key))
|
||||
.route(
|
||||
"/keys/{ref}",
|
||||
get(get_key).put(update_key).delete(delete_key),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user