use serde_json::{json, Value};
use crate::apierror::{is_resource_not_found, raise_api_error};
use crate::error::SailError;
use crate::http::{HttpCore, IdempotencyKey, Method, RequestSpec};
use crate::retry::{RetryPolicy, DEFAULT_RETRY_POLICY, NO_RETRY};
use super::types::{
validate_policy_name, validate_rules, validate_secret_name, validate_secret_value,
CredentialInjectionPolicyInfo, CredentialInjectionPolicyPage, InjectionRule,
ListCredentialInjectionPoliciesQuery, SecretInfo,
};
pub(crate) struct CredentialApi<'a> {
http: &'a HttpCore,
}
impl<'a> CredentialApi<'a> {
pub(crate) fn new(http: &'a HttpCore) -> CredentialApi<'a> {
CredentialApi { http }
}
pub(crate) async fn set_secret(
&self,
name: &str,
value: &str,
) -> Result<SecretInfo, SailError> {
validate_secret_name(name)?;
validate_secret_value(value)?;
let (status, data) = self
.request(
Method::Put,
&format!("/v1/secrets/{name}"),
&[],
Some(&json!({ "value": value })),
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, &format!("Secret {name:?}"))?;
secret_from(data)
}
pub(crate) async fn get_secret(&self, name: &str) -> Result<SecretInfo, SailError> {
validate_secret_name(name)?;
let (status, data) = self
.request(
Method::Get,
&format!("/v1/secrets/{name}"),
&[],
None,
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, &format!("Secret {name:?}"))?;
secret_from(data)
}
pub(crate) async fn list_secrets(&self) -> Result<Vec<SecretInfo>, SailError> {
let (status, data) = self
.request(
Method::Get,
"/v1/secrets",
&[],
None,
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, "")?;
let rows = data.get("data").cloned().unwrap_or(Value::Null);
serde_json::from_value(rows).map_err(|e| SailError::Internal {
message: format!("failed to parse secrets: {e}"),
})
}
pub(crate) async fn delete_secret(&self, name: &str) -> Result<(), SailError> {
validate_secret_name(name)?;
let (status, data) = self
.request(
Method::Delete,
&format!("/v1/secrets/{name}"),
&[],
None,
NO_RETRY,
)
.await?;
raise_api_error(status, &data, &format!("Secret {name:?}"))
}
pub(crate) async fn create_policy(
&self,
name: &str,
rules: &[InjectionRule],
) -> Result<CredentialInjectionPolicyInfo, SailError> {
validate_policy_name(name)?;
validate_rules(rules)?;
let (status, data) = self
.request(
Method::Post,
"/v1/credential-injection-policies",
&[],
Some(&json!({ "name": name, "rules": rules })),
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, "")?;
policy_from(data)
}
pub(crate) async fn get_policy(
&self,
policy_id: &str,
) -> Result<CredentialInjectionPolicyInfo, SailError> {
require_policy_id(policy_id)?;
let (status, data) = self
.request(
Method::Get,
&format!("/v1/credential-injection-policies/{policy_id}"),
&[],
None,
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, &format!("Credential policy {policy_id:?}"))?;
policy_from(data)
}
pub(crate) async fn list_policies(
&self,
query: &ListCredentialInjectionPoliciesQuery,
) -> Result<CredentialInjectionPolicyPage, SailError> {
let mut params: Vec<(String, String)> = vec![
("limit".to_string(), query.limit.to_string()),
("offset".to_string(), query.offset.to_string()),
];
if let Some(search) = &query.search {
params.push(("search".to_string(), search.clone()));
}
let (status, data) = self
.request(
Method::Get,
"/v1/credential-injection-policies",
¶ms,
None,
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, "")?;
serde_json::from_value(data).map_err(|e| SailError::Internal {
message: format!("failed to parse credential policy page: {e}"),
})
}
pub(crate) async fn rename_policy(
&self,
policy_id: &str,
name: &str,
) -> Result<CredentialInjectionPolicyInfo, SailError> {
require_policy_id(policy_id)?;
validate_policy_name(name)?;
let (status, data) = self
.request(
Method::Patch,
&format!("/v1/credential-injection-policies/{policy_id}"),
&[],
Some(&json!({ "name": name })),
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, &format!("Credential policy {policy_id:?}"))?;
policy_from(data)
}
pub(crate) async fn delete_policy(&self, policy_id: &str) -> Result<(), SailError> {
require_policy_id(policy_id)?;
let (status, data) = self
.request(
Method::Delete,
&format!("/v1/credential-injection-policies/{policy_id}"),
&[],
None,
NO_RETRY,
)
.await?;
raise_api_error(status, &data, &format!("Credential policy {policy_id:?}"))
}
pub(crate) async fn sailbox_policy(
&self,
sailbox_id: &str,
) -> Result<Option<CredentialInjectionPolicyInfo>, SailError> {
let (status, data) = self
.request(
Method::Get,
&format!("/v1/sailboxes/{sailbox_id}/credential-injection-policy"),
&[],
None,
DEFAULT_RETRY_POLICY,
)
.await?;
if status == 404 && is_resource_not_found(&data) {
return Ok(None);
}
raise_api_error(status, &data, &format!("Sailbox {sailbox_id:?}"))?;
policy_from(data).map(Some)
}
pub(crate) async fn attach_sailbox_policy(
&self,
sailbox_id: &str,
policy_id: &str,
) -> Result<(), SailError> {
require_policy_id(policy_id)?;
let (status, data) = self
.request(
Method::Put,
&format!("/v1/sailboxes/{sailbox_id}/credential-injection-policy"),
&[],
Some(&json!({ "policy_id": policy_id })),
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, &format!("Sailbox {sailbox_id:?}"))
}
pub(crate) async fn detach_sailbox_policy(&self, sailbox_id: &str) -> Result<(), SailError> {
let (status, data) = self
.request(
Method::Delete,
&format!("/v1/sailboxes/{sailbox_id}/credential-injection-policy"),
&[],
None,
DEFAULT_RETRY_POLICY,
)
.await?;
raise_api_error(status, &data, &format!("Sailbox {sailbox_id:?}"))
}
async fn request(
&self,
method: Method,
path: &str,
query: &[(String, String)],
body: Option<&Value>,
policy: RetryPolicy,
) -> Result<(u16, Value), SailError> {
let body = body
.map(|value| {
serde_json::to_vec(value).map_err(|e| SailError::Internal {
message: format!("failed to serialize request body: {e}"),
})
})
.transpose()?;
let spec = RequestSpec {
method,
path: path.to_string(),
query: query.to_vec(),
body,
extra_headers: Vec::new(),
timeout: None,
policy,
idempotency_key: IdempotencyKey::Auto,
};
self.http.request(&spec).await
}
}
fn require_policy_id(policy_id: &str) -> Result<(), SailError> {
if policy_id.trim().is_empty() {
return Err(SailError::InvalidArgument {
message: "policy_id is required".to_string(),
});
}
Ok(())
}
fn secret_from(data: Value) -> Result<SecretInfo, SailError> {
serde_json::from_value(data).map_err(|e| SailError::Internal {
message: format!("failed to parse secret: {e}"),
})
}
fn policy_from(data: Value) -> Result<CredentialInjectionPolicyInfo, SailError> {
serde_json::from_value(data).map_err(|e| SailError::Internal {
message: format!("failed to parse credential policy: {e}"),
})
}