diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-10 17:49:29 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-10 17:49:29 -0600 |
| commit | ef572ae666732e87a35417710669ce88233a754a (patch) | |
| tree | 3cc32004dee9600014417d404dbe01ac0e1faca9 /src/authorization | |
| parent | 8417a15087cc6f42c77fe070011ac2207f8d852d (diff) | |
| parent | 6721aaffa33894624c87a54f4ed10eccd3c080e5 (diff) | |
Merge branch 'entities' into 'main'
Use a static ACL file(s) to make authorization decisions
See merge request gitlab-org/software-supply-chain-security/authorization/authzd!6
Diffstat (limited to 'src/authorization')
| -rw-r--r-- | src/authorization/cedar_authorizer.rs | 67 | ||||
| -rw-r--r-- | src/authorization/entities.rs | 143 | ||||
| -rw-r--r-- | src/authorization/mod.rs | 2 |
3 files changed, 176 insertions, 36 deletions
diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 64287414..662aafeb 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -23,7 +23,14 @@ impl CedarAuthorizer { pub fn new_from(path: &std::path::Path, entities: cedar_policy::Entities) -> CedarAuthorizer { Self::new( - Self::load_from(path).unwrap_or_else(|_| cedar_policy::PolicySet::default()), + 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, ) } @@ -31,27 +38,24 @@ impl CedarAuthorizer { fn load_from( path: &std::path::Path, ) -> Result<cedar_policy::PolicySet, Box<dyn std::error::Error>> { - if !path.exists() || !path.is_dir() { + if !path.exists() { return Ok(cedar_policy::PolicySet::default()); } - let mut policies = cedar_policy::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 = cedar_policy::PolicySet::from_str(&content)?; + if path.is_file() && path.extension().map_or(false, |ext| ext == "cedar") { + let content = fs::read_to_string(&path)?; + return Ok(cedar_policy::PolicySet::from_str(&content)?); + } - for policy in file_policies.policies() { - policies.add(policy.clone())?; - } - } - } + if !path.is_dir() { + return Ok(cedar_policy::PolicySet::default()); } - Ok(policies) + let mut policies = cedar_policy::PolicySet::new(); + for entry in fs::read_dir(path)? { + policies.merge(&Self::load_from(&entry?.path())?, true)?; + } + return Ok(policies); } fn map_from( @@ -70,31 +74,36 @@ impl CedarAuthorizer { fn principal_from( &self, - _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, + 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("client")?, + cedar_policy::EntityId::from_str(subject)?, )) } fn permission_from( &self, - _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, + 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("check")?, + cedar_policy::EntityId::from_str(&http_request.method)?, )) } fn resource_from( &self, - _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, + 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("resource")?, + cedar_policy::EntityId::from_str(&http_request.path)?, )) } @@ -104,7 +113,6 @@ impl CedarAuthorizer { ) -> Result<cedar_policy::Context, Box<dyn std::error::Error>> { let mut items = std::collections::HashMap::new(); - items.insert("bearer_token".to_string(), self.token_from(&http_request)); 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)); @@ -114,19 +122,6 @@ impl CedarAuthorizer { )?) } - fn token_from( - &self, - http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, - ) -> cedar_policy::RestrictedExpression { - let bearer_token = &http_request - .headers - .get("authorization") - .and_then(|auth| auth.strip_prefix("Bearer ")) - .unwrap_or(""); - - self.safe_string(bearer_token) - } - fn safe_string(&self, item: &str) -> cedar_policy::RestrictedExpression { cedar_policy::RestrictedExpression::new_string(item.to_string()) } diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs new file mode 100644 index 00000000..fc1246d7 --- /dev/null +++ b/src/authorization/entities.rs @@ -0,0 +1,143 @@ +use crate::gitlab::Api; +use serde::Serialize; +use std::collections::HashSet; + +// 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: 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>, + ) -> std::pin::Pin< + Box<dyn std::future::Future<Output = Result<(), Box<dyn std::error::Error>>> + 'a>, + > { + 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 index d664815b..d687d53f 100644 --- a/src/authorization/mod.rs +++ b/src/authorization/mod.rs @@ -1,9 +1,11 @@ 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; |
