summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/authorization/entities.rs267
-rw-r--r--src/authorization/mod.rs2
-rw-r--r--src/bin/cli.rs60
-rw-r--r--src/lib.rs6
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(())
+}
diff --git a/src/lib.rs b/src/lib.rs
index 3bd8fbd1..cf570238 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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,
+};