summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-07-02 12:32:27 -0600
committermo khan <mo@mokhan.ca>2025-07-02 12:32:27 -0600
commita577c62277e3d651b66fd68dbe800bf3ab5c4921 (patch)
tree7ae4e79fc84c539c12fb0313d0d3cc929b2e12ae /src
parentc2b8edab01b23fde6cc196a3349ad6aa19a93299 (diff)
parent0b610d061e45811130d8cf3919037fdc9513e340 (diff)
Merge branch 'rs' into 'main'
Re-write the authorization daemon in rust See merge request gitlab-org/software-supply-chain-security/authorization/authzd!1
Diffstat (limited to 'src')
-rw-r--r--src/authorization/authorizer.rs5
-rw-r--r--src/authorization/cedar_authorizer.rs141
-rw-r--r--src/authorization/check_service.rs35
-rw-r--r--src/authorization/mod.rs9
-rw-r--r--src/authorization/server.rs50
-rw-r--r--src/lib.rs2
-rw-r--r--src/main.rs14
7 files changed, 256 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()
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
new file mode 100644
index 00000000..3bd8fbd1
--- /dev/null
+++ b/src/lib.rs
@@ -0,0 +1,2 @@
+pub mod authorization;
+pub use authorization::{Authorizer, CedarAuthorizer, CheckService, Server};
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 00000000..8638e14b
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,14 @@
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+ tracing_subscriber::fmt().json().init();
+
+ let addr = std::env::var("BIND_ADDR")
+ .unwrap_or_else(|_| "[::1]:50051".to_string())
+ .parse()?;
+
+ log::info!("Listening on... {addr}");
+ let server = authzd::authorization::Server::new()?;
+ server.serve(addr).await?;
+
+ Ok(())
+}