use async_trait::async_trait;
use serde::Deserialize;
use crate::connector::Connector;
use crate::models::{NewCredential, ProviderKind, RotationError, RotationStyle, Secret};
pub struct CloudflareConnector {
client: reqwest::Client,
token: String,
}
impl CloudflareConnector {
pub fn new(token: String) -> Self {
Self {
client: reqwest::Client::new(),
token,
}
}
fn url(&self, path: &str) -> String {
format!("https://api.cloudflare.com/client/v4{path}")
}
async fn err_body(resp: reqwest::Response) -> RotationError {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
RotationError::Http(format!("{status}: {body}"))
}
}
#[async_trait]
impl Connector for CloudflareConnector {
fn kind(&self) -> ProviderKind {
ProviderKind::Cloudflare
}
fn rotation_style(&self) -> RotationStyle {
RotationStyle::AtomicRotate
}
async fn connect(&self) -> Result<(), RotationError> {
let resp = self
.client
.get(self.url("/user/tokens/verify"))
.bearer_auth(&self.token)
.send()
.await
.map_err(|e| RotationError::Http(e.to_string()))?;
if resp.status().is_success() {
Ok(())
} else {
Err(RotationError::NotAuthorized(
Self::err_body(resp).await.to_string(),
))
}
}
async fn list_keys(&self) -> Result<Vec<Secret>, RotationError> {
let resp = self
.client
.get(self.url("/user/tokens"))
.bearer_auth(&self.token)
.send()
.await
.map_err(|e| RotationError::Http(e.to_string()))?;
if !resp.status().is_success() {
return Err(Self::err_body(resp).await);
}
let parsed: CfList = resp
.json()
.await
.map_err(|e| RotationError::Http(format!("parse tokens: {e}")))?;
Ok(parsed
.result
.into_iter()
.map(|t| Secret::new(ProviderKind::Cloudflare, t.id.clone(), t.name, t.id))
.collect())
}
async fn create(&self, secret: &Secret) -> Result<NewCredential, RotationError> {
let resp = self
.client
.put(self.url(&format!("/user/tokens/{}/value", secret.external_id)))
.bearer_auth(&self.token)
.json(&serde_json::json!({}))
.send()
.await
.map_err(|e| RotationError::Http(e.to_string()))?;
if !resp.status().is_success() {
return Err(Self::err_body(resp).await);
}
let rolled: CfRoll = resp
.json()
.await
.map_err(|e| RotationError::Http(format!("parse roll: {e}")))?;
Ok(NewCredential::new(
secret.external_id.clone(),
rolled.result,
))
}
async fn verify(&self, cred: &NewCredential) -> Result<(), RotationError> {
let resp = self
.client
.get(self.url("/user/tokens/verify"))
.bearer_auth(&cred.secret_value)
.send()
.await
.map_err(|e| RotationError::Http(e.to_string()))?;
if resp.status().is_success() {
Ok(())
} else {
Err(RotationError::VerificationFailed(
Self::err_body(resp).await.to_string(),
))
}
}
async fn current_key_id(&self) -> Option<String> {
let resp = self
.client
.get(self.url("/user/tokens/verify"))
.bearer_auth(&self.token)
.send()
.await
.ok()?;
if !resp.status().is_success() {
return None;
}
let v: serde_json::Value = resp.json().await.ok()?;
v["result"]["id"].as_str().map(str::to_string)
}
}
#[derive(Deserialize)]
struct CfList {
result: Vec<CfToken>,
}
#[derive(Deserialize)]
struct CfToken {
id: String,
name: String,
}
#[derive(Deserialize)]
struct CfRoll {
result: String,
}