2025-11-22 22:55:35 -03:00
|
|
|
use anyhow::{anyhow, Result};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use tokio::sync::RwLock;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ZitadelConfig {
|
|
|
|
|
pub issuer_url: String,
|
|
|
|
|
pub issuer: String,
|
|
|
|
|
pub client_id: String,
|
|
|
|
|
pub client_secret: String,
|
|
|
|
|
pub redirect_uri: String,
|
|
|
|
|
pub project_id: String,
|
|
|
|
|
pub api_url: String,
|
|
|
|
|
pub service_account_key: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone)]
|
|
|
|
|
pub struct ZitadelClient {
|
|
|
|
|
config: ZitadelConfig,
|
|
|
|
|
http_client: reqwest::Client,
|
|
|
|
|
access_token: Arc<RwLock<Option<String>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ZitadelClient {
|
|
|
|
|
pub async fn new(config: ZitadelConfig) -> Result<Self> {
|
|
|
|
|
let http_client = reqwest::Client::builder()
|
|
|
|
|
.timeout(std::time::Duration::from_secs(30))
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
|
|
|
|
|
|
|
|
|
|
Ok(Self {
|
|
|
|
|
config,
|
|
|
|
|
http_client,
|
|
|
|
|
access_token: Arc::new(RwLock::new(None)),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-27 08:34:24 -03:00
|
|
|
/// Get the API base URL
|
|
|
|
|
pub fn api_url(&self) -> &str {
|
|
|
|
|
&self.config.api_url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Make a GET request with authentication
|
|
|
|
|
pub async fn http_get(&self, url: String) -> reqwest::RequestBuilder {
|
|
|
|
|
let token = self.get_access_token().await.unwrap_or_default();
|
|
|
|
|
self.http_client.get(url).bearer_auth(token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Make a POST request with authentication
|
|
|
|
|
pub async fn http_post(&self, url: String) -> reqwest::RequestBuilder {
|
|
|
|
|
let token = self.get_access_token().await.unwrap_or_default();
|
|
|
|
|
self.http_client.post(url).bearer_auth(token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Make a PUT request with authentication
|
|
|
|
|
pub async fn http_put(&self, url: String) -> reqwest::RequestBuilder {
|
|
|
|
|
let token = self.get_access_token().await.unwrap_or_default();
|
|
|
|
|
self.http_client.put(url).bearer_auth(token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Make a PATCH request with authentication
|
|
|
|
|
pub async fn http_patch(&self, url: String) -> reqwest::RequestBuilder {
|
|
|
|
|
let token = self.get_access_token().await.unwrap_or_default();
|
|
|
|
|
self.http_client.patch(url).bearer_auth(token)
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-22 22:55:35 -03:00
|
|
|
pub async fn get_access_token(&self) -> Result<String> {
|
|
|
|
|
// Check if we have a cached token
|
|
|
|
|
{
|
|
|
|
|
let token = self.access_token.read().await;
|
|
|
|
|
if let Some(t) = token.as_ref() {
|
|
|
|
|
return Ok(t.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Get new token using client credentials
|
|
|
|
|
let token_url = format!("{}/oauth/v2/token", self.config.api_url);
|
|
|
|
|
|
|
|
|
|
let params = [
|
|
|
|
|
("grant_type", "client_credentials"),
|
|
|
|
|
("client_id", &self.config.client_id),
|
|
|
|
|
("client_secret", &self.config.client_secret),
|
|
|
|
|
("scope", "openid profile email"),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.post(&token_url)
|
|
|
|
|
.form(¶ms)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to get access token: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let token_data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse token response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let access_token = token_data
|
|
|
|
|
.get("access_token")
|
|
|
|
|
.and_then(|t| t.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow!("No access token in response"))?
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
// Cache the token
|
|
|
|
|
{
|
|
|
|
|
let mut token = self.access_token.write().await;
|
|
|
|
|
*token = Some(access_token.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(access_token)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn create_user(
|
|
|
|
|
&self,
|
|
|
|
|
email: &str,
|
|
|
|
|
first_name: &str,
|
|
|
|
|
last_name: &str,
|
|
|
|
|
username: Option<&str>,
|
|
|
|
|
) -> Result<String> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
|
|
|
|
let url = format!("{}/v2/users/human", self.config.api_url);
|
|
|
|
|
|
|
|
|
|
let body = serde_json::json!({
|
|
|
|
|
"userName": username.unwrap_or(email),
|
|
|
|
|
"profile": {
|
|
|
|
|
"givenName": first_name,
|
|
|
|
|
"familyName": last_name,
|
|
|
|
|
"displayName": format!("{} {}", first_name, last_name)
|
|
|
|
|
},
|
|
|
|
|
"email": {
|
|
|
|
|
"email": email,
|
|
|
|
|
"isVerified": false
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.json(&body)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to create user: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to create user: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let user_data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse user response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let user_id = user_data
|
|
|
|
|
.get("userId")
|
|
|
|
|
.and_then(|id| id.as_str())
|
|
|
|
|
.ok_or_else(|| anyhow!("No userId in response"))?
|
|
|
|
|
.to_string();
|
|
|
|
|
|
|
|
|
|
Ok(user_id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_user(&self, user_id: &str) -> Result<serde_json::Value> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
|
|
|
|
let url = format!("{}/v2/users/{}", self.config.api_url, user_id);
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.get(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to get user: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to get user: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let user_data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse user response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
Ok(user_data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list_users(&self, limit: u32, offset: u32) -> Result<Vec<serde_json::Value>> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
|
|
|
|
let url = format!(
|
|
|
|
|
"{}/v2/users?limit={}&offset={}",
|
|
|
|
|
self.config.api_url, limit, offset
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.get(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to list users: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to list users: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse users response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let users = data
|
|
|
|
|
.get("result")
|
|
|
|
|
.and_then(|r| r.as_array())
|
|
|
|
|
.map(|arr| arr.iter().cloned().collect())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
Ok(users)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn search_users(&self, query: &str) -> Result<Vec<serde_json::Value>> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
|
|
|
|
let url = format!("{}/v2/users/_search", self.config.api_url);
|
|
|
|
|
|
|
|
|
|
let body = serde_json::json!({
|
|
|
|
|
"queries": [{
|
|
|
|
|
"userNameQuery": {
|
|
|
|
|
"userName": query,
|
|
|
|
|
"method": "TEXT_QUERY_METHOD_CONTAINS_IGNORE_CASE"
|
|
|
|
|
}
|
|
|
|
|
}]
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.json(&body)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to search users: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to search users: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse search response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let users = data
|
|
|
|
|
.get("result")
|
|
|
|
|
.and_then(|r| r.as_array())
|
|
|
|
|
.map(|arr| arr.iter().cloned().collect())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
Ok(users)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_user_memberships(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: &str,
|
|
|
|
|
offset: u32,
|
|
|
|
|
limit: u32,
|
|
|
|
|
) -> Result<serde_json::Value> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
|
|
|
|
let url = format!(
|
|
|
|
|
"{}/v2/users/{}/memberships?limit={}&offset={}",
|
|
|
|
|
self.config.api_url, user_id, limit, offset
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.get(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to get memberships: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to get memberships: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse memberships response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
Ok(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn add_org_member(
|
|
|
|
|
&self,
|
|
|
|
|
org_id: &str,
|
|
|
|
|
user_id: &str,
|
|
|
|
|
roles: Vec<String>,
|
|
|
|
|
) -> Result<()> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
2025-11-27 08:34:24 -03:00
|
|
|
let url = format!(
|
|
|
|
|
"{}/v2/organizations/{}/members",
|
|
|
|
|
self.config.api_url, org_id
|
|
|
|
|
);
|
2025-11-22 22:55:35 -03:00
|
|
|
|
|
|
|
|
let body = serde_json::json!({
|
|
|
|
|
"userId": user_id,
|
|
|
|
|
"roles": roles
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.json(&body)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to add org member: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to add org member: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn remove_org_member(&self, org_id: &str, user_id: &str) -> Result<()> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
|
|
|
|
let url = format!(
|
|
|
|
|
"{}/v2/organizations/{}/members/{}",
|
|
|
|
|
self.config.api_url, org_id, user_id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.delete(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to remove org member: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to remove org member: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_org_members(&self, org_id: &str) -> Result<Vec<serde_json::Value>> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
2025-11-27 08:34:24 -03:00
|
|
|
let url = format!(
|
|
|
|
|
"{}/v2/organizations/{}/members",
|
|
|
|
|
self.config.api_url, org_id
|
|
|
|
|
);
|
2025-11-22 22:55:35 -03:00
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.get(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to get org members: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to get org members: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse org members response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
let members = data
|
|
|
|
|
.get("result")
|
|
|
|
|
.and_then(|r| r.as_array())
|
|
|
|
|
.map(|arr| arr.iter().cloned().collect())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
|
|
|
|
|
Ok(members)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn get_organization(&self, org_id: &str) -> Result<serde_json::Value> {
|
|
|
|
|
let token = self.get_access_token().await?;
|
|
|
|
|
let url = format!("{}/v2/organizations/{}", self.config.api_url, org_id);
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.get(&url)
|
|
|
|
|
.bearer_auth(&token)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to get organization: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to get organization: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse organization response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
Ok(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn introspect_token(&self, token: &str) -> Result<serde_json::Value> {
|
|
|
|
|
let url = format!("{}/oauth/v2/introspect", self.config.api_url);
|
|
|
|
|
|
|
|
|
|
let params = [
|
|
|
|
|
("token", token),
|
|
|
|
|
("client_id", &self.config.client_id),
|
|
|
|
|
("client_secret", &self.config.client_secret),
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
|
|
|
|
.post(&url)
|
|
|
|
|
.form(¶ms)
|
|
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to introspect token: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
let error_text = response.text().await.unwrap_or_default();
|
|
|
|
|
return Err(anyhow!("Failed to introspect token: {}", error_text));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let data: serde_json::Value = response
|
|
|
|
|
.json()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to parse introspection response: {}", e))?;
|
|
|
|
|
|
|
|
|
|
Ok(data)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn check_permission(
|
|
|
|
|
&self,
|
|
|
|
|
user_id: &str,
|
|
|
|
|
permission: &str,
|
|
|
|
|
resource: &str,
|
|
|
|
|
) -> Result<bool> {
|
2025-11-27 08:34:24 -03:00
|
|
|
// Check if user has specific permission on resource
|
2025-11-22 22:55:35 -03:00
|
|
|
let token = self.get_access_token().await?;
|
2025-11-27 08:34:24 -03:00
|
|
|
let url = format!(
|
|
|
|
|
"{}/v2/users/{}/permissions/check",
|
|
|
|
|
self.config.api_url, user_id
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let check_payload = serde_json::json!({
|
|
|
|
|
"permission": permission,
|
|
|
|
|
"resource": resource,
|
|
|
|
|
"namespace": self.config.project_id.clone()
|
|
|
|
|
});
|
2025-11-22 22:55:35 -03:00
|
|
|
|
|
|
|
|
let response = self
|
|
|
|
|
.http_client
|
2025-11-27 08:34:24 -03:00
|
|
|
.post(&url)
|
2025-11-22 22:55:35 -03:00
|
|
|
.bearer_auth(&token)
|
2025-11-27 08:34:24 -03:00
|
|
|
.json(&check_payload)
|
2025-11-22 22:55:35 -03:00
|
|
|
.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| anyhow!("Failed to check permissions: {}", e))?;
|
|
|
|
|
|
|
|
|
|
if !response.status().is_success() {
|
|
|
|
|
return Ok(false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Simple check - in production, parse and validate permissions
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
}
|