summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-06-24 14:36:58 -0600
committermo khan <mo@mokhan.ca>2025-06-24 14:36:58 -0600
commit85490a4cfa7f3836d3d2f1e7cbfe48b668aa484b (patch)
tree3fb62e5566ef838187b8568f9e71d0495f24d812 /src
parenta0537b163037a92652ec92c1f47945e0572bb76e (diff)
feat: connect check service to a minimal cedar policy
Diffstat (limited to 'src')
-rw-r--r--src/authorization/authorizer.rs2
-rw-r--r--src/authorization/cedar_authorizer.rs123
-rw-r--r--src/authorization/check_service.rs29
-rw-r--r--src/main.rs8
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()