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, } #[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, Box> { 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> { 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, groups: &'a mut HashSet, ) -> std::pin::Pin< Box>> + '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(()) }) } }