use reqwest::{Client, Url};
use serde_json::Value;
use super::error::AuthError;
#[derive(Debug, Clone)]
pub struct AuthorizationConfig {
pub read_url: String,
pub write_url: String,
}
impl Default for AuthorizationConfig {
fn default() -> Self {
Self {
read_url: "http://localhost:4466".to_string(),
write_url: "http://localhost:4467".to_string(),
}
}
}
#[derive(Debug, Clone)]
pub struct AuthorizationClient {
client: Client,
read_url: Url,
write_url: Url,
}
impl AuthorizationClient {
pub fn new(config: AuthorizationConfig) -> Result<Self, AuthError> {
Ok(Self {
client: Client::new(),
read_url: parse_base_url(&config.read_url)
.map_err(|e| AuthError::AuthorizationBackend(e.to_string()))?,
write_url: parse_base_url(&config.write_url)
.map_err(|e| AuthError::AuthorizationBackend(e.to_string()))?,
})
}
pub fn with_defaults() -> Result<Self, AuthError> {
Self::new(AuthorizationConfig::default())
}
pub async fn check_permission(
&self,
namespace: &str,
object: &str,
relation: &str,
subject_id: &str,
) -> Result<bool, AuthError> {
let url = url_with_query(
&self.read_url,
"relation-tuples/check",
&[
("namespace", namespace),
("object", object),
("relation", relation),
("subject_id", subject_id),
],
)?;
let response = self
.client
.get(url)
.send()
.await
.map_err(|e| AuthError::AuthorizationBackendUnavailable(e.to_string()))?;
if response.status().is_success() {
let body: Value = response
.json()
.await
.map_err(|e| AuthError::AuthorizationBackend(e.to_string()))?;
Ok(body
.get("allowed")
.and_then(|v| v.as_bool())
.unwrap_or(false))
} else if response.status() == reqwest::StatusCode::FORBIDDEN {
Ok(false)
} else {
Err(backend_error(response).await)
}
}
pub async fn create_relation_tuple(
&self,
namespace: &str,
object: &str,
relation: &str,
subject_id: &str,
) -> Result<Value, AuthError> {
let url = self
.write_url
.join("admin/relation-tuples")
.map_err(|e| AuthError::AuthorizationBackend(e.to_string()))?;
let payload = serde_json::json!({
"namespace": namespace,
"object": object,
"relation": relation,
"subject_id": subject_id,
});
let patch = serde_json::json!([{
"action": "insert",
"relation_tuple": payload,
}]);
let response = self
.client
.patch(url)
.header("accept", "application/json")
.json(&patch)
.send()
.await
.map_err(|e| AuthError::AuthorizationBackendUnavailable(e.to_string()))?;
if response.status().is_success() {
Ok(payload)
} else {
Err(backend_error(response).await)
}
}
pub async fn delete_relation_tuple(
&self,
namespace: &str,
object: &str,
relation: &str,
subject_id: &str,
) -> Result<(), AuthError> {
let url = url_with_query(
&self.write_url,
"admin/relation-tuples",
&[
("namespace", namespace),
("object", object),
("relation", relation),
("subject_id", subject_id),
],
)?;
let response = self
.client
.delete(url)
.send()
.await
.map_err(|e| AuthError::AuthorizationBackendUnavailable(e.to_string()))?;
if response.status().is_success() {
Ok(())
} else {
Err(backend_error(response).await)
}
}
pub async fn expand(
&self,
namespace: &str,
object: &str,
relation: &str,
) -> Result<Value, AuthError> {
let url = url_with_query(
&self.read_url,
"relation-tuples/expand",
&[
("namespace", namespace),
("object", object),
("relation", relation),
],
)?;
let response = self
.client
.get(url)
.send()
.await
.map_err(|e| AuthError::AuthorizationBackendUnavailable(e.to_string()))?;
if response.status().is_success() {
response
.json()
.await
.map_err(|e| AuthError::AuthorizationBackend(e.to_string()))
} else {
Err(backend_error(response).await)
}
}
}
fn parse_base_url(url: &str) -> Result<Url, String> {
let mut url = Url::parse(url).map_err(|e| e.to_string())?;
if !url.path().ends_with('/') {
url.path_segments_mut()
.map_err(|_| "cannot modify url path".to_string())?
.push("");
}
Ok(url)
}
fn url_with_query(base: &Url, path: &str, params: &[(&str, &str)]) -> Result<Url, AuthError> {
let mut url = base
.join(path)
.map_err(|e| AuthError::AuthorizationBackend(e.to_string()))?;
{
let mut pairs = url.query_pairs_mut();
for (key, value) in params {
pairs.append_pair(key, value);
}
}
Ok(url)
}
async fn backend_error(response: reqwest::Response) -> AuthError {
let status = response.status().as_u16();
let message = response
.text()
.await
.unwrap_or_else(|_| "<unreadable body>".to_string());
AuthError::AuthorizationBackend(format!("backend returned {status}: {message}"))
}