summaryrefslogtreecommitdiff
path: root/src/authorization
diff options
context:
space:
mode:
authormo khan <mo@mokhan.ca>2025-07-10 17:49:29 -0600
committermo khan <mo@mokhan.ca>2025-07-10 17:49:29 -0600
commitef572ae666732e87a35417710669ce88233a754a (patch)
tree3cc32004dee9600014417d404dbe01ac0e1faca9 /src/authorization
parent8417a15087cc6f42c77fe070011ac2207f8d852d (diff)
parent6721aaffa33894624c87a54f4ed10eccd3c080e5 (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.rs67
-rw-r--r--src/authorization/entities.rs143
-rw-r--r--src/authorization/mod.rs2
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;