Implement Phase 9: client libraries (Go, Rust, Lisp, Python)
- clients/README.md: canonical API surface and error type reference - clients/testdata/: shared JSON response fixtures - clients/go/: mciasgoclient package; net/http + TLS 1.2+; sync.RWMutex token state; DisallowUnknownFields on all decoders; 25 tests pass - clients/rust/: async mcias-client crate; reqwest+rustls (no OpenSSL); thiserror MciasError enum; Arc<RwLock> token state; 22+1 tests pass; cargo clippy -D warnings clean - clients/lisp/: ASDF mcias-client; dexador HTTP, yason JSON; mcias-error condition hierarchy; Hunchentoot mock-dispatcher; 37 fiveam checks pass on SBCL 2.6.1; yason boolean normalisation in validate-token - clients/python/: mcias_client package (Python 3.11+); httpx sync; py.typed; dataclasses; 32 pytest tests; mypy --strict + ruff clean - test/mock/mockserver.go: in-memory mock server for Go client tests - ARCHITECTURE.md §19: updated per-language notes to match implementation - PROGRESS.md: Phase 9 marked complete - .gitignore: exclude clients/rust/target/, python .venv, .pytest_cache, .fasl files Security: token never logged or exposed in error messages in any library; TLS enforced in all four languages; token stored under lock/mutex/RwLock
This commit is contained in:
1619
clients/rust/Cargo.lock
generated
Normal file
1619
clients/rust/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
clients/rust/Cargo.toml
Normal file
17
clients/rust/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "mcias-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Rust client library for the MCIAS identity and access management API"
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
|
||||
thiserror = "2"
|
||||
|
||||
[dev-dependencies]
|
||||
wiremock = "0.6"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "time"] }
|
||||
88
clients/rust/README.md
Normal file
88
clients/rust/README.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# mcias-client (Rust)
|
||||
|
||||
Async Rust client library for the [MCIAS](../../README.md) identity and access management API.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Rust 2021 edition (stable toolchain)
|
||||
- Tokio async runtime
|
||||
|
||||
## Installation
|
||||
|
||||
Add to `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
mcias-client = { path = "path/to/clients/rust" }
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```rust
|
||||
use mcias_client::{Client, ClientOptions};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let client = Client::new(
|
||||
"https://auth.example.com".to_string(),
|
||||
ClientOptions::default(),
|
||||
)?;
|
||||
|
||||
// Authenticate.
|
||||
let (token, expires_at) = client.login("alice", "s3cret", None).await?;
|
||||
println!("token expires at {expires_at}");
|
||||
|
||||
// The token is stored in the client automatically.
|
||||
let accounts = client.list_accounts().await?;
|
||||
|
||||
// Revoke the token when done.
|
||||
client.logout().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Custom CA Certificate
|
||||
|
||||
```rust
|
||||
let ca_pem = std::fs::read("/etc/mcias/ca.pem")?;
|
||||
let client = Client::new(
|
||||
"https://auth.example.com".to_string(),
|
||||
ClientOptions {
|
||||
ca_cert_pem: Some(ca_pem),
|
||||
token: None,
|
||||
},
|
||||
)?;
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
All methods return `Result<_, MciasError>`:
|
||||
|
||||
```rust
|
||||
use mcias_client::MciasError;
|
||||
|
||||
match client.login("alice", "wrongpass", None).await {
|
||||
Err(MciasError::Auth { message }) => eprintln!("auth failed: {message}"),
|
||||
Err(MciasError::Forbidden { message }) => eprintln!("forbidden: {message}"),
|
||||
Err(MciasError::NotFound { message }) => eprintln!("not found: {message}"),
|
||||
Err(MciasError::InvalidInput { message }) => eprintln!("bad input: {message}"),
|
||||
Err(MciasError::Conflict { message }) => eprintln!("conflict: {message}"),
|
||||
Err(MciasError::Server { status, message }) => eprintln!("server error {status}: {message}"),
|
||||
Err(MciasError::Transport(e)) => eprintln!("network error: {e}"),
|
||||
Err(MciasError::Decode(e)) => eprintln!("parse error: {e}"),
|
||||
Ok((token, _)) => println!("ok: {token}"),
|
||||
}
|
||||
```
|
||||
|
||||
## Thread Safety
|
||||
|
||||
`Client` is `Send + Sync`. The internal token is wrapped in
|
||||
`Arc<RwLock<Option<String>>>` for safe concurrent access.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```sh
|
||||
cargo test
|
||||
cargo clippy -- -D warnings
|
||||
```
|
||||
514
clients/rust/src/lib.rs
Normal file
514
clients/rust/src/lib.rs
Normal file
@@ -0,0 +1,514 @@
|
||||
//! # mcias-client
|
||||
//!
|
||||
//! Async Rust client for the MCIAS (Metacircular Identity and Access System)
|
||||
//! REST API.
|
||||
//!
|
||||
//! ## Usage
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use mcias_client::{Client, ClientOptions};
|
||||
//!
|
||||
//! #[tokio::main]
|
||||
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let client = Client::new("https://auth.example.com", ClientOptions::default())?;
|
||||
//!
|
||||
//! let (token, expires_at) = client.login("alice", "s3cret", None).await?;
|
||||
//! println!("Logged in, token expires at {expires_at}");
|
||||
//!
|
||||
//! client.logout().await?;
|
||||
//! Ok(())
|
||||
//! }
|
||||
//! ```
|
||||
//!
|
||||
//! ## Thread Safety
|
||||
//!
|
||||
//! [`Client`] is `Clone + Send + Sync`. The internally stored bearer token is
|
||||
//! protected by an `Arc<tokio::sync::RwLock<...>>` so concurrent async tasks
|
||||
//! may share a single client safely.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use reqwest::{header, Certificate, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
// ---- Error types ----
|
||||
|
||||
/// All errors returned by the MCIAS client.
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum MciasError {
|
||||
/// HTTP 401 — authentication required or credentials invalid.
|
||||
#[error("authentication error: {0}")]
|
||||
Auth(String),
|
||||
|
||||
/// HTTP 403 — caller lacks required role.
|
||||
#[error("permission denied: {0}")]
|
||||
Forbidden(String),
|
||||
|
||||
/// HTTP 404 — requested resource does not exist.
|
||||
#[error("not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
/// HTTP 400 — the request payload was invalid.
|
||||
#[error("invalid input: {0}")]
|
||||
InvalidInput(String),
|
||||
|
||||
/// HTTP 409 — resource conflict (e.g. duplicate username).
|
||||
#[error("conflict: {0}")]
|
||||
Conflict(String),
|
||||
|
||||
/// HTTP 5xx — the server returned an internal error.
|
||||
#[error("server error ({status}): {message}")]
|
||||
Server { status: u16, message: String },
|
||||
|
||||
/// Transport-level error (DNS failure, connection refused, timeout, etc.).
|
||||
#[error("transport error: {0}")]
|
||||
Transport(#[from] reqwest::Error),
|
||||
|
||||
/// Response body could not be decoded.
|
||||
#[error("decode error: {0}")]
|
||||
Decode(String),
|
||||
}
|
||||
|
||||
// ---- Data types ----
|
||||
|
||||
/// Account information returned by the server.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct Account {
|
||||
pub id: String,
|
||||
pub username: String,
|
||||
pub account_type: String,
|
||||
pub status: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub totp_enabled: bool,
|
||||
}
|
||||
|
||||
/// Result of a token validation request.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct TokenClaims {
|
||||
pub valid: bool,
|
||||
#[serde(default)]
|
||||
pub sub: String,
|
||||
#[serde(default)]
|
||||
pub roles: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub expires_at: String,
|
||||
}
|
||||
|
||||
/// The server's Ed25519 public key in JWK format.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PublicKey {
|
||||
pub kty: String,
|
||||
pub crv: String,
|
||||
pub x: String,
|
||||
}
|
||||
|
||||
/// Postgres credentials returned by the server.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct PgCreds {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub database: String,
|
||||
pub username: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
// ---- Internal request/response types ----
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct LoginRequest<'a> {
|
||||
username: &'a str,
|
||||
password: &'a str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
totp_code: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TokenResponse {
|
||||
token: String,
|
||||
expires_at: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ErrorResponse {
|
||||
#[serde(default)]
|
||||
error: String,
|
||||
}
|
||||
|
||||
// ---- Client options ----
|
||||
|
||||
/// Configuration options for the MCIAS client.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct ClientOptions {
|
||||
/// Optional PEM-encoded CA certificate for TLS verification.
|
||||
/// Use when connecting to a server with a self-signed or private-CA cert.
|
||||
pub ca_cert_pem: Option<Vec<u8>>,
|
||||
|
||||
/// Optional pre-existing bearer token.
|
||||
pub token: Option<String>,
|
||||
}
|
||||
|
||||
// ---- Client ----
|
||||
|
||||
/// Async MCIAS REST API client.
|
||||
///
|
||||
/// `Client` is cheaply cloneable — the internal HTTP client and token storage
|
||||
/// are reference-counted. All clones share the same token.
|
||||
#[derive(Clone)]
|
||||
pub struct Client {
|
||||
base_url: String,
|
||||
http: reqwest::Client,
|
||||
/// Bearer token storage. `Arc<RwLock<...>>` so clones share the token.
|
||||
token: Arc<RwLock<Option<String>>>,
|
||||
}
|
||||
|
||||
impl Client {
|
||||
/// Create a new client.
|
||||
///
|
||||
/// `base_url` must be an HTTPS URL (e.g. `"https://auth.example.com"`).
|
||||
/// TLS 1.2+ is enforced by the underlying `reqwest` / `rustls` stack.
|
||||
pub fn new(base_url: &str, opts: ClientOptions) -> Result<Self, MciasError> {
|
||||
let mut builder = reqwest::ClientBuilder::new()
|
||||
// Security: enforce TLS 1.2+ minimum.
|
||||
.min_tls_version(reqwest::tls::Version::TLS_1_2)
|
||||
.use_rustls_tls();
|
||||
|
||||
if let Some(pem) = opts.ca_cert_pem {
|
||||
let cert = Certificate::from_pem(&pem)
|
||||
.map_err(|e| MciasError::Decode(format!("parse CA cert: {e}")))?;
|
||||
builder = builder.add_root_certificate(cert);
|
||||
}
|
||||
|
||||
let http = builder.build()?;
|
||||
Ok(Self {
|
||||
base_url: base_url.trim_end_matches('/').to_owned(),
|
||||
http,
|
||||
token: Arc::new(RwLock::new(opts.token)),
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the currently stored bearer token, if any.
|
||||
pub async fn token(&self) -> Option<String> {
|
||||
self.token.read().await.clone()
|
||||
}
|
||||
|
||||
/// Replace the stored bearer token.
|
||||
pub async fn set_token(&self, tok: Option<String>) {
|
||||
*self.token.write().await = tok;
|
||||
}
|
||||
|
||||
// ---- Authentication ----
|
||||
|
||||
/// Login with username and password. On success stores the returned token
|
||||
/// and returns `(token, expires_at)`.
|
||||
///
|
||||
/// `totp_code` may be `None` when TOTP is not enrolled.
|
||||
pub async fn login(
|
||||
&self,
|
||||
username: &str,
|
||||
password: &str,
|
||||
totp_code: Option<&str>,
|
||||
) -> Result<(String, String), MciasError> {
|
||||
let body = LoginRequest {
|
||||
username,
|
||||
password,
|
||||
totp_code,
|
||||
};
|
||||
let resp: TokenResponse = self.post("/v1/auth/login", &body).await?;
|
||||
*self.token.write().await = Some(resp.token.clone());
|
||||
Ok((resp.token, resp.expires_at))
|
||||
}
|
||||
|
||||
/// Logout — revoke the current token on the server. Clears the stored token.
|
||||
pub async fn logout(&self) -> Result<(), MciasError> {
|
||||
self.post_empty("/v1/auth/logout").await?;
|
||||
*self.token.write().await = None;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Renew the current token. The old token is revoked server-side; the new
|
||||
/// token is stored and returned as `(token, expires_at)`.
|
||||
pub async fn renew_token(&self) -> Result<(String, String), MciasError> {
|
||||
let resp: TokenResponse = self.post("/v1/auth/renew", &serde_json::json!({})).await?;
|
||||
*self.token.write().await = Some(resp.token.clone());
|
||||
Ok((resp.token, resp.expires_at))
|
||||
}
|
||||
|
||||
/// Validate a token. Returns [`TokenClaims`] with `valid: false` (no error)
|
||||
/// if the token is invalid or revoked.
|
||||
pub async fn validate_token(&self, token: &str) -> Result<TokenClaims, MciasError> {
|
||||
let body = serde_json::json!({ "token": token });
|
||||
self.post("/v1/token/validate", &body).await
|
||||
}
|
||||
|
||||
// ---- Server information ----
|
||||
|
||||
/// Call the health endpoint. Returns `Ok(())` on HTTP 200.
|
||||
pub async fn health(&self) -> Result<(), MciasError> {
|
||||
self.get_empty("/v1/health").await
|
||||
}
|
||||
|
||||
/// Return the server's Ed25519 public key in JWK format.
|
||||
pub async fn get_public_key(&self) -> Result<PublicKey, MciasError> {
|
||||
self.get("/v1/keys/public").await
|
||||
}
|
||||
|
||||
// ---- Account management (admin only) ----
|
||||
|
||||
/// Create a new account. `account_type` must be `"human"` or `"system"`.
|
||||
pub async fn create_account(
|
||||
&self,
|
||||
username: &str,
|
||||
password: Option<&str>,
|
||||
account_type: &str,
|
||||
) -> Result<Account, MciasError> {
|
||||
let mut body = serde_json::json!({
|
||||
"username": username,
|
||||
"account_type": account_type,
|
||||
});
|
||||
if let Some(pw) = password {
|
||||
body["password"] = serde_json::Value::String(pw.to_owned());
|
||||
}
|
||||
self.post_expect_status("/v1/accounts", &body, StatusCode::CREATED)
|
||||
.await
|
||||
}
|
||||
|
||||
/// List all accounts.
|
||||
pub async fn list_accounts(&self) -> Result<Vec<Account>, MciasError> {
|
||||
self.get("/v1/accounts").await
|
||||
}
|
||||
|
||||
/// Get a single account by UUID.
|
||||
pub async fn get_account(&self, id: &str) -> Result<Account, MciasError> {
|
||||
self.get(&format!("/v1/accounts/{id}")).await
|
||||
}
|
||||
|
||||
/// Update an account's status. Allowed values: `"active"`, `"inactive"`.
|
||||
pub async fn update_account(&self, id: &str, status: &str) -> Result<Account, MciasError> {
|
||||
let body = serde_json::json!({ "status": status });
|
||||
self.patch(&format!("/v1/accounts/{id}"), &body).await
|
||||
}
|
||||
|
||||
/// Soft-delete an account and revoke all its tokens.
|
||||
pub async fn delete_account(&self, id: &str) -> Result<(), MciasError> {
|
||||
self.delete(&format!("/v1/accounts/{id}")).await
|
||||
}
|
||||
|
||||
// ---- Role management (admin only) ----
|
||||
|
||||
/// Get all roles assigned to an account.
|
||||
pub async fn get_roles(&self, account_id: &str) -> Result<Vec<String>, MciasError> {
|
||||
self.get(&format!("/v1/accounts/{account_id}/roles")).await
|
||||
}
|
||||
|
||||
/// Replace the complete role set for an account.
|
||||
pub async fn set_roles(&self, account_id: &str, roles: &[&str]) -> Result<(), MciasError> {
|
||||
let url = format!("/v1/accounts/{account_id}/roles");
|
||||
self.put_no_content(&url, roles).await
|
||||
}
|
||||
|
||||
// ---- Token management (admin only) ----
|
||||
|
||||
/// Issue a long-lived token for a system account.
|
||||
pub async fn issue_service_token(
|
||||
&self,
|
||||
account_id: &str,
|
||||
) -> Result<(String, String), MciasError> {
|
||||
let body = serde_json::json!({ "account_id": account_id });
|
||||
let resp: TokenResponse = self.post("/v1/token/issue", &body).await?;
|
||||
Ok((resp.token, resp.expires_at))
|
||||
}
|
||||
|
||||
/// Revoke a token by JTI.
|
||||
pub async fn revoke_token(&self, jti: &str) -> Result<(), MciasError> {
|
||||
self.delete(&format!("/v1/token/{jti}")).await
|
||||
}
|
||||
|
||||
// ---- PG credentials (admin only) ----
|
||||
|
||||
/// Get decrypted Postgres credentials for an account.
|
||||
pub async fn get_pg_creds(&self, account_id: &str) -> Result<PgCreds, MciasError> {
|
||||
self.get(&format!("/v1/accounts/{account_id}/pgcreds"))
|
||||
.await
|
||||
}
|
||||
|
||||
/// Store Postgres credentials for an account.
|
||||
pub async fn set_pg_creds(
|
||||
&self,
|
||||
account_id: &str,
|
||||
host: &str,
|
||||
port: u16,
|
||||
database: &str,
|
||||
username: &str,
|
||||
password: &str,
|
||||
) -> Result<(), MciasError> {
|
||||
let body = serde_json::json!({
|
||||
"host": host,
|
||||
"port": port,
|
||||
"database": database,
|
||||
"username": username,
|
||||
"password": password,
|
||||
});
|
||||
self.put_no_content(&format!("/v1/accounts/{account_id}/pgcreds"), &body)
|
||||
.await
|
||||
}
|
||||
|
||||
// ---- HTTP helpers ----
|
||||
|
||||
/// Build a request with the Authorization header set from the stored token.
|
||||
/// Security: the token is read under a read-lock and is not logged.
|
||||
async fn auth_header(&self) -> Option<header::HeaderValue> {
|
||||
let guard = self.token.read().await;
|
||||
guard.as_deref().and_then(|tok| {
|
||||
header::HeaderValue::from_str(&format!("Bearer {tok}")).ok()
|
||||
})
|
||||
}
|
||||
|
||||
async fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, MciasError> {
|
||||
let mut req = self.http.get(format!("{}{path}", self.base_url));
|
||||
if let Some(auth) = self.auth_header().await {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
self.decode(resp).await
|
||||
}
|
||||
|
||||
async fn get_empty(&self, path: &str) -> Result<(), MciasError> {
|
||||
let mut req = self.http.get(format!("{}{path}", self.base_url));
|
||||
if let Some(auth) = self.auth_header().await {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
self.expect_success(resp).await
|
||||
}
|
||||
|
||||
async fn post<B: Serialize, T: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
) -> Result<T, MciasError> {
|
||||
let mut req = self
|
||||
.http
|
||||
.post(format!("{}{path}", self.base_url))
|
||||
.json(body);
|
||||
if let Some(auth) = self.auth_header().await {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
self.decode(resp).await
|
||||
}
|
||||
|
||||
async fn post_expect_status<B: Serialize, T: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
expected: StatusCode,
|
||||
) -> Result<T, MciasError> {
|
||||
let mut req = self
|
||||
.http
|
||||
.post(format!("{}{path}", self.base_url))
|
||||
.json(body);
|
||||
if let Some(auth) = self.auth_header().await {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
if resp.status() == expected {
|
||||
return resp
|
||||
.json::<T>()
|
||||
.await
|
||||
.map_err(|e| MciasError::Decode(e.to_string()));
|
||||
}
|
||||
Err(self.error_from_response(resp).await)
|
||||
}
|
||||
|
||||
async fn post_empty(&self, path: &str) -> Result<(), MciasError> {
|
||||
let mut req = self
|
||||
.http
|
||||
.post(format!("{}{path}", self.base_url))
|
||||
.header(header::CONTENT_LENGTH, "0");
|
||||
if let Some(auth) = self.auth_header().await {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
self.expect_success(resp).await
|
||||
}
|
||||
|
||||
async fn patch<B: Serialize, T: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
path: &str,
|
||||
body: &B,
|
||||
) -> Result<T, MciasError> {
|
||||
let mut req = self
|
||||
.http
|
||||
.patch(format!("{}{path}", self.base_url))
|
||||
.json(body);
|
||||
if let Some(auth) = self.auth_header().await {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
self.decode(resp).await
|
||||
}
|
||||
|
||||
async fn put_no_content<B: Serialize + ?Sized>(&self, path: &str, body: &B) -> Result<(), MciasError> {
|
||||
let mut req = self
|
||||
.http
|
||||
.put(format!("{}{path}", self.base_url))
|
||||
.json(body);
|
||||
if let Some(auth) = self.auth_header().await {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
self.expect_success(resp).await
|
||||
}
|
||||
|
||||
async fn delete(&self, path: &str) -> Result<(), MciasError> {
|
||||
let mut req = self.http.delete(format!("{}{path}", self.base_url));
|
||||
if let Some(auth) = self.auth_header().await {
|
||||
req = req.header(header::AUTHORIZATION, auth);
|
||||
}
|
||||
let resp = req.send().await?;
|
||||
self.expect_success(resp).await
|
||||
}
|
||||
|
||||
async fn decode<T: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
resp: reqwest::Response,
|
||||
) -> Result<T, MciasError> {
|
||||
if resp.status().is_success() {
|
||||
return resp
|
||||
.json::<T>()
|
||||
.await
|
||||
.map_err(|e| MciasError::Decode(e.to_string()));
|
||||
}
|
||||
Err(self.error_from_response(resp).await)
|
||||
}
|
||||
|
||||
async fn expect_success(&self, resp: reqwest::Response) -> Result<(), MciasError> {
|
||||
if resp.status().is_success() {
|
||||
return Ok(());
|
||||
}
|
||||
Err(self.error_from_response(resp).await)
|
||||
}
|
||||
|
||||
async fn error_from_response(&self, resp: reqwest::Response) -> MciasError {
|
||||
let status = resp.status();
|
||||
let message = resp
|
||||
.json::<ErrorResponse>()
|
||||
.await
|
||||
.map(|e| if e.error.is_empty() { status.to_string() } else { e.error })
|
||||
.unwrap_or_else(|_| status.to_string());
|
||||
|
||||
match status {
|
||||
StatusCode::UNAUTHORIZED => MciasError::Auth(message),
|
||||
StatusCode::FORBIDDEN => MciasError::Forbidden(message),
|
||||
StatusCode::NOT_FOUND => MciasError::NotFound(message),
|
||||
StatusCode::BAD_REQUEST => MciasError::InvalidInput(message),
|
||||
StatusCode::CONFLICT => MciasError::Conflict(message),
|
||||
s => MciasError::Server {
|
||||
status: s.as_u16(),
|
||||
message,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
1
clients/rust/target/.rustc_info.json
Normal file
1
clients/rust/target/.rustc_info.json
Normal file
@@ -0,0 +1 @@
|
||||
{"rustc_fingerprint":14247534662873507473,"outputs":{"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.91.1 (ed61e7d7e 2025-11-07)\nbinary: rustc\ncommit-hash: ed61e7d7e242494fb7057f2657300d9e77bb4fcb\ncommit-date: 2025-11-07\nhost: aarch64-apple-darwin\nrelease: 1.91.1\nLLVM version: 21.1.2\n","stderr":""},"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___\nlib___.rlib\nlib___.dylib\nlib___.dylib\nlib___.a\nlib___.dylib\n/Users/kyle/.rustup/toolchains/stable-aarch64-apple-darwin\noff\npacked\nunpacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"aarch64\"\ntarget_endian=\"little\"\ntarget_env=\"\"\ntarget_family=\"unix\"\ntarget_feature=\"aes\"\ntarget_feature=\"crc\"\ntarget_feature=\"dit\"\ntarget_feature=\"dotprod\"\ntarget_feature=\"dpb\"\ntarget_feature=\"dpb2\"\ntarget_feature=\"fcma\"\ntarget_feature=\"fhm\"\ntarget_feature=\"flagm\"\ntarget_feature=\"fp16\"\ntarget_feature=\"frintts\"\ntarget_feature=\"jsconv\"\ntarget_feature=\"lor\"\ntarget_feature=\"lse\"\ntarget_feature=\"neon\"\ntarget_feature=\"paca\"\ntarget_feature=\"pacg\"\ntarget_feature=\"pan\"\ntarget_feature=\"pmuv3\"\ntarget_feature=\"ras\"\ntarget_feature=\"rcpc\"\ntarget_feature=\"rcpc2\"\ntarget_feature=\"rdm\"\ntarget_feature=\"sb\"\ntarget_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"ssbs\"\ntarget_feature=\"vh\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"macos\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"apple\"\nunix\n","stderr":""}},"successes":{}}
|
||||
3
clients/rust/target/CACHEDIR.TAG
Normal file
3
clients/rust/target/CACHEDIR.TAG
Normal file
@@ -0,0 +1,3 @@
|
||||
Signature: 8a477f597d28d172789f06886806bc55
|
||||
# This file is a cache directory tag created by cargo.
|
||||
# For information about cache directory tags see https://bford.info/cachedir/
|
||||
0
clients/rust/target/debug/.cargo-lock
Normal file
0
clients/rust/target/debug/.cargo-lock
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
c5ced89412783353
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":5347358027863023418,"path":9018917292316982161,"deps":[[1363051979936526615,"memchr",false,9245471952160789708]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/aho-corasick-28f87e939d4c721c/dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
d9ac40d9444c6550
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":14508078720126780090,"profile":5347358027863023418,"path":4281972190121210328,"deps":[[13548984313718623784,"serde",false,16120970927335062175],[13795362694956882968,"serde_json",false,4117845875591086358]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/assert-json-diff-be78b649a0eddaee/dep-lib-assert_json_diff","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
e8c38ee247aa7954
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":5347358027863023418,"path":7991844974711207059,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/atomic-waker-a55bbb641f718f5a/dep-lib-atomic_waker","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
a127bdcba5e37867
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":5347358027863023418,"path":16063635426002685251,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/base64-2f49d490615b4e49/dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
2d331b77805e9a04
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[\"arbitrary\", \"bytemuck\", \"example_generated\", \"serde\", \"serde_core\", \"std\"]","target":7691312148208718491,"profile":5347358027863023418,"path":8614177946156764418,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bitflags-eaedb70d6a41c30b/dep-lib-bitflags","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
8863bee26ad65812
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"extra-platforms\", \"serde\", \"std\"]","target":11402411492164584411,"profile":7855341030452660939,"path":9957043344816735784,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/bytes-9d0fd7b2ef5dbc06/dep-lib-bytes","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
6cc51f38803aae8d
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[\"jobserver\", \"parallel\"]","target":11042037588551934598,"profile":9003321226815314314,"path":14917407608417183273,"deps":[[8410525223747752176,"shlex",false,1715702720107712345],[9159843920629750842,"find_msvc_tools",false,2771186347935837742]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cc-de54006d6d00ad47/dep-lib-cc","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
dfe3f4728b16209c
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[\"core\", \"rustc-dep-of-std\"]","target":13840298032947503755,"profile":5347358027863023418,"path":17596718033636595651,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/cfg-if-8b6c5d6bdcf1deb9/dep-lib-cfg_if","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
330793750225f046
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"default\", \"managed\", \"unmanaged\"]","declared_features":"[\"default\", \"managed\", \"rt_async-std_1\", \"rt_tokio_1\", \"serde\", \"unmanaged\"]","target":13835509349254682884,"profile":5347358027863023418,"path":15683544176102990216,"deps":[[2357570525450087091,"num_cpus",false,18058056838497664298],[3554703672530437239,"deadpool_runtime",false,16524056477937411507],[13298363700532491723,"tokio",false,634726864496810915],[17917672826516349275,"lazy_static",false,3006984610137439678]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/deadpool-1cc37ec91796c245/dep-lib-deadpool","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
b3a5f387d13d51e5
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[\"async-std_1\", \"tokio_1\"]","target":12160367133229451087,"profile":5347358027863023418,"path":13187418150853952588,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/deadpool-runtime-0c3bfe3b184e819d/dep-lib-deadpool_runtime","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
ea0f0f6f2a2e515b
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[\"default\", \"std\"]","target":9331843185013996172,"profile":3033921117576893,"path":14663136940038511305,"deps":[[4289358735036141001,"proc_macro2",false,9691147391376955975],[10420560437213941093,"syn",false,5387550519180858585],[13111758008314797071,"quote",false,7524150538574845385]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/displaydoc-95590d2b87c9dab6/dep-lib-displaydoc","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
a0cadea7aeaa9e8d
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":1524667692659508025,"profile":5347358027863023418,"path":18405681531942536603,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/equivalent-e12128825f2bed30/dep-lib-equivalent","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
2eae20134d3b7526
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":10620166500288925791,"profile":9003321226815314314,"path":5526718681476264472,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/find-msvc-tools-be39ce7d61c79dc4/dep-lib-find_msvc_tools","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
2cc50f9e9e3de195
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":10248144769085601448,"profile":5347358027863023418,"path":6113750312496810284,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/fnv-7eafb3796651fc69/dep-lib-fnv","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
8835d71f33f6c9f3
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":6496257856677244489,"profile":5347358027863023418,"path":15610404081906102017,"deps":[[6803352382179706244,"percent_encoding",false,10389927364073220351]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/form_urlencoded-701bfaa4b527b19a/dep-lib-form_urlencoded","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
aa9a94ab748eaff6
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"alloc\", \"async-await\", \"default\", \"executor\", \"futures-executor\", \"std\"]","declared_features":"[\"alloc\", \"async-await\", \"bilock\", \"cfg-target-has-atomic\", \"compat\", \"default\", \"executor\", \"futures-executor\", \"io-compat\", \"spin\", \"std\", \"thread-pool\", \"unstable\", \"write-all-vectored\"]","target":7465627196321967167,"profile":17669703692130904899,"path":3204528599378229077,"deps":[[270634688040536827,"futures_sink",false,5748329606332216778],[302948626015856208,"futures_core",false,16395551869408918501],[5898568623609459682,"futures_util",false,17637614804480441737],[9128867168860799549,"futures_channel",false,14115314800078754606],[12256881686772805731,"futures_task",false,2612834557237516838],[17736352539849991289,"futures_io",false,5875146560703383865],[18054922619297524099,"futures_executor",false,12869042343505724837]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-1f82e392f9c7d455/dep-lib-futures","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
2e13824706ace3c3
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"alloc\", \"default\", \"futures-sink\", \"sink\", \"std\"]","declared_features":"[\"alloc\", \"cfg-target-has-atomic\", \"default\", \"futures-sink\", \"sink\", \"std\", \"unstable\"]","target":13634065851578929263,"profile":17669703692130904899,"path":6422289095744675876,"deps":[[270634688040536827,"futures_sink",false,5748329606332216778],[302948626015856208,"futures_core",false,16395551869408918501]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-channel-7010c68467a1b4fc/dep-lib-futures_channel","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
e53b9cd08eb388e3
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"cfg-target-has-atomic\", \"default\", \"portable-atomic\", \"std\", \"unstable\"]","target":9453135960607436725,"profile":17669703692130904899,"path":2058673473276210506,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-core-5fe8d65c922e7e22/dep-lib-futures_core","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
a5851e0cd40598b2
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"std\"]","declared_features":"[\"default\", \"std\", \"thread-pool\"]","target":11409328241454404632,"profile":17669703692130904899,"path":1553358905421552803,"deps":[[302948626015856208,"futures_core",false,16395551869408918501],[5898568623609459682,"futures_util",false,17637614804480441737],[12256881686772805731,"futures_task",false,2612834557237516838]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-executor-765ff97309c2360f/dep-lib-futures_executor","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
3949154aabb68851
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"std\"]","declared_features":"[\"default\", \"std\", \"unstable\"]","target":5742820543410686210,"profile":17669703692130904899,"path":1595730894326808548,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-io-6ad7e562befc2637/dep-lib-futures_io","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
9a3cdbc220bd14dc
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[]","declared_features":"[]","target":10957102547526291127,"profile":17878142613068009629,"path":18137792755441028436,"deps":[[4289358735036141001,"proc_macro2",false,9691147391376955975],[10420560437213941093,"syn",false,5387550519180858585],[13111758008314797071,"quote",false,7524150538574845385]],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-macro-dfcb695529e4e7a2/dep-lib-futures_macro","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
caf54a2d522bc64f
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":10827111567014737887,"profile":17669703692130904899,"path":17556092990125094149,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-sink-2ed24914c6766e7b/dep-lib-futures_sink","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
This file has an mtime of when this was started.
|
||||
@@ -0,0 +1 @@
|
||||
26da01b92fa74224
|
||||
@@ -0,0 +1 @@
|
||||
{"rustc":16243257175721966122,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"cfg-target-has-atomic\", \"default\", \"std\", \"unstable\"]","target":13518091470260541623,"profile":17669703692130904899,"path":13902076858023257041,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug/.fingerprint/futures-task-6cd194d5cabe3033/dep-lib-futures_task","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
||||
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user