diff options
| author | mo khan <mo@mokhan.ca> | 2025-06-24 14:36:58 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-06-24 14:36:58 -0600 |
| commit | 85490a4cfa7f3836d3d2f1e7cbfe48b668aa484b (patch) | |
| tree | 3fb62e5566ef838187b8568f9e71d0495f24d812 /src | |
| parent | a0537b163037a92652ec92c1f47945e0572bb76e (diff) | |
feat: connect check service to a minimal cedar policy
Diffstat (limited to 'src')
| -rw-r--r-- | src/authorization/authorizer.rs | 2 | ||||
| -rw-r--r-- | src/authorization/cedar_authorizer.rs | 123 | ||||
| -rw-r--r-- | src/authorization/check_service.rs | 29 | ||||
| -rw-r--r-- | src/main.rs | 8 |
4 files changed, 141 insertions, 21 deletions
diff --git a/src/authorization/authorizer.rs b/src/authorization/authorizer.rs index 0f700ba7..14a7df27 100644 --- a/src/authorization/authorizer.rs +++ b/src/authorization/authorizer.rs @@ -1,5 +1,5 @@ use envoy_types::ext_authz::v3::pb::CheckRequest; -pub trait Authorizer { +pub trait Authorizer: std::fmt::Debug { fn authorize(&self, request: CheckRequest) -> bool; } diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 44bc9e06..577b75ba 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -1,11 +1,28 @@ use super::authorizer::Authorizer; +use cedar_policy::{ + Authorizer as CedarAuth, Context, Entities, EntityId, EntityTypeName, EntityUid, PolicySet, + Request as CedarRequest, RestrictedExpression, +}; use envoy_types::ext_authz::v3::pb::CheckRequest; +use std::collections::HashMap; +use std::str::FromStr; -pub struct CedarAuthorizer {} +#[derive(Debug)] +pub struct CedarAuthorizer { + policies: PolicySet, + authorizer: CedarAuth, +} impl CedarAuthorizer { pub fn new() -> CedarAuthorizer { - CedarAuthorizer {} + let policy_src = include_str!("../../policies/auth_policy.cedar"); + let policies = policy_src.parse().expect("Failed to parse Cedar policies"); + let authorizer = CedarAuth::new(); + + CedarAuthorizer { + policies, + authorizer, + } } } @@ -17,21 +34,83 @@ impl Default for CedarAuthorizer { impl Authorizer for CedarAuthorizer { fn authorize(&self, request: CheckRequest) -> bool { - let headers = request + let headers = match request .attributes .as_ref() .and_then(|attr| attr.request.as_ref()) .and_then(|req| req.http.as_ref()) .map(|http| &http.headers) - .unwrap(); - - if let Some(authorization) = headers.get("authorization") { - if authorization == "Bearer valid-token" { - return true; + { + Some(headers) => headers, + None => return false, + }; + + // Extract authorization token + let bearer_token = headers + .get("authorization") + .and_then(|auth| auth.strip_prefix("Bearer ")) + .unwrap_or(""); + + // Extract request path for static asset checking + let path = headers + .get(":path") + .or_else(|| headers.get("path")) + .map_or("", |v| v.as_str()); + + // Create Cedar entities and request + match self.create_cedar_request(bearer_token, path) { + Ok(cedar_request) => { + let entities = Entities::empty(); + let response = + self.authorizer + .is_authorized(&cedar_request, &self.policies, &entities); + matches!(response.decision(), cedar_policy::Decision::Allow) } + Err(_) => false, } + } +} - false +impl CedarAuthorizer { + fn create_cedar_request( + &self, + bearer_token: &str, + path: &str, + ) -> Result<CedarRequest, Box<dyn std::error::Error>> { + // Create principal entity + let principal_id = EntityId::from_str("client")?; + let principal_type = EntityTypeName::from_str("User")?; + let principal = EntityUid::from_type_name_and_id(principal_type, principal_id); + + // Create action entity + let action_id = EntityId::from_str("check")?; + let action_type = EntityTypeName::from_str("Action")?; + let action = EntityUid::from_type_name_and_id(action_type, action_id); + + // Create resource entity + let resource_id = EntityId::from_str("resource")?; + let resource_type = EntityTypeName::from_str("Resource")?; + let resource = EntityUid::from_type_name_and_id(resource_type, resource_id); + + // Create context with bearer token and path + let mut context_map = HashMap::new(); + if !bearer_token.is_empty() { + context_map.insert( + "bearer_token".to_string(), + RestrictedExpression::from_str(&format!("\"{}\"", bearer_token))?, + ); + } + if !path.is_empty() { + context_map.insert( + "path".to_string(), + RestrictedExpression::from_str(&format!("\"{}\"", path))?, + ); + } + + let context = Context::from_pairs(context_map.into_iter().collect::<Vec<_>>())?; + + CedarRequest::new(principal, action, resource, context, None) + .map_err(|e| Box::new(e) as Box<dyn std::error::Error>) } } @@ -96,6 +175,32 @@ mod tests { assert!(!result); } + #[test] + fn test_cedar_authorizer_allows_static_assets() { + let authorizer = CedarAuthorizer::new(); + let mut headers = HashMap::new(); + headers.insert(":path".to_string(), "/static/style.css".to_string()); + let request = create_request(|item: &mut HttpRequest| { + item.headers = headers; + }); + + let result = authorizer.authorize(request); + assert!(result); + } + + #[test] + fn test_cedar_authorizer_allows_js_assets() { + let authorizer = CedarAuthorizer::new(); + let mut headers = HashMap::new(); + headers.insert(":path".to_string(), "/app.js".to_string()); + let request = create_request(|item: &mut HttpRequest| { + item.headers = headers; + }); + + let result = authorizer.authorize(request); + assert!(result); + } + // test css passthrough // test javascript passthrough // test ico passthrough diff --git a/src/authorization/check_service.rs b/src/authorization/check_service.rs index 7ca39fcd..a4d0ec7b 100644 --- a/src/authorization/check_service.rs +++ b/src/authorization/check_service.rs @@ -1,12 +1,20 @@ use envoy_types::ext_authz::v3::CheckResponseExt; use envoy_types::ext_authz::v3::pb::{CheckRequest, CheckResponse}; +use std::sync::Arc; use tonic::{Request, Response, Status}; use super::authorizer::Authorizer; -use super::cedar_authorizer::CedarAuthorizer; -#[derive(Debug, Default)] -pub struct CheckService; +#[derive(Debug)] +pub struct CheckService { + authorizer: Arc<dyn Authorizer + Send + Sync>, +} + +impl CheckService { + pub fn new(authorizer: Arc<dyn Authorizer + Send + Sync>) -> Self { + Self { authorizer } + } +} #[tonic::async_trait] impl envoy_types::ext_authz::v3::pb::Authorization for CheckService { @@ -16,8 +24,7 @@ impl envoy_types::ext_authz::v3::pb::Authorization for CheckService { ) -> Result<Response<CheckResponse>, Status> { let request = request.into_inner(); - let authorizer = CedarAuthorizer::new(); - if authorizer.authorize(request) { + if self.authorizer.authorize(request) { Ok(Response::new(CheckResponse::with_status(Status::ok("OK")))) } else { Ok(Response::new(CheckResponse::with_status( @@ -30,9 +37,11 @@ impl envoy_types::ext_authz::v3::pb::Authorization for CheckService { #[cfg(test)] mod tests { use super::*; + use super::super::cedar_authorizer::CedarAuthorizer; use envoy_types::ext_authz::v3::pb::{Authorization, CheckRequest}; use envoy_types::pb::envoy::service::auth::v3::{AttributeContext, attribute_context}; use std::collections::HashMap; + use std::sync::Arc; use tonic::Request; fn create_test_request_with_headers(headers: HashMap<String, String>) -> Request<CheckRequest> { @@ -68,7 +77,8 @@ mod tests { #[tokio::test] async fn test_check_allows_valid_bearer_token() { let token = String::from("valid-token"); - let server = CheckService::default(); + let authorizer = Arc::new(CedarAuthorizer::new()); + let server = CheckService::new(authorizer); let headers = create_headers_with_auth(&format!("Bearer {}", token)); let request = create_test_request_with_headers(headers); @@ -78,12 +88,13 @@ mod tests { let check_response = response.unwrap().into_inner(); assert!(check_response.status.is_some()); let status = check_response.status.unwrap(); - assert_eq!(status.code, tonic::Code::Ok.into()); + assert_eq!(status.code, tonic::Code::Ok as i32); } #[tokio::test] async fn test_check_denies_invalid_bearer_token() { - let server = CheckService::default(); + let authorizer = Arc::new(CedarAuthorizer::new()); + let server = CheckService::new(authorizer); let request = create_test_request_with_headers(HashMap::new()); let response = server.check(request).await; @@ -92,6 +103,6 @@ mod tests { let check_response = response.unwrap().into_inner(); assert!(check_response.status.is_some()); let status = check_response.status.unwrap(); - assert_eq!(status.code, tonic::Code::Unauthenticated.into()); + assert_eq!(status.code, tonic::Code::Unauthenticated as i32); } } diff --git a/src/main.rs b/src/main.rs index 57f98b9a..8eb7b5ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ use envoy_types::ext_authz::v3::pb::AuthorizationServer; +use std::sync::Arc; use tonic::transport::Server; pub mod authorization; -use authorization::CheckService; +use authorization::{CedarAuthorizer, CheckService}; #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { @@ -10,8 +11,11 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { let (_health_reporter, health_service) = tonic_health::server::health_reporter(); + let authorizer = Arc::new(CedarAuthorizer::new()); + let check_service = CheckService::new(authorizer); + Server::builder() - .add_service(AuthorizationServer::new(CheckService::default())) + .add_service(AuthorizationServer::new(check_service)) .add_service(health_service) .add_service( tonic_reflection::server::Builder::configure() |
