1use crate::case::to_camel_case;
10use crate::config::{ResolvedEntity, ResolvedReport};
11use crate::error::AppError;
12use serde::Deserialize;
13use std::sync::Arc;
14
15pub struct AuthrsClient {
16 base_url: String,
17 service_name: String,
18 client: reqwest::Client,
19}
20
21#[derive(Deserialize)]
22struct CheckResponse {
23 allowed: Option<bool>,
24}
25
26impl AuthrsClient {
27 pub fn from_env() -> Option<Arc<Self>> {
28 let base_url = std::env::var("AUTHRS_URL").ok()?;
29 let service_name = std::env::var("SERVICE_NAME").ok()?;
30 let client = reqwest::Client::builder()
31 .timeout(std::time::Duration::from_secs(5))
32 .build()
33 .ok()?;
34 tracing::info!(url = %base_url, service = %service_name, "authrs permission checks enabled");
35 Some(Arc::new(Self {
36 base_url,
37 service_name,
38 client,
39 }))
40 }
41
42 async fn check(
43 &self,
44 tenant_id: &str,
45 user_id: &str,
46 resource: &str,
47 action: &str,
48 ) -> Result<bool, AppError> {
49 let url = format!("{}/admin/permissions/check", self.base_url);
50 let body = serde_json::json!({
51 "userId": user_id,
52 "resource": resource,
53 "action": action,
54 });
55 let mut request = self
56 .client
57 .post(&url)
58 .header("X-Tenant-ID", tenant_id)
59 .json(&body);
60 if let Some(tp) = crate::middleware::outbound_traceparent() {
62 request = request.header(crate::middleware::TRACEPARENT_HEADER, tp);
63 }
64 let resp = request.send().await.map_err(|e| {
65 tracing::error!(error = %e, "authrs request failed");
66 AppError::Unauthorized(format!("permission service unavailable: {}", e))
67 })?;
68
69 if !resp.status().is_success() {
70 let status = resp.status().as_u16();
71 tracing::error!(status, "authrs returned non-success status");
72 return Err(AppError::Unauthorized(format!(
73 "permission check failed with status {}",
74 status
75 )));
76 }
77
78 let check_resp: CheckResponse = resp.json().await.map_err(|e| {
79 tracing::error!(error = %e, "authrs response parse failed");
80 AppError::Unauthorized(format!("permission check response invalid: {}", e))
81 })?;
82
83 Ok(check_resp.allowed.unwrap_or(false))
84 }
85}
86
87fn pascal_case(s: &str) -> String {
88 let camel = to_camel_case(s);
89 let mut chars = camel.chars();
90 match chars.next() {
91 None => String::new(),
92 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
93 }
94}
95
96pub async fn check_entity_permission_opt(
101 client_opt: &Option<Arc<AuthrsClient>>,
102 tenant_id: Option<&str>,
103 user_id: Option<&str>,
104 entity: &ResolvedEntity,
105 http_verb: &str,
106) -> Result<(), AppError> {
107 let client = match client_opt {
108 Some(c) => c,
109 None => return Ok(()),
110 };
111
112 let user_id =
113 user_id.ok_or_else(|| AppError::Unauthorized("X-User-ID header is required".into()))?;
114 let tenant_id = tenant_id.unwrap_or("");
115
116 let action = format!("{}{}", http_verb, pascal_case(&entity.table_name));
117 let resource = format!(
118 "service:{}/package:{}/table:{}",
119 client.service_name, entity.package_id, entity.table_name
120 );
121
122 tracing::debug!(
123 user_id = %user_id,
124 resource = %resource,
125 action = %action,
126 "checking authrs permission"
127 );
128
129 let allowed = client.check(tenant_id, user_id, &resource, &action).await?;
130
131 if allowed {
132 tracing::info!(
133 user_id = %user_id,
134 tenant_id = %tenant_id,
135 resource = %resource,
136 action = %action,
137 "permission granted"
138 );
139 } else {
140 tracing::warn!(
141 user_id = %user_id,
142 tenant_id = %tenant_id,
143 resource = %resource,
144 action = %action,
145 "permission denied"
146 );
147 return Err(AppError::Unauthorized(format!(
148 "action '{}' not permitted on '{}'",
149 action, resource
150 )));
151 }
152
153 Ok(())
154}
155
156pub async fn check_report_permission_opt(
164 client_opt: &Option<Arc<AuthrsClient>>,
165 tenant_id: Option<&str>,
166 user_id: Option<&str>,
167 report: &ResolvedReport,
168 action: &str,
169) -> Result<(), AppError> {
170 let client = match client_opt {
171 Some(c) => c,
172 None => return Ok(()),
173 };
174
175 let user_id =
176 user_id.ok_or_else(|| AppError::Unauthorized("X-User-ID header is required".into()))?;
177 let tenant_id = tenant_id.unwrap_or("");
178
179 let resource = format!(
180 "service:{}/package:{}/report:{}",
181 client.service_name, report.package_id, report.id
182 );
183
184 tracing::debug!(
185 user_id = %user_id,
186 resource = %resource,
187 action = %action,
188 "checking authrs report permission"
189 );
190
191 let allowed = client.check(tenant_id, user_id, &resource, action).await?;
192
193 if allowed {
194 tracing::info!(
195 user_id = %user_id,
196 tenant_id = %tenant_id,
197 resource = %resource,
198 action = %action,
199 "report permission granted"
200 );
201 } else {
202 tracing::warn!(
203 user_id = %user_id,
204 tenant_id = %tenant_id,
205 resource = %resource,
206 action = %action,
207 "report permission denied"
208 );
209 return Err(AppError::Unauthorized(format!(
210 "action '{}' not permitted on '{}'",
211 action, resource
212 )));
213 }
214
215 Ok(())
216}