diff options
Diffstat (limited to 'src/authorization')
| -rw-r--r-- | src/authorization/authorizer.rs | 5 | ||||
| -rw-r--r-- | src/authorization/cedar_authorizer.rs | 141 | ||||
| -rw-r--r-- | src/authorization/check_service.rs | 35 | ||||
| -rw-r--r-- | src/authorization/mod.rs | 9 | ||||
| -rw-r--r-- | src/authorization/server.rs | 50 |
5 files changed, 240 insertions, 0 deletions
diff --git a/src/authorization/authorizer.rs b/src/authorization/authorizer.rs new file mode 100644 index 00000000..14a7df27 --- /dev/null +++ b/src/authorization/authorizer.rs @@ -0,0 +1,5 @@ +use envoy_types::ext_authz::v3::pb::CheckRequest; + +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 new file mode 100644 index 00000000..a877cf87 --- /dev/null +++ b/src/authorization/cedar_authorizer.rs @@ -0,0 +1,141 @@ +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::fs; +use std::str::FromStr; + +#[derive(Debug)] +pub struct CedarAuthorizer { + policies: PolicySet, + authorizer: CedarAuth, +} + +impl CedarAuthorizer { + pub fn new(policies: cedar_policy::PolicySet) -> CedarAuthorizer { + CedarAuthorizer { + policies, + authorizer: CedarAuth::new(), + } + } + + pub fn new_from(path: &std::path::Path) -> CedarAuthorizer { + Self::new(Self::load_from(path).unwrap_or_else(|_| PolicySet::default())) + } + + fn load_from(path: &std::path::Path) -> Result<PolicySet, Box<dyn std::error::Error>> { + if !path.exists() || !path.is_dir() { + return Ok(PolicySet::default()); + } + + let mut policies = PolicySet::new(); + + for entry in fs::read_dir(path)? { + let file_path = entry?.path(); + + if let Some(extension) = file_path.extension() { + if extension == "cedar" { + let content = fs::read_to_string(&file_path)?; + let file_policies = PolicySet::from_str(&content)?; + + for policy in file_policies.policies() { + policies.add(policy.clone())?; + } + } + } + } + + Ok(policies) + } +} + +impl Default for CedarAuthorizer { + fn default() -> Self { + Self::new_from(std::path::Path::new("/etc/authzd")) + } +} + +impl Authorizer for CedarAuthorizer { + fn authorize(&self, request: CheckRequest) -> bool { + 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) + { + 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, + } + } +} + +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>) + } +} diff --git a/src/authorization/check_service.rs b/src/authorization/check_service.rs new file mode 100644 index 00000000..57f7b5d5 --- /dev/null +++ b/src/authorization/check_service.rs @@ -0,0 +1,35 @@ +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; + +#[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 { + async fn check( + &self, + request: Request<CheckRequest>, + ) -> Result<Response<CheckResponse>, Status> { + if self.authorizer.authorize(request.into_inner()) { + log::info!("OK"); + Ok(Response::new(CheckResponse::with_status(Status::ok("OK")))) + } else { + log::info!("Unauthorized"); + Ok(Response::new(CheckResponse::with_status( + Status::unauthenticated("Unauthorized"), + ))) + } + } +} diff --git a/src/authorization/mod.rs b/src/authorization/mod.rs new file mode 100644 index 00000000..d664815b --- /dev/null +++ b/src/authorization/mod.rs @@ -0,0 +1,9 @@ +pub mod authorizer; +pub mod cedar_authorizer; +pub mod check_service; +pub mod server; + +pub use authorizer::Authorizer; +pub use cedar_authorizer::CedarAuthorizer; +pub use check_service::CheckService; +pub use server::Server; diff --git a/src/authorization/server.rs b/src/authorization/server.rs new file mode 100644 index 00000000..2ad270df --- /dev/null +++ b/src/authorization/server.rs @@ -0,0 +1,50 @@ +use super::cedar_authorizer::CedarAuthorizer; +use super::check_service::CheckService; +use envoy_types::ext_authz::v3::pb::AuthorizationServer; +use std::sync::Arc; + +pub struct Server { + router: tonic::transport::server::Router, +} + +impl Server { + pub fn new() -> Result<Server, Box<dyn std::error::Error>> { + let (_health_reporter, health_service) = tonic_health::server::health_reporter(); + let authorization_service = + AuthorizationServer::new(CheckService::new(Arc::new(CedarAuthorizer::default()))); + + Ok(Self::new_with(|mut builder| { + builder + .add_service(authorization_service) + .add_service(health_service) + })) + } + + pub fn new_with<F>(f: F) -> Server + where + F: FnOnce(tonic::transport::Server) -> tonic::transport::server::Router, + { + let builder = tonic::transport::Server::builder() + .trace_fn(|req| { + tracing::info_span!( + "request", + method = %req.method(), + path = %req.uri().path(), + headers = ?req.headers(), + ) + }) + .timeout(std::time::Duration::from_secs(30)); + let router = f(builder); + Server { router } + } + + pub async fn serve(self, addr: std::net::SocketAddr) -> Result<(), tonic::transport::Error> { + self.router.serve(addr).await + } +} + +impl Default for Server { + fn default() -> Self { + Self::new().unwrap() + } +} |
