diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/authorization/authorizer.rs | 5 | ||||
| -rw-r--r-- | src/authorization/cedar_authorizer.rs | 183 | ||||
| -rw-r--r-- | src/authorization/check_service.rs | 33 | ||||
| -rw-r--r-- | src/authorization/entities.rs | 145 | ||||
| -rw-r--r-- | src/authorization/mod.rs | 11 | ||||
| -rw-r--r-- | src/authorization/server.rs | 42 | ||||
| -rw-r--r-- | src/bin/cli.rs | 94 | ||||
| -rw-r--r-- | src/gitlab/api.rs | 53 | ||||
| -rw-r--r-- | src/gitlab/group.rs | 10 | ||||
| -rw-r--r-- | src/gitlab/member.rs | 9 | ||||
| -rw-r--r-- | src/gitlab/mod.rs | 11 | ||||
| -rw-r--r-- | src/gitlab/namespace.rs | 11 | ||||
| -rw-r--r-- | src/gitlab/project.rs | 11 | ||||
| -rw-r--r-- | src/lib.rs | 6 |
14 files changed, 0 insertions, 624 deletions
diff --git a/src/authorization/authorizer.rs b/src/authorization/authorizer.rs deleted file mode 100644 index 62733585..00000000 --- a/src/authorization/authorizer.rs +++ /dev/null @@ -1,5 +0,0 @@ -use envoy_types::ext_authz::v3::pb::CheckRequest; - -pub trait Authorizer: std::fmt::Debug + std::marker::Sync + std::marker::Send + 'static { - fn authorize(&self, request: CheckRequest) -> bool; -} diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs deleted file mode 100644 index 0d30ee77..00000000 --- a/src/authorization/cedar_authorizer.rs +++ /dev/null @@ -1,183 +0,0 @@ -use super::authorizer::Authorizer; -use std::fs; -use std::str::FromStr; - -#[derive(Debug)] -pub struct CedarAuthorizer { - authorizer: cedar_policy::Authorizer, - entities: cedar_policy::Entities, - policies: cedar_policy::PolicySet, -} - -impl CedarAuthorizer { - pub fn new( - policies: cedar_policy::PolicySet, - entities: cedar_policy::Entities, - ) -> CedarAuthorizer { - CedarAuthorizer { - policies, - entities, - authorizer: cedar_policy::Authorizer::new(), - } - } - - pub fn new_from(path: &std::path::Path, entities: cedar_policy::Entities) -> CedarAuthorizer { - Self::new( - Self::load_from(path).unwrap_or_else(|e| { - tracing::error!( - path = ?path, - error = %e, - "Failed to load Cedar policies, using empty policy set" - ); - cedar_policy::PolicySet::default() - }), - entities, - ) - } - - fn load_from( - path: &std::path::Path, - ) -> Result<cedar_policy::PolicySet, Box<dyn std::error::Error>> { - if !path.exists() { - return Ok(cedar_policy::PolicySet::default()); - } - - if path.is_file() && path.extension().is_some_and(|ext| ext == "cedar") { - let content = fs::read_to_string(path)?; - return Ok(cedar_policy::PolicySet::from_str(&content)?); - } - - if !path.is_dir() { - return Ok(cedar_policy::PolicySet::default()); - } - - let mut policies = cedar_policy::PolicySet::new(); - for entry in fs::read_dir(path)? { - policies.merge(&Self::load_from(&entry?.path())?, true)?; - } - Ok(policies) - } - - fn map_from( - &self, - http_request: envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, - ) -> Result<cedar_policy::Request, Box<dyn std::error::Error>> { - let principal = self.principal_from(&http_request)?; - let permission = self.permission_from(&http_request)?; - let resource = self.resource_from(&http_request)?; - let context = self.context_from(http_request)?; - - Ok(cedar_policy::Request::new( - principal, permission, resource, context, None, - )?) - } - - fn principal_from( - &self, - http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, - ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> { - let subject = http_request - .headers - .get("x-jwt-claim-sub") - .map_or("", |v| v); - - Ok(cedar_policy::EntityUid::from_type_name_and_id( - cedar_policy::EntityTypeName::from_str("User")?, - cedar_policy::EntityId::from_str(subject)?, - )) - } - - fn permission_from( - &self, - http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, - ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> { - Ok(cedar_policy::EntityUid::from_type_name_and_id( - cedar_policy::EntityTypeName::from_str("Action")?, - cedar_policy::EntityId::from_str(&http_request.method)?, - )) - } - - fn resource_from( - &self, - http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, - ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> { - Ok(cedar_policy::EntityUid::from_type_name_and_id( - cedar_policy::EntityTypeName::from_str("Resource")?, - cedar_policy::EntityId::from_str(&http_request.path)?, - )) - } - - fn context_from( - &self, - http_request: envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, - ) -> Result<cedar_policy::Context, Box<dyn std::error::Error>> { - let mut items = std::collections::HashMap::new(); - - items.insert("host".to_string(), self.safe_string(&http_request.host)); - items.insert("method".to_string(), self.safe_string(&http_request.method)); - items.insert("path".to_string(), self.safe_string(&http_request.path)); - - Ok(cedar_policy::Context::from_pairs( - items.into_iter().collect::<Vec<_>>(), - )?) - } - - fn safe_string(&self, item: &str) -> cedar_policy::RestrictedExpression { - cedar_policy::RestrictedExpression::new_string(item.to_string()) - } -} - -impl Default for CedarAuthorizer { - fn default() -> Self { - Self::new_from( - std::path::Path::new("./etc/authzd"), - cedar_policy::Entities::empty(), - ) - } -} - -impl Authorizer for CedarAuthorizer { - fn authorize(&self, request: envoy_types::ext_authz::v3::pb::CheckRequest) -> bool { - let http_request = match request - .attributes - .as_ref() - .and_then(|attr| attr.request.as_ref()) - .and_then(|req| req.http.as_ref()) - { - Some(http) => http, - None => return false, - }; - - match self.map_from(http_request.clone()) { - Ok(cedar_request) => { - let response = - self.authorizer - .is_authorized(&cedar_request, &self.policies, &self.entities); - - let decision = response.decision(); - - tracing::info!( - decision = ?decision, - diagnostics = ?response.diagnostics(), - principal = %cedar_request.principal().unwrap(), - action = %cedar_request.action().unwrap(), - resource = %cedar_request.resource().unwrap(), - host = %http_request.host, - method = %http_request.method, - path = %http_request.path, - "http" - ); - - matches!(decision, cedar_policy::Decision::Allow) - } - Err(e) => { - tracing::error!( - error = %e, - path = %http_request.path, - "Failed to create Cedar request" - ); - false - } - } - } -} diff --git a/src/authorization/check_service.rs b/src/authorization/check_service.rs deleted file mode 100644 index f8c7577f..00000000 --- a/src/authorization/check_service.rs +++ /dev/null @@ -1,33 +0,0 @@ -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()) { - Ok(Response::new(CheckResponse::with_status(Status::ok("OK")))) - } else { - Ok(Response::new(CheckResponse::with_status( - Status::unauthenticated("Unauthorized"), - ))) - } - } -} diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs deleted file mode 100644 index 050f6f26..00000000 --- a/src/authorization/entities.rs +++ /dev/null @@ -1,145 +0,0 @@ -use crate::gitlab::Api; -use serde::Serialize; -use std::collections::HashSet; -use std::future::Future; -use std::pin::Pin; - -type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>; - -// Cedar entity structures -// Note: We define custom types instead of using cedar_policy::Entity directly because: -// 1. Cedar's Entity type is for runtime use, not JSON serialization -// 2. These types ensure our JSON output matches Cedar's expected format exactly -// 3. The #[serde(rename)] attributes handle Cedar's specific field naming requirements -#[derive(Debug, Serialize)] -pub struct CedarEntity { - pub uid: CedarUid, - pub attrs: serde_json::Value, - pub parents: Vec<CedarParent>, -} - -#[derive(Debug, Serialize)] -pub struct CedarUid { - #[serde(rename = "type")] - pub entity_type: String, - pub id: String, -} - -#[derive(Debug, Serialize)] -pub struct CedarParent { - #[serde(rename = "type")] - pub parent_type: String, - pub id: String, -} - -pub struct EntitiesRepository { - api: Api, -} - -impl EntitiesRepository { - pub fn new(api: Api) -> EntitiesRepository { - EntitiesRepository { api } - } - - pub async fn all( - &self, - project_path: String, - ) -> Result<Vec<CedarEntity>, Box<dyn std::error::Error>> { - let mut entities = Vec::new(); - let mut groups = HashSet::new(); - - let project = self.api.get_project(&project_path).await?; - - entities.push(CedarEntity { - uid: CedarUid { - entity_type: "Project".to_string(), - id: project.id.to_string(), - }, - attrs: serde_json::json!({ - "name": project.name, - "path": project.path, - "full_path": format!("{}/{}", project.namespace.full_path, project.path), - }), - parents: if project.namespace.kind == "group" { - vec![CedarParent { - parent_type: "Group".to_string(), - id: project.namespace.id.to_string(), - }] - } else { - vec![] - }, - }); - - for member in self.api.get_project_members(project.id).await? { - if member.state == "active" { - entities.push(CedarEntity { - uid: CedarUid { - entity_type: "User".to_string(), - id: member.id.to_string(), - }, - attrs: serde_json::json!({ - "username": member.username, - "access_level": member.access_level, - }), - parents: vec![], - }); - } - } - - if project.namespace.kind == "group" { - self.fetch_hierarchy(project.namespace.id, &mut entities, &mut groups) - .await?; - } - - Ok(entities) - } - - /// Validates that the entities can be parsed by Cedar - pub fn is_valid(entities: &[CedarEntity]) -> Result<(), Box<dyn std::error::Error>> { - let json = serde_json::to_string(entities)?; - cedar_policy::Entities::from_json_str(&json, None)?; - Ok(()) - } - - fn fetch_hierarchy<'a>( - &'a self, - group_id: u64, - entities: &'a mut Vec<CedarEntity>, - groups: &'a mut HashSet<u64>, - ) -> BoxFuture<'a, Result<(), Box<dyn std::error::Error>>> { - Box::pin(async move { - if groups.contains(&group_id) { - return Ok(()); - } - - groups.insert(group_id); - - let group = self.api.get_group(group_id).await?; - - let parents = if let Some(parent_id) = group.parent_id { - self.fetch_hierarchy(parent_id, entities, groups).await?; - vec![CedarParent { - parent_type: "Group".to_string(), - id: parent_id.to_string(), - }] - } else { - vec![] - }; - - entities.push(CedarEntity { - uid: CedarUid { - entity_type: "Group".to_string(), - id: group.id.to_string(), - }, - attrs: serde_json::json!({ - "name": group.name, - "path": group.path, - "full_path": group.full_path, - }), - parents, - }); - - Ok(()) - }) - } -} diff --git a/src/authorization/mod.rs b/src/authorization/mod.rs deleted file mode 100644 index d687d53f..00000000 --- a/src/authorization/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod authorizer; -pub mod cedar_authorizer; -pub mod check_service; -pub mod entities; -pub mod server; - -pub use authorizer::Authorizer; -pub use cedar_authorizer::CedarAuthorizer; -pub use check_service::CheckService; -pub use entities::{CedarEntity, EntitiesRepository}; -pub use server::Server; diff --git a/src/authorization/server.rs b/src/authorization/server.rs deleted file mode 100644 index 31bf2af8..00000000 --- a/src/authorization/server.rs +++ /dev/null @@ -1,42 +0,0 @@ -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<T: super::Authorizer>(authorizer: T) -> Result<Server, Box<dyn std::error::Error>> { - let authorization_service = - AuthorizationServer::new(CheckService::new(Arc::new(authorizer))); - - Ok(Self::new_with(|mut builder| { - builder.add_service(authorization_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!("rpc", method = %req.method(), path = %req.uri().path()), - ) - .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(CedarAuthorizer::default()).unwrap() - } -} diff --git a/src/bin/cli.rs b/src/bin/cli.rs deleted file mode 100644 index 78aa1ba1..00000000 --- a/src/bin/cli.rs +++ /dev/null @@ -1,94 +0,0 @@ -use authzd::EntitiesRepository; -use authzd::gitlab::Api; -use clap::{Parser, Subcommand}; - -#[derive(Parser, Debug)] -#[command( - author, - version, - about = "Authorization CLI for managing Cedar entities and policies" -)] -struct Args { - #[command(subcommand)] - command: Commands, -} - -#[derive(Subcommand, Debug)] -enum Commands { - /// Generate entities from GitLab API - Generate { - /// Project ID or path (e.g., gitlab-org/gitlab) - #[arg(short, long)] - project: String, - - /// Output file path - #[arg(short, long, default_value = "entities.json")] - output: String, - - /// GitLab API token - #[arg(short, long, env = "GITLAB_TOKEN")] - token: String, - - /// GitLab instance URL - #[arg( - short = 'H', - long, - env = "GITLAB_HOST", - default_value = "https://gitlab.com" - )] - host: String, - }, - Server { - /// Address to bind to - #[arg(short, long, env = "BIND_ADDR", default_value = "127.0.0.1:50052")] - addr: String, - }, -} - -#[tokio::main] -async fn main() -> Result<(), Box<dyn std::error::Error>> { - let args = Args::parse(); - - match args.command { - Commands::Generate { - project, - output, - token, - host, - } => { - let repository = EntitiesRepository::new(Api::new(token, host)); - let entities = repository.all(project).await?; - EntitiesRepository::is_valid(&entities)?; - let json = serde_json::to_string_pretty(&entities)?; - std::fs::write(&output, json)?; - - println!( - "Successfully generated {} entities to {}", - entities.len(), - output - ); - } - Commands::Server { addr } => { - tracing_subscriber::fmt() - .json() - .with_ansi(false) - .with_current_span(true) - .with_file(true) - .with_level(false) - .with_line_number(true) - .with_max_level(tracing::Level::INFO) - .with_span_list(true) - .with_target(false) - .with_thread_ids(false) - .with_thread_names(false) - .init(); - - tracing::info!(address = %addr, "Starting"); - authzd::authorization::Server::new(authzd::authorization::CedarAuthorizer::default())? - .serve(addr.parse().unwrap()) - .await?; - } - } - - Ok(()) -} diff --git a/src/gitlab/api.rs b/src/gitlab/api.rs deleted file mode 100644 index 9047ddaf..00000000 --- a/src/gitlab/api.rs +++ /dev/null @@ -1,53 +0,0 @@ -use crate::gitlab::{Group, Member, Project}; -use reqwest::Client; - -pub struct Api { - pub token: String, - pub host: String, - client: Client, -} - -impl Api { - pub fn new(token: String, host: String) -> Api { - Api { - token, - host, - client: Client::new(), - } - } - - pub async fn get_project(&self, project: &str) -> Result<Project, Box<dyn std::error::Error>> { - self.get::<Project>(format!("/api/v4/projects/{}", urlencoding::encode(project))) - .await - } - - pub async fn get_project_members( - &self, - project_id: u64, - ) -> Result<Vec<Member>, Box<dyn std::error::Error>> { - self.get::<Vec<Member>>(format!("/api/v4/projects/{project_id}/members/all")) - .await - } - - pub async fn get_group(&self, group_id: u64) -> Result<Group, Box<dyn std::error::Error>> { - self.get::<Group>(format!("/api/v4/groups/{group_id}")) - .await - } - - async fn get<T: serde::de::DeserializeOwned>( - &self, - path: String, - ) -> Result<T, Box<dyn std::error::Error>> { - let url = format!("{}{}", self.host.trim_end_matches('/'), path); - - Ok(self - .client - .get(&url) - .header("PRIVATE-TOKEN", &self.token) - .send() - .await? - .error_for_status()? - .json() - .await?) - } -} diff --git a/src/gitlab/group.rs b/src/gitlab/group.rs deleted file mode 100644 index 6b00e87d..00000000 --- a/src/gitlab/group.rs +++ /dev/null @@ -1,10 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct Group { - pub id: u64, - pub name: String, - pub path: String, - pub full_path: String, - pub parent_id: Option<u64>, -} diff --git a/src/gitlab/member.rs b/src/gitlab/member.rs deleted file mode 100644 index b44b88f2..00000000 --- a/src/gitlab/member.rs +++ /dev/null @@ -1,9 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct Member { - pub id: u64, - pub username: String, - pub state: String, - pub access_level: u64, -} diff --git a/src/gitlab/mod.rs b/src/gitlab/mod.rs deleted file mode 100644 index e1993d81..00000000 --- a/src/gitlab/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod api; -pub mod group; -pub mod member; -pub mod namespace; -pub mod project; - -pub use api::Api; -pub use group::Group; -pub use member::Member; -pub use namespace::Namespace; -pub use project::Project; diff --git a/src/gitlab/namespace.rs b/src/gitlab/namespace.rs deleted file mode 100644 index d4a1e8f4..00000000 --- a/src/gitlab/namespace.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serde::Deserialize; - -#[derive(Debug, Deserialize)] -pub struct Namespace { - pub id: u64, - pub name: String, - pub path: String, - pub kind: String, - pub full_path: String, - pub parent_id: Option<u64>, -} diff --git a/src/gitlab/project.rs b/src/gitlab/project.rs deleted file mode 100644 index ba88c2e3..00000000 --- a/src/gitlab/project.rs +++ /dev/null @@ -1,11 +0,0 @@ -use serde::Deserialize; - -use super::Namespace; - -#[derive(Debug, Deserialize)] -pub struct Project { - pub id: u64, - pub name: String, - pub path: String, - pub namespace: Namespace, -} diff --git a/src/lib.rs b/src/lib.rs deleted file mode 100644 index 3681a859..00000000 --- a/src/lib.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod authorization; -pub mod gitlab; - -pub use authorization::{ - Authorizer, CedarAuthorizer, CedarEntity, CheckService, EntitiesRepository, Server, -}; |
