agentguard_server/
grpc.rs1use crate::auth_layer::AuthenticationFailure;
10use crate::authzen::{evaluation_request_for_caller, AppState};
11use crate::proto::agentguard::v1::{
12 access_evaluation_server::{AccessEvaluation, AccessEvaluationServer},
13 EvaluationRequest as PbRequest, EvaluationResponse as PbResponse,
14};
15use agentguard_core::Effect;
16use axum::http::StatusCode;
17use std::sync::Arc;
18use tonic::{Request, Response, Status};
19
20pub const MAX_GRPC_REQUEST_BYTES: usize = 64 * 1024;
23
24#[derive(Clone)]
26pub struct AccessEvaluationService {
27 state: Arc<AppState>,
28}
29
30impl AccessEvaluationService {
31 pub fn new(state: Arc<AppState>) -> Self {
32 Self { state }
33 }
34}
35
36#[tonic::async_trait]
37impl AccessEvaluation for AccessEvaluationService {
38 #[tracing::instrument(
39 skip_all,
40 fields(
41 subject_id = tracing::field::Empty,
42 action_id = tracing::field::Empty,
43 resource_id = tracing::field::Empty,
44 )
45 )]
46 async fn evaluation(
47 &self,
48 request: Request<PbRequest>,
49 ) -> Result<Response<PbResponse>, Status> {
50 let caller = self
51 .state
52 .auth
53 .authenticate_bearer_async(
54 request
55 .metadata()
56 .get("authorization")
57 .and_then(|value| value.to_str().ok()),
58 "authorize",
59 true,
60 )
61 .await
62 .map_err(|failure| match failure {
63 AuthenticationFailure::Unauthenticated => Status::unauthenticated("unauthorized"),
64 AuthenticationFailure::Forbidden => Status::permission_denied("forbidden"),
65 AuthenticationFailure::Unavailable => {
66 Status::unavailable("authentication capacity exhausted")
67 }
68 })?;
69 let req = request.into_inner();
70 let request_bytes = req
71 .subject
72 .as_ref()
73 .map_or(0, |entity| {
74 entity.r#type.len().saturating_add(entity.id.len())
75 })
76 .saturating_add(req.action.as_ref().map_or(0, |entity| {
77 entity.r#type.len().saturating_add(entity.id.len())
78 }))
79 .saturating_add(req.resource.as_ref().map_or(0, |entity| {
80 entity.r#type.len().saturating_add(entity.id.len())
81 }))
82 .saturating_add(req.context_json.len())
83 .saturating_add(req.entities_json.len());
84 if request_bytes > MAX_GRPC_REQUEST_BYTES {
85 return Err(Status::resource_exhausted(
86 "evaluation request exceeds 64 KiB",
87 ));
88 }
89 let subject = req
90 .subject
91 .ok_or_else(|| Status::invalid_argument("missing subject"))?;
92 let action = req
93 .action
94 .ok_or_else(|| Status::invalid_argument("missing action"))?;
95 let resource = req
96 .resource
97 .ok_or_else(|| Status::invalid_argument("missing resource"))?;
98
99 let context: serde_json::Value = if req.context_json.is_empty() {
104 serde_json::json!({})
105 } else {
106 serde_json::from_str(&req.context_json)
107 .map_err(|e| Status::invalid_argument(format!("context_json: {e}")))?
108 };
109 let per_request_entities: Vec<serde_json::Value> = if req.entities_json.is_empty() {
110 Vec::new()
111 } else {
112 serde_json::from_str(&req.entities_json)
113 .map_err(|e| Status::invalid_argument(format!("entities_json: {e}")))?
114 };
115
116 let http_style = crate::authzen::EvaluationRequest {
118 subject: crate::authzen::EntityRef {
119 entity_type: subject.r#type,
120 id: subject.id,
121 },
122 action: crate::authzen::EntityRef {
123 entity_type: action.r#type,
124 id: action.id,
125 },
126 resource: crate::authzen::EntityRef {
127 entity_type: resource.r#type,
128 id: resource.id,
129 },
130 context: context.clone(),
131 entities: per_request_entities.clone(),
132 };
133
134 let agent_req = evaluation_request_for_caller(http_style, caller.as_ref()).map_err(
135 |error| match error {
136 crate::authzen::EvaluationMappingError::IdentityMismatch => {
137 Status::permission_denied("subject or tenant does not match API key")
138 }
139 crate::authzen::EvaluationMappingError::InvalidRequest(message) => {
140 Status::invalid_argument(message)
141 }
142 },
143 )?;
144 tracing::Span::current().record("subject_id", agent_req.principal.id().to_string());
146 tracing::Span::current().record("action_id", format!("{}", agent_req.action));
147 tracing::Span::current().record("resource_id", agent_req.resource.uid.to_string());
148 let action_label = format!("{}", agent_req.action);
149
150 let (decision, elapsed) = crate::authzen::authorize_and_persist_decision(
151 self.state.pdp_work_slots.clone(),
152 self.state.authorizer().snapshot(),
153 agent_req,
154 per_request_entities,
155 self.state.audit_handle(),
156 crate::authzen::authenticated_actor(caller.as_ref()),
157 )
158 .await
159 .map_err(|error| {
160 let (status, message) = crate::authzen::report_pdp_work_failure(&self.state, error);
161 match status {
162 StatusCode::SERVICE_UNAVAILABLE => Status::unavailable(message),
163 StatusCode::BAD_REQUEST => Status::invalid_argument(message),
164 _ => Status::internal(message),
165 }
166 })?;
167
168 let effect_label = match decision.effect {
169 Effect::Allow => "allow",
170 Effect::Deny => "deny",
171 };
172 let policy_id = decision
173 .policies
174 .first()
175 .cloned()
176 .unwrap_or_else(|| "none".into());
177 self.state
178 .metrics()
179 .record_decision(effect_label, &policy_id, &action_label, "", elapsed);
180 if decision.from_cache {
181 self.state.metrics().record_cache_hit();
182 } else {
183 self.state.metrics().record_cache_miss();
184 }
185
186 Ok(Response::new(PbResponse {
187 decision: matches!(decision.effect, Effect::Allow),
188 reason: decision.reasons.first().cloned().unwrap_or_default(),
189 context_json: String::new(),
190 }))
191 }
192}
193
194pub fn service(state: Arc<AppState>) -> AccessEvaluationServer<AccessEvaluationService> {
196 AccessEvaluationServer::new(AccessEvaluationService::new(state))
197 .max_decoding_message_size(MAX_GRPC_REQUEST_BYTES)
198}