feat(phase-5): WS1 — Rust SDK
Implements the sentryagent-idp Rust SDK crate (sdk-rust/) with: - TokenManager with Arc<Mutex<TokenCache>> for thread-safe token caching - AgentIdPClient with full method coverage: agents, oauth2, credentials, audit, marketplace, delegation - Error hierarchy via thiserror (AgentIdPError enum) - All model types with serde derive - 429 RateLimited handling with Retry-After parsing; zero unwrap() calls - Unit tests (mockito), doc tests, and integration tests (#[ignore]) - quickstart example, full README, cargo doc clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
98
sdk-rust/src/credentials.rs
Normal file
98
sdk-rust/src/credentials.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
//! Credential management methods for `AgentIdPClient`.
|
||||
//!
|
||||
//! Covers `POST /agents/{id}/credentials` (generate),
|
||||
//! `POST /agents/{id}/credentials/rotate`, and
|
||||
//! `DELETE /agents/{id}/credentials/{cred_id}`.
|
||||
|
||||
use crate::agents::parse_response;
|
||||
use crate::client::AgentIdPClient;
|
||||
use crate::error::AgentIdPError;
|
||||
use crate::models::Credentials;
|
||||
|
||||
impl AgentIdPClient {
|
||||
/// Generates a new set of credentials (client ID + secret) for an agent.
|
||||
///
|
||||
/// `POST /agents/{id}/credentials` → `201 Credentials`
|
||||
///
|
||||
/// The `client_secret` field in the response is the **only time** the
|
||||
/// plaintext secret is returned — store it securely.
|
||||
pub async fn generate_credentials(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
) -> Result<Credentials, AgentIdPError> {
|
||||
let auth = self.get_auth_header().await?;
|
||||
let url = format!("{}/agents/{}/credentials", self.base_url, agent_id);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.post(&url)
|
||||
.header("Authorization", auth)
|
||||
.header("Content-Length", "0")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
parse_response(resp).await
|
||||
}
|
||||
|
||||
/// Rotates the credentials for an agent, invalidating the previous secret.
|
||||
///
|
||||
/// `POST /agents/{id}/credentials/rotate` → `200 Credentials`
|
||||
///
|
||||
/// The new `client_secret` is returned in the response and will not be
|
||||
/// retrievable again.
|
||||
pub async fn rotate_credentials(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
) -> Result<Credentials, AgentIdPError> {
|
||||
let auth = self.get_auth_header().await?;
|
||||
let url = format!(
|
||||
"{}/agents/{}/credentials/rotate",
|
||||
self.base_url, agent_id
|
||||
);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.post(&url)
|
||||
.header("Authorization", auth)
|
||||
.header("Content-Length", "0")
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
parse_response(resp).await
|
||||
}
|
||||
|
||||
/// Revokes a specific credential set for an agent.
|
||||
///
|
||||
/// `DELETE /agents/{id}/credentials/{cred_id}` → `204 No Content`
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::error::AgentIdPError::NotFound`] when the agent or
|
||||
/// credential ID does not exist.
|
||||
pub async fn revoke_credentials(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
cred_id: &str,
|
||||
) -> Result<(), AgentIdPError> {
|
||||
let auth = self.get_auth_header().await?;
|
||||
let url = format!(
|
||||
"{}/agents/{}/credentials/{}",
|
||||
self.base_url, agent_id, cred_id
|
||||
);
|
||||
|
||||
let resp = self
|
||||
.http
|
||||
.delete(&url)
|
||||
.header("Authorization", auth)
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if resp.status().as_u16() == 204 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Delegate error handling to parse_response; the Ok branch is unreachable.
|
||||
let _: Credentials = parse_response(resp).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user