use crate::agent::aws::{
create_s3_scoped_user, delete_s3_scoped_user, generate_temporary_s3_credentials,
IamCredentials, StsCredentials,
};
use crate::agent::ns::auth_ns;
use crate::config::CONFIG;
use crate::models::{V1ResourceMeta, V1UserProfile};
use crate::state::AppState;
use aws_config::{self, BehaviorVersion, Region};
use aws_sdk_iam::Client as IamClient;
use axum::{
extract::{Extension, Path, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use serde::Serialize;
use serde_json::json;
use tracing::{debug, error};
#[derive(Serialize)]
pub struct V1IamCredentialsResponse {
kind: String,
metadata: V1ResourceMeta,
username: String,
access_key_id: String,
secret_access_key: String,
base_key: String,
}
#[derive(Serialize)]
pub struct V1StsCredentialsResponse {
kind: String,
metadata: V1ResourceMeta,
access_key_id: String,
secret_access_key: String,
session_token: String,
expiration: Option<i64>,
s3_base_uri: String,
}
pub async fn create_scoped_s3_token(
State(state): State<AppState>,
Extension(user_profile): Extension<V1UserProfile>,
Path((namespace, name)): Path<(String, String)>,
) -> Result<Json<V1IamCredentialsResponse>, (StatusCode, Json<serde_json::Value>)> {
debug!(?namespace, ?name, "Entered create_scoped_s3_token handler");
let db_pool = &state.db_pool;
debug!("Starting authorization step");
let mut owner_ids: Vec<String> = if let Some(orgs) = &user_profile.organizations {
orgs.keys().cloned().collect()
} else {
Vec::new()
};
if let Some(handle) = &user_profile.handle {
owner_ids.push(handle.clone());
debug!("Ensuring namespace: {}", handle);
match crate::handlers::v1::namespaces::ensure_namespace(
db_pool,
&handle,
&user_profile.email,
&user_profile.email,
None,
)
.await
{
Ok(_) => (),
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({ "error": format!("Invalid namespace: {}", e) })),
));
}
}
}
owner_ids.push(user_profile.email.clone());
debug!(?owner_ids, "Constructed owner_ids for authorization check");
debug!("Calling auth_ns");
let owner = match auth_ns(db_pool, &owner_ids, &namespace).await {
Ok(owner) => {
debug!(?owner, "auth_ns successful");
owner
}
Err(e) => {
error!("Authorization failed for namespace {}: {}", namespace, e);
debug!("Returning 403 Forbidden due to auth_ns failure");
return Err((
StatusCode::FORBIDDEN,
Json(json!({"error": format!("Not authorized for namespace '{}'", namespace)})),
));
}
};
let bucket_name = CONFIG.bucket_name.clone();
debug!(?bucket_name, "Retrieved bucket name from config");
debug!("Calling create_s3_scoped_user");
let credentials = match create_s3_scoped_user(&bucket_name, &namespace, &name).await {
Ok(creds) => {
debug!("create_s3_scoped_user successful");
creds
}
Err(e) => {
error!(
"Failed to create S3 scoped user '{}/{}': {}",
namespace, name, e
);
debug!("Returning 500 Internal Server Error due to create_s3_scoped_user failure");
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Failed to create AWS IAM user: {}", e)
})),
));
}
};
debug!("Formatting successful response");
let response = V1IamCredentialsResponse {
kind: "IamCredentials".to_string(),
metadata: V1ResourceMeta {
id: credentials.username.clone(),
name: name.clone(),
namespace: namespace.clone(),
owner: owner.clone(),
owner_ref: None,
created_by: user_profile.email,
labels: None,
created_at: chrono::Utc::now().timestamp(),
updated_at: chrono::Utc::now().timestamp(),
},
username: credentials.username.clone(),
access_key_id: credentials.access_key_id,
secret_access_key: credentials.secret_access_key,
base_key: format!("s3://{}/data/{}", bucket_name, namespace),
};
debug!("Returning Ok response");
Ok(Json(response))
}
pub async fn delete_scoped_s3_token(
State(state): State<AppState>,
Extension(user_profile): Extension<V1UserProfile>,
Path((namespace, name)): Path<(String, String)>,
) -> Result<impl IntoResponse, (StatusCode, Json<serde_json::Value>)> {
let db_pool = &state.db_pool;
let mut owner_ids: Vec<String> = if let Some(orgs) = &user_profile.organizations {
orgs.keys().cloned().collect()
} else {
Vec::new()
};
if let Some(handle) = &user_profile.handle {
owner_ids.push(handle.clone());
}
owner_ids.push(user_profile.email.clone());
match auth_ns(db_pool, &owner_ids, &namespace).await {
Ok(_) => (),
Err(e) => {
error!(
"Authorization failed for delete request on namespace {}: {}",
namespace, e
);
return Err((
StatusCode::FORBIDDEN,
Json(json!({"error": format!("Not authorized for namespace '{}'", namespace)})),
));
}
};
let config = aws_config::defaults(BehaviorVersion::latest())
.region(Region::new("us-east-1"))
.load()
.await;
let client = IamClient::new(&config);
match delete_s3_scoped_user(&namespace, &name).await {
Ok(_) => {
Ok(StatusCode::NO_CONTENT)
}
Err(e) => {
error!(
"Failed to delete S3 scoped user '{}/{}': {}",
namespace, name, e
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Failed to delete AWS IAM user: {}", e)
})),
));
}
}
}
pub async fn generate_temp_s3_credentials(
State(state): State<AppState>,
Extension(user_profile): Extension<V1UserProfile>,
Path((namespace, name)): Path<(String, String)>,
) -> Result<Json<V1StsCredentialsResponse>, (StatusCode, Json<serde_json::Value>)> {
debug!(
?namespace,
?name,
"Entered generate_temp_s3_credentials handler"
);
let db_pool = &state.db_pool;
let mut owner_ids: Vec<String> = if let Some(orgs) = &user_profile.organizations {
orgs.keys().cloned().collect()
} else {
Vec::new()
};
if let Some(handle) = &user_profile.handle {
owner_ids.push(handle.clone());
debug!("Ensuring namespace: {}", handle);
match crate::handlers::v1::namespaces::ensure_namespace(
db_pool,
&handle,
&user_profile.email,
&user_profile.email,
None,
)
.await
{
Ok(_) => (),
Err(e) => {
return Err((
StatusCode::BAD_REQUEST,
Json(json!({ "error": format!("Invalid namespace: {}", e) })),
));
}
}
}
owner_ids.push(user_profile.email.clone());
let owner = match auth_ns(db_pool, &owner_ids, &namespace).await {
Ok(owner) => owner,
Err(e) => {
error!("Authorization failed for namespace {}: {}", namespace, e);
return Err((
StatusCode::FORBIDDEN,
Json(json!({"error": format!("Not authorized for namespace '{}'", namespace)})),
));
}
};
let bucket_name = CONFIG.bucket_name.clone();
let duration_seconds = 3600;
let credentials =
match generate_temporary_s3_credentials(&bucket_name, &namespace, duration_seconds).await {
Ok(creds) => creds,
Err(e) => {
error!(
"Failed to generate temporary S3 credentials '{}/{}': {}",
namespace, name, e
);
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Failed to generate temporary AWS credentials: {}", e)
})),
));
}
};
let expiration_timestamp = credentials.expiration.map(|dt| dt.as_secs_f64() as i64);
let response = V1StsCredentialsResponse {
kind: "StsCredentials".to_string(),
metadata: V1ResourceMeta {
id: format!("sts-{}-{}", namespace, name),
name: name.clone(),
namespace: namespace.clone(),
owner: owner.clone(),
owner_ref: None,
created_by: user_profile.email,
labels: None,
created_at: chrono::Utc::now().timestamp(),
updated_at: chrono::Utc::now().timestamp(),
},
access_key_id: credentials.access_key_id,
secret_access_key: credentials.secret_access_key,
session_token: credentials.session_token,
expiration: expiration_timestamp,
s3_base_uri: format!("s3://{}/data/{}", bucket_name, namespace),
};
Ok(Json(response))
}