use axum::Json;
use axum::extract::{Query, State};
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
use crate::server::AppState;
use crate::server::credentials::{CredError, CredSource, MaybeCreds};
use crate::storage::build_credentialed_client;
#[derive(Deserialize)]
pub struct CheckParams {
pub bucket: Option<String>,
}
#[derive(Serialize)]
pub struct CheckResponse {
pub ok: bool,
pub region: Option<String>,
pub error: Option<String>,
}
pub async fn check_credentials(
State(state): State<AppState>,
creds: MaybeCreds,
Query(params): Query<CheckParams>,
) -> Result<Json<CheckResponse>, (StatusCode, String)> {
if !state.allow_byo_creds {
return Err((
StatusCode::BAD_REQUEST,
"this server does not accept user credentials".to_string(),
));
}
let temp = match creds.0 {
Ok(CredSource::Static(temp)) => temp,
Ok(CredSource::AssumeRole { role_arn, region }) => {
state.assume(&role_arn, region.as_deref()).await?
}
Ok(CredSource::Default) => {
return Err((
StatusCode::BAD_REQUEST,
"credentials required: supply x-dial9-aws-* headers or a role ARN".to_string(),
));
}
Err(
e @ (CredError::Incomplete
| CredError::Malformed
| CredError::InvalidRegion
| CredError::ConflictingCredentials
| CredError::InvalidRoleArn),
) => {
return Err((StatusCode::BAD_REQUEST, e.message().to_string()));
}
};
let bucket = match params.bucket.or_else(|| state.default_bucket.clone()) {
Some(b) => b,
None => {
return Err((StatusCode::BAD_REQUEST, "bucket is required".to_string()));
}
};
let client = build_credentialed_client(
temp.credentials,
temp.region.as_deref(),
&state.ephemeral_s3,
);
match client.head_bucket().bucket(&bucket).send().await {
Ok(resp) => Ok(Json(CheckResponse {
ok: true,
region: resp.bucket_region().map(|r| r.to_string()).or(temp.region),
error: None,
})),
Err(err) => {
let raw = err.raw_response();
let status = raw.map(|r| r.status().as_u16());
let redirect_region = raw.and_then(|r| {
r.headers()
.get("x-amz-bucket-region")
.map(|v| v.to_string())
});
if let (Some(301), Some(region)) = (status, redirect_region) {
return Ok(Json(CheckResponse {
ok: true,
region: Some(region),
error: None,
}));
}
Ok(Json(CheckResponse {
ok: false,
region: None,
error: Some(classify_check_failure(&err)),
}))
}
}
}
fn classify_check_failure(
err: &aws_sdk_s3::error::SdkError<
aws_sdk_s3::operation::head_bucket::HeadBucketError,
aws_sdk_s3::config::http::HttpResponse,
>,
) -> String {
use aws_sdk_s3::error::ProvideErrorMetadata;
match err.code() {
Some("InvalidAccessKeyId" | "UnrecognizedClientException" | "InvalidClientTokenId") => {
return "access key id not recognized".to_string();
}
Some("SignatureDoesNotMatch") => return "secret access key is incorrect".to_string(),
Some("ExpiredToken" | "ExpiredTokenException" | "InvalidToken") => {
return "session token is invalid or expired".to_string();
}
Some("AccessDenied" | "AccessDeniedException" | "Forbidden") => {
return "access denied for this bucket".to_string();
}
Some("NoSuchBucket" | "NotFound") => return "bucket not found".to_string(),
_ => {}
}
match err.raw_response().map(|r| r.status().as_u16()) {
Some(401 | 403) => {
"credentials rejected or access denied (check keys, token, or permissions)".to_string()
}
Some(404) => "bucket not found".to_string(),
_ => "could not access bucket with these credentials".to_string(),
}
}