From 7f253078df95ea0ec725ccbd000f11723697b64d Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 12:52:39 -0600 Subject: feat: hack together a CLI to generate an entitites.json file --- src/bin/cli.rs | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 src/bin/cli.rs (limited to 'src/bin/cli.rs') 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> { + 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(()) +} -- cgit v1.2.3 From ff30574117a996df332e23d1fb6f65259b316b5b Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 13:10:52 -0600 Subject: refactor: move functions to repository type --- src/authorization/entities.rs | 223 +++++++++++++++++++----------------------- src/authorization/mod.rs | 2 +- src/bin/cli.rs | 9 +- src/lib.rs | 3 +- 4 files changed, 108 insertions(+), 129 deletions(-) (limited to 'src/bin/cli.rs') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index 7af5fa4a..8c3f7955 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -1,6 +1,5 @@ use serde::Serialize; use std::collections::HashSet; -use std::fs; // Cedar entity structures #[derive(Debug, Serialize)] @@ -61,117 +60,118 @@ pub struct Group { pub parent_id: Option, } -pub async fn generate_entities_from_api( - token: String, - host: String, - project: String, -) -> Result, Box> { - let client = reqwest::Client::new(); - let mut entities = Vec::new(); - let mut processed_groups = HashSet::new(); +pub struct EntitiesRepository { + pub token: String, + pub host: String, + pub project: String, +} - // Fetch project information - let project_url = format!( - "{}/api/v4/projects/{}", - host.trim_end_matches('/'), - urlencoding::encode(&project) - ); +impl EntitiesRepository { + pub fn new(token: String, host: String, project: String) -> EntitiesRepository { + EntitiesRepository { + token: token, + host: host, + project: project, + } + } - println!("Fetching project information..."); - let project: Project = client - .get(&project_url) - .header("PRIVATE-TOKEN", &token) - .send() - .await? - .error_for_status()? - .json() - .await?; + pub async fn generate(&self) -> Result, Box> { + let client = reqwest::Client::new(); + let mut entities = Vec::new(); + let mut processed_groups = HashSet::new(); - // Add organization - entities.push(CedarEntity { - uid: CedarUid { - entity_type: "Organization".to_string(), - id: "1".to_string(), - }, - attrs: serde_json::json!({ - "name": "gitlab", - }), - parents: vec![], - }); + let project_url = format!( + "{}/api/v4/projects/{}", + self.host.trim_end_matches('/'), + urlencoding::encode(&self.project) + ); - // 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![] - }, - }); + let project: Project = client + .get(&project_url) + .header("PRIVATE-TOKEN", &self.token) + .send() + .await? + .error_for_status()? + .json() + .await?; - // Fetch project members - let members_url = format!( - "{}/api/v4/projects/{}/members/all", - host.trim_end_matches('/'), - project.id - ); + entities.push(CedarEntity { + uid: CedarUid { + entity_type: "Organization".to_string(), + id: "1".to_string(), + }, + attrs: serde_json::json!({ + "name": "gitlab", + }), + parents: vec![], + }); - println!("Fetching project members..."); - let members: Vec = client - .get(&members_url) - .header("PRIVATE-TOKEN", &token) - .send() - .await? - .error_for_status()? - .json() - .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![] + }, + }); + + let members_url = format!( + "{}/api/v4/projects/{}/members/all", + self.host.trim_end_matches('/'), + project.id + ); + + let members: Vec = client + .get(&members_url) + .header("PRIVATE-TOKEN", &self.token) + .send() + .await? + .error_for_status()? + .json() + .await?; - println!("Found {} members", members.len()); + 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![], + }); + } + } - // 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![], - }); + if project.namespace.kind == "group" { + fetch_group_hierarchy( + &client, + &self.host, + &self.token, + project.namespace.id, + &mut entities, + &mut processed_groups, + ) + .await?; } - } - // 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) } - - Ok(entities) } pub fn fetch_group_hierarchy<'a>( @@ -244,24 +244,3 @@ pub fn fetch_group_hierarchy<'a>( Ok(()) }) } - -pub fn write_entities_file( - entities: &[CedarEntity], - output: &str, -) -> Result<(), Box> { - 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 7e1d69b5..d687d53f 100644 --- a/src/authorization/mod.rs +++ b/src/authorization/mod.rs @@ -7,5 +7,5 @@ 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 entities::{CedarEntity, EntitiesRepository}; pub use server::Server; diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 275bd410..d0bbe989 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -1,4 +1,4 @@ -use authzd::{generate_entities_from_api, write_entities_file}; +use authzd::EntitiesRepository; use clap::{Parser, Subcommand}; #[derive(Parser, Debug)] @@ -50,9 +50,10 @@ async fn main() -> Result<(), Box> { token, host, } => { - let entities = generate_entities_from_api(token, host, project).await?; - - write_entities_file(&entities, &output)?; + let repository = EntitiesRepository::new(token, host, project); + let entities = repository.generate().await?; + let json = serde_json::to_string_pretty(&entities)?; + std::fs::write(output, json)?; } } diff --git a/src/lib.rs b/src/lib.rs index cf570238..e70e64f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,5 @@ pub mod authorization; pub use authorization::{ - Authorizer, CedarAuthorizer, CedarEntity, CheckService, Server, generate_entities_from_api, - write_entities_file, + Authorizer, CedarAuthorizer, CedarEntity, CheckService, EntitiesRepository, Server, }; -- cgit v1.2.3 From ecf43aac41e27f6546858cd98a152ac761a9afb6 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 13:13:16 -0600 Subject: refactor: rename generate to all --- src/authorization/entities.rs | 2 +- src/bin/cli.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src/bin/cli.rs') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index 8c3f7955..4824fdd8 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -75,7 +75,7 @@ impl EntitiesRepository { } } - pub async fn generate(&self) -> Result, Box> { + pub async fn all(&self) -> Result, Box> { let client = reqwest::Client::new(); let mut entities = Vec::new(); let mut processed_groups = HashSet::new(); diff --git a/src/bin/cli.rs b/src/bin/cli.rs index d0bbe989..fbd07d35 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -51,7 +51,7 @@ async fn main() -> Result<(), Box> { host, } => { let repository = EntitiesRepository::new(token, host, project); - let entities = repository.generate().await?; + let entities = repository.all().await?; let json = serde_json::to_string_pretty(&entities)?; std::fs::write(output, json)?; } -- cgit v1.2.3 From 8e297ae732660b8174703af67b574d64145bd7dc Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 14:01:26 -0600 Subject: refactor: pass project path to all() --- src/authorization/entities.rs | 11 ++++++----- src/bin/cli.rs | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) (limited to 'src/bin/cli.rs') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index 6e7fd568..6b3807d6 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -26,22 +26,23 @@ pub struct CedarParent { pub struct EntitiesRepository { api: Api, - project: String, } impl EntitiesRepository { - pub fn new(token: String, host: String, project: String) -> EntitiesRepository { + pub fn new(token: String, host: String) -> EntitiesRepository { EntitiesRepository { api: Api::new(token, host), - project, } } - pub async fn all(&self) -> Result, Box> { + 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(&self.project).await?; + let project = self.api.get_project(&project_path).await?; entities.push(CedarEntity { uid: CedarUid { diff --git a/src/bin/cli.rs b/src/bin/cli.rs index fbd07d35..c6c741a2 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -50,8 +50,8 @@ async fn main() -> Result<(), Box> { token, host, } => { - let repository = EntitiesRepository::new(token, host, project); - let entities = repository.all().await?; + let repository = EntitiesRepository::new(token, host); + let entities = repository.all(project).await?; let json = serde_json::to_string_pretty(&entities)?; std::fs::write(output, json)?; } -- cgit v1.2.3 From bc4fb74239b37c98bdf2dcd85c69c8ec05b91088 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 14:03:33 -0600 Subject: refactor: pass api client to repository ctor --- src/authorization/entities.rs | 6 ++---- src/bin/cli.rs | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) (limited to 'src/bin/cli.rs') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index 6b3807d6..c2e56bd7 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -29,10 +29,8 @@ pub struct EntitiesRepository { } impl EntitiesRepository { - pub fn new(token: String, host: String) -> EntitiesRepository { - EntitiesRepository { - api: Api::new(token, host), - } + pub fn new(api: Api) -> EntitiesRepository { + EntitiesRepository { api: api } } pub async fn all( diff --git a/src/bin/cli.rs b/src/bin/cli.rs index c6c741a2..0751ed05 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -1,4 +1,5 @@ use authzd::EntitiesRepository; +use authzd::gitlab::Api; use clap::{Parser, Subcommand}; #[derive(Parser, Debug)] @@ -50,7 +51,7 @@ async fn main() -> Result<(), Box> { token, host, } => { - let repository = EntitiesRepository::new(token, host); + let repository = EntitiesRepository::new(Api::new(token, host)); let entities = repository.all(project).await?; let json = serde_json::to_string_pretty(&entities)?; std::fs::write(output, json)?; -- cgit v1.2.3 From 8dcd2807ee81dc65e872e0d62273cdc7cee58ed2 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 14:14:54 -0600 Subject: chore: validate the generated entities.json --- src/authorization/entities.rs | 15 ++++++++++++--- src/bin/cli.rs | 9 ++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) (limited to 'src/bin/cli.rs') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index c2e56bd7..a26cace2 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -3,6 +3,10 @@ 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, @@ -62,9 +66,7 @@ impl EntitiesRepository { }, }); - let members = self.api.get_project_members(project.id).await?; - - for member in members { + for member in self.api.get_project_members(project.id).await? { if member.state == "active" { entities.push(CedarEntity { uid: CedarUid { @@ -89,6 +91,13 @@ impl EntitiesRepository { 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, diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 0751ed05..fc70ae82 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -53,8 +53,15 @@ async fn main() -> Result<(), Box> { } => { 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)?; + std::fs::write(&output, json)?; + + println!( + "Successfully generated {} entities to {}", + entities.len(), + output + ); } } -- cgit v1.2.3