use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::client::AgentTrustClient;
use crate::error::Result;
use crate::models::{
FederationProvider, IssueFederatedIDTokenRequest, IssueFederatedIDTokenResult,
RegisterFederationProviderRequest, Session, VerifyFederatedTokenRequest,
VerifyFederatedTokenResult,
};
pub struct Federation<'a> {
pub(crate) client: &'a AgentTrustClient,
}
#[derive(Debug, Deserialize)]
struct ProviderRegisterResponse {
#[serde(default)]
provider: Option<FederationProvider>,
}
#[derive(Debug, Deserialize)]
struct ProviderListResponse {
#[serde(default)]
providers: Option<Vec<FederationProvider>>,
}
#[derive(Debug, Serialize)]
struct IssueFederationTokenRequest<'a> {
agent_id: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
audience: Option<&'a str>,
#[serde(skip_serializing_if = "slice_is_empty")]
scopes: &'a [String],
#[serde(skip_serializing_if = "Option::is_none")]
ttl: Option<u64>,
}
fn slice_is_empty<T>(items: &[T]) -> bool {
items.is_empty()
}
impl<'a> Federation<'a> {
pub fn register_provider(
&self,
req: &RegisterFederationProviderRequest,
) -> Result<FederationProvider> {
let value: Value =
self.client
.request("POST", "/api/v1/federation/providers", Some(req))?;
if value.get("provider").is_some() {
let resp: ProviderRegisterResponse = serde_json::from_value(value)?;
return resp
.provider
.ok_or_else(|| crate::error::AgentTrustError::Api {
message: "missing provider in response".to_string(),
code: "PROVIDER_MISSING".to_string(),
status: 500,
});
}
let p: FederationProvider = serde_json::from_value(value)?;
Ok(p)
}
pub fn list_providers(&self) -> Result<Vec<FederationProvider>> {
let value: Value =
self.client
.request("GET", "/api/v1/federation/providers", None::<&()>)?;
if let Value::Array(_) = &value {
let v: Vec<FederationProvider> = serde_json::from_value(value)?;
return Ok(v);
}
let resp: ProviderListResponse = serde_json::from_value(value)?;
Ok(resp.providers.unwrap_or_default())
}
pub fn delete_provider(&self, provider_id: &str) -> Result<()> {
let path = format!("/api/v1/federation/providers/{}", provider_id);
self.client
.request_no_response("DELETE", &path, None::<&()>)
}
pub fn issue_token(
&self,
agent_id: &str,
req: &IssueFederatedIDTokenRequest,
) -> Result<IssueFederatedIDTokenResult> {
let body = IssueFederationTokenRequest {
agent_id,
audience: req.audience.as_deref(),
scopes: req.scopes.as_slice(),
ttl: req.ttl,
};
self.client
.request("POST", "/api/v1/federation/tokens/issue", Some(&body))
}
pub fn introspect_token(
&self,
req: &VerifyFederatedTokenRequest,
) -> Result<VerifyFederatedTokenResult> {
self.client
.request("POST", "/api/v1/federation/tokens/verify", Some(req))
}
pub fn revoke_token(&self, token: &str) -> Result<()> {
#[derive(serde::Serialize)]
struct RevokeBody<'a> {
token: &'a str,
}
self.client.request_no_response(
"POST",
"/api/v1/federation/tokens/revoke",
Some(&RevokeBody { token }),
)
}
pub fn init_session(&self, req: &VerifyFederatedTokenRequest) -> Result<Session> {
self.client
.request("POST", "/api/v1/federation/sessions/init", Some(req))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::AgentTrustError;
use mockito::Server;
fn provider_body() -> &'static str {
r#"{
"id":"prov-1","org_id":"o1","issuer":"https://idp",
"name":"idp","jwks_uri":"https://idp/.well-known/jwks.json",
"trust_level":"standard","status":"active"
}"#
}
#[test]
fn test_register_provider_wrapped() {
let mut srv = Server::new();
let body = format!(r#"{{"provider":{}}}"#, provider_body());
let mock = srv
.mock("POST", "/api/v1/federation/providers")
.with_status(200)
.with_body(body)
.create();
let client = AgentTrustClient::builder()
.base_url(&srv.url())
.build()
.unwrap();
let p = client
.federation()
.register_provider(&RegisterFederationProviderRequest {
issuer: "https://idp".into(),
name: "idp".into(),
trust_level: None,
})
.unwrap();
assert_eq!(p.id, "prov-1");
assert_eq!(p.trust_level, "standard");
mock.assert();
}
#[test]
fn test_list_providers_validation_error() {
let mut srv = Server::new();
let mock = srv
.mock("GET", "/api/v1/federation/providers")
.with_status(400)
.with_body(r#"{"message":"invalid query"}"#)
.create();
let client = AgentTrustClient::builder()
.base_url(&srv.url())
.build()
.unwrap();
let err = client.federation().list_providers().unwrap_err();
assert!(matches!(err, AgentTrustError::Validation { .. }));
mock.assert();
}
#[test]
fn test_introspect_token_success() {
let mut srv = Server::new();
let mock = srv
.mock("POST", "/api/v1/federation/tokens/verify")
.with_status(200)
.with_body(r#"{"valid":true,"agent_id":"a1","issuer":"https://idp"}"#)
.create();
let client = AgentTrustClient::builder()
.base_url(&srv.url())
.build()
.unwrap();
let r = client
.federation()
.introspect_token(&VerifyFederatedTokenRequest {
token: "eyJ".into(),
issuer_hint: None,
})
.unwrap();
assert!(r.valid);
assert_eq!(r.agent_id.as_deref(), Some("a1"));
mock.assert();
}
#[test]
fn test_revoke_token_server_error() {
let mut srv = Server::new();
let mock = srv
.mock("POST", "/api/v1/federation/tokens/revoke")
.with_status(500)
.with_body(r#"{"message":"failed"}"#)
.create();
let client = AgentTrustClient::builder()
.base_url(&srv.url())
.build()
.unwrap();
let err = client.federation().revoke_token("eyJ").unwrap_err();
match err {
AgentTrustError::Api { status, .. } => assert_eq!(status, 500),
other => panic!("unexpected: {:?}", other),
}
mock.assert();
}
}