diff options
| author | mo khan <mo@mokhan.ca> | 2025-07-10 12:52:39 -0600 |
|---|---|---|
| committer | mo khan <mo@mokhan.ca> | 2025-07-10 12:52:39 -0600 |
| commit | 7f253078df95ea0ec725ccbd000f11723697b64d (patch) | |
| tree | 388e9f1298ccf394cfff32e4ad5a4f8090aa7fd4 /src | |
| parent | ffed217e230f6c6e725383c00900c5d5b4981f6c (diff) | |
feat: hack together a CLI to generate an entitites.json file
Diffstat (limited to 'src')
| -rw-r--r-- | src/authorization/entities.rs | 267 | ||||
| -rw-r--r-- | src/authorization/mod.rs | 2 | ||||
| -rw-r--r-- | src/bin/cli.rs | 60 | ||||
| -rw-r--r-- | src/lib.rs | 6 |
4 files changed, 334 insertions, 1 deletions
diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs new file mode 100644 index 00000000..7af5fa4a --- /dev/null +++ b/src/authorization/entities.rs @@ -0,0 +1,267 @@ +use serde::Serialize; +use std::collections::HashSet; +use std::fs; + +// Cedar entity structures +#[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, +} + +// API structures +#[derive(Debug, serde::Deserialize)] +pub struct Project { + pub id: u64, + pub name: String, + pub path: String, + pub namespace: Namespace, +} + +#[derive(Debug, serde::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>, +} + +#[derive(Debug, serde::Deserialize)] +pub struct Member { + pub id: u64, + pub username: String, + pub name: String, + pub state: String, + pub access_level: u8, +} + +#[derive(Debug, serde::Deserialize)] +pub struct Group { + pub id: u64, + pub name: String, + pub path: String, + pub full_path: String, + pub parent_id: Option<u64>, +} + +pub async fn generate_entities_from_api( + token: String, + host: String, + project: String, +) -> Result<Vec<CedarEntity>, Box<dyn std::error::Error>> { + let client = reqwest::Client::new(); + let mut entities = Vec::new(); + let mut processed_groups = HashSet::new(); + + // Fetch project information + let project_url = format!( + "{}/api/v4/projects/{}", + host.trim_end_matches('/'), + urlencoding::encode(&project) + ); + + println!("Fetching project information..."); + let project: Project = client + .get(&project_url) + .header("PRIVATE-TOKEN", &token) + .send() + .await? + .error_for_status()? + .json() + .await?; + + // Add organization + entities.push(CedarEntity { + uid: CedarUid { + entity_type: "Organization".to_string(), + id: "1".to_string(), + }, + attrs: serde_json::json!({ + "name": "gitlab", + }), + parents: vec![], + }); + + // Add project entity + 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![] + }, + }); + + // Fetch project members + let members_url = format!( + "{}/api/v4/projects/{}/members/all", + host.trim_end_matches('/'), + project.id + ); + + println!("Fetching project members..."); + let members: Vec<Member> = client + .get(&members_url) + .header("PRIVATE-TOKEN", &token) + .send() + .await? + .error_for_status()? + .json() + .await?; + + println!("Found {} members", members.len()); + + // Add user entities + for member in members { + 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, + "name": member.name, + "access_level": member.access_level, + }), + parents: vec![], + }); + } + } + + // Fetch group hierarchy if project belongs to a group + if project.namespace.kind == "group" { + println!("Fetching group hierarchy..."); + fetch_group_hierarchy( + &client, + &host, + &token, + project.namespace.id, + &mut entities, + &mut processed_groups, + ) + .await?; + } + + Ok(entities) +} + +pub fn fetch_group_hierarchy<'a>( + client: &'a reqwest::Client, + api_url: &'a str, + token: &'a str, + group_id: u64, + entities: &'a mut Vec<CedarEntity>, + processed_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 processed_groups.contains(&group_id) { + return Ok(()); + } + + processed_groups.insert(group_id); + + let group_url = format!( + "{}/api/v4/groups/{}", + api_url.trim_end_matches('/'), + group_id + ); + + let group: Group = client + .get(&group_url) + .header("PRIVATE-TOKEN", token) + .send() + .await? + .error_for_status()? + .json() + .await?; + + let parents = if let Some(parent_id) = group.parent_id { + // Recursively fetch parent group + fetch_group_hierarchy( + client, + api_url, + token, + parent_id, + entities, + processed_groups, + ) + .await?; + vec![CedarParent { + parent_type: "Group".to_string(), + id: parent_id.to_string(), + }] + } else { + // Top-level group belongs to organization + vec![CedarParent { + parent_type: "Organization".to_string(), + id: "1".to_string(), + }] + }; + + 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(()) + }) +} + +pub fn write_entities_file( + entities: &[CedarEntity], + output: &str, +) -> Result<(), Box<dyn std::error::Error>> { + let json = serde_json::to_string_pretty(&entities)?; + fs::write(output, json)?; + + println!( + "\nSuccessfully wrote {} entities to {}", + entities.len(), + output + ); + println!("\nTo use these entities with Cedar:"); + println!( + " let entities = cedar_policy::Entities::from_json_file(\"{}\", None)?;", + output + ); + + Ok(()) +} diff --git a/src/authorization/mod.rs b/src/authorization/mod.rs index d664815b..7e1d69b5 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, generate_entities_from_api, write_entities_file}; pub use server::Server; diff --git a/src/bin/cli.rs b/src/bin/cli.rs new file mode 100644 index 00000000..275bd410 --- /dev/null +++ b/src/bin/cli.rs @@ -0,0 +1,60 @@ +use authzd::{generate_entities_from_api, write_entities_file}; +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, + }, +} + +#[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 entities = generate_entities_from_api(token, host, project).await?; + + write_entities_file(&entities, &output)?; + } + } + + Ok(()) +} @@ -1,2 +1,6 @@ pub mod authorization; -pub use authorization::{Authorizer, CedarAuthorizer, CheckService, Server}; + +pub use authorization::{ + Authorizer, CedarAuthorizer, CedarEntity, CheckService, Server, generate_entities_from_api, + write_entities_file, +}; |
