From bb20adc3b469ddbd89a881b188dc50bf32658a81 Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 9 Jul 2025 16:28:50 -0600 Subject: chore: print error when policies cannot be loaded --- src/authorization/cedar_authorizer.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 64287414..51061648 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, ) } -- cgit v1.2.3 From 9c62299e87a89b48e829eca0491076a82d61263e Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 9 Jul 2025 17:17:14 -0600 Subject: fix: use PolicySet#merge to merge policies from multiple files --- src/authorization/cedar_authorizer.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 51061648..230c10a8 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -50,10 +50,7 @@ impl CedarAuthorizer { if extension == "cedar" { let content = fs::read_to_string(&file_path)?; let file_policies = cedar_policy::PolicySet::from_str(&content)?; - - for policy in file_policies.policies() { - policies.add(policy.clone())?; - } + policies.merge(&file_policies, true)?; } } } -- cgit v1.2.3 From c203a32775e10f9a5a8e474ab36631b95cf13bb3 Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 9 Jul 2025 17:27:10 -0600 Subject: refactor: recursively load files and directories --- src/authorization/cedar_authorizer.rs | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 230c10a8..5ed810a7 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -38,24 +38,29 @@ impl CedarAuthorizer { fn load_from( path: &std::path::Path, ) -> Result> { - 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 path.is_file() { + if let Some(extension) = path.extension() { if extension == "cedar" { - let content = fs::read_to_string(&file_path)?; - let file_policies = cedar_policy::PolicySet::from_str(&content)?; - policies.merge(&file_policies, true)?; + let content = fs::read_to_string(&path)?; + return Ok(cedar_policy::PolicySet::from_str(&content)?); } } } - Ok(policies) + if !path.is_dir() { + 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(); + policies.merge(&Self::load_from(&file_path)?, true)?; + } + return Ok(policies); } fn map_from( -- cgit v1.2.3 From 350be4f0dc862ea9b02a492356280ff63d4ace4e Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 9 Jul 2025 17:30:07 -0600 Subject: refactor: inline file_path variable --- src/authorization/cedar_authorizer.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 5ed810a7..f86bcf03 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -57,8 +57,7 @@ impl CedarAuthorizer { let mut policies = cedar_policy::PolicySet::new(); for entry in fs::read_dir(path)? { - let file_path = entry?.path(); - policies.merge(&Self::load_from(&file_path)?, true)?; + policies.merge(&Self::load_from(&entry?.path())?, true)?; } return Ok(policies); } -- cgit v1.2.3 From 392874038d2ac78796bd8bdceaee6ce18e11c648 Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 9 Jul 2025 17:40:28 -0600 Subject: refactor: use map_or instead of match expression --- src/authorization/cedar_authorizer.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index f86bcf03..6d729772 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -43,11 +43,9 @@ impl CedarAuthorizer { } if path.is_file() { - if let Some(extension) = path.extension() { - if extension == "cedar" { - let content = fs::read_to_string(&path)?; - return Ok(cedar_policy::PolicySet::from_str(&content)?); - } + if path.extension().map_or(false, |ext| ext == "cedar") { + let content = fs::read_to_string(&path)?; + return Ok(cedar_policy::PolicySet::from_str(&content)?); } } -- cgit v1.2.3 From c50336c0bd581c7e9c5799188212e81318f829a2 Mon Sep 17 00:00:00 2001 From: mo khan Date: Wed, 9 Jul 2025 17:43:19 -0600 Subject: refactor: combine conditionals --- src/authorization/cedar_authorizer.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 6d729772..c6b886ec 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -42,11 +42,9 @@ impl CedarAuthorizer { return Ok(cedar_policy::PolicySet::default()); } - if path.is_file() { - if path.extension().map_or(false, |ext| ext == "cedar") { - let content = fs::read_to_string(&path)?; - return Ok(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)?); } if !path.is_dir() { -- cgit v1.2.3 From 539cf6a187637783ae11becfa9d7b2d5faba4c24 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 08:22:16 -0600 Subject: feat: extract JWT subject claim header --- src/authorization/cedar_authorizer.rs | 9 +++++++-- tests/authorization/cedar_authorizer_test.rs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index c6b886ec..ceaee51c 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -74,11 +74,16 @@ 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> { + 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)?, )) } diff --git a/tests/authorization/cedar_authorizer_test.rs b/tests/authorization/cedar_authorizer_test.rs index 8add9868..0cffeb13 100644 --- a/tests/authorization/cedar_authorizer_test.rs +++ b/tests/authorization/cedar_authorizer_test.rs @@ -74,7 +74,7 @@ mod tests { #[test] fn test_authenticated_create_sparkle() { let request = build_request(|item: &mut HttpRequest| { - item.method = "GET".to_string(); + item.method = "POST".to_string(); item.path = "/sparkles".to_string(); item.host = "sparkle.staging.runway.gitlab.net".to_string(); item.headers = build_headers(vec![ -- cgit v1.2.3 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 --- Cargo.lock | 813 +++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 9 + src/authorization/entities.rs | 267 ++++++++++++++ src/authorization/mod.rs | 2 + src/bin/cli.rs | 60 ++++ src/lib.rs | 6 +- 6 files changed, 1155 insertions(+), 2 deletions(-) create mode 100644 src/authorization/entities.rs create mode 100644 src/bin/cli.rs (limited to 'src') diff --git a/Cargo.lock b/Cargo.lock index 4c657df8..d9f7eca9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,6 +41,56 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anstyle-parse" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.59.0", +] + [[package]] name = "anyhow" version = "1.0.98" @@ -106,9 +156,13 @@ name = "authzd" version = "0.1.0" dependencies = [ "cedar-policy", + "clap", "envoy-types", "log", "please", + "reqwest", + "serde", + "serde_json", "tokio", "tokio-stream", "tokio-test", @@ -118,6 +172,7 @@ dependencies = [ "tonic-reflection", "tracing", "tracing-subscriber", + "urlencoding", ] [[package]] @@ -372,6 +427,62 @@ dependencies = [ "windows-link", ] +[[package]] +name = "clap" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -452,6 +563,17 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dyn-clone" version = "1.0.19" @@ -485,6 +607,15 @@ dependencies = [ "log", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "enum-ordinalize" version = "4.3.0" @@ -556,6 +687,30 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -605,6 +760,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.3.3" @@ -742,6 +908,22 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + [[package]] name = "hyper-timeout" version = "0.5.2" @@ -755,12 +937,29 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ + "base64", "bytes", "futures-channel", "futures-core", @@ -768,12 +967,16 @@ dependencies = [ "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] @@ -800,12 +1003,119 @@ dependencies = [ "cc", ] +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -828,6 +1138,28 @@ dependencies = [ "serde", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.12.1" @@ -921,6 +1253,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + [[package]] name = "lock_api" version = "0.4.13" @@ -1038,6 +1376,23 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -1093,6 +1448,56 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "overload" version = "0.1.1" @@ -1195,11 +1600,26 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + [[package]] name = "please" version = "0.1.0" source = "git+https://github.com/xlgmokha/please.git#bff13caf9ee2f806dd32c5d77f0e4ac9eb28c7f5" +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1409,6 +1829,60 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rustc-demangle" version = "0.1.25" @@ -1446,6 +1920,39 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "rustls" +version = "0.23.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.21" @@ -1467,6 +1974,15 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "schemars" version = "0.9.0" @@ -1485,6 +2001,29 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "semver" version = "1.0.26" @@ -1524,6 +2063,18 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.13.0" @@ -1618,6 +2169,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "stacker" version = "0.1.21" @@ -1649,6 +2206,12 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.104" @@ -1665,6 +2228,41 @@ name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] [[package]] name = "tempfile" @@ -1673,7 +2271,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -1749,6 +2347,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.9.0" @@ -1791,6 +2399,26 @@ dependencies = [ "syn", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.17" @@ -1915,6 +2543,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2058,12 +2704,53 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" @@ -2130,6 +2817,19 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.100" @@ -2162,6 +2862,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2234,6 +2944,17 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-registry" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" +dependencies = [ + "windows-link", + "windows-result", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.3.4" @@ -2415,3 +3136,93 @@ checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ "bitflags", ] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index ebacce3b..7c4ae3d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,21 +7,30 @@ edition = "2024" name = "authzd" path = "src/main.rs" +[[bin]] +name = "cli" +path = "src/bin/cli.rs" + [lib] name = "authzd" path = "src/lib.rs" [dependencies] cedar-policy = "4.4.1" +clap = { version = "4.5", features = ["derive", "env"] } envoy-types = "0.6.0" log = "0.4.27" please = { git = "https://github.com/xlgmokha/please.git", version = "0.1.0" } +reqwest = { version = "0.12", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" tokio = { version = "1.0.0", features = ["macros", "rt-multi-thread"] } tonic = "0.13.1" tonic-health = "0.13.1" tonic-reflection = "0.13.1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["json"] } +urlencoding = "2.1" [dev-dependencies] tokio-stream = "0.1" 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, +} + +#[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, +} + +#[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, +} + +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(); + + // 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 = 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, + processed_groups: &'a mut HashSet, +) -> std::pin::Pin>> + '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> { + 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> { + 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, +}; -- 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') 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') 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 51dc342dead1408993c6a2d9d27471d5da7fd9d3 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 13:23:38 -0600 Subject: refactor: remove hard-coded organization --- src/authorization/entities.rs | 48 ++++++++++++++----------------------------- 1 file changed, 15 insertions(+), 33 deletions(-) (limited to 'src') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index 4824fdd8..8ff4e5bd 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -63,31 +63,29 @@ pub struct Group { pub struct EntitiesRepository { pub token: String, pub host: String, - pub project: String, + pub project_url: String, } impl EntitiesRepository { pub fn new(token: String, host: String, project: String) -> EntitiesRepository { EntitiesRepository { token: token, - host: host, - project: project, + host: host.clone(), + project_url: format!( + "{}/api/v4/projects/{}", + host.trim_end_matches('/'), + urlencoding::encode(&project) + ), } } pub async fn all(&self) -> Result, Box> { - let client = reqwest::Client::new(); + let http = reqwest::Client::new(); let mut entities = Vec::new(); - let mut processed_groups = HashSet::new(); + let mut groups = HashSet::new(); - let project_url = format!( - "{}/api/v4/projects/{}", - self.host.trim_end_matches('/'), - urlencoding::encode(&self.project) - ); - - let project: Project = client - .get(&project_url) + let project: Project = http + .get(&self.project_url) .header("PRIVATE-TOKEN", &self.token) .send() .await? @@ -95,17 +93,6 @@ impl EntitiesRepository { .json() .await?; - entities.push(CedarEntity { - uid: CedarUid { - entity_type: "Organization".to_string(), - id: "1".to_string(), - }, - attrs: serde_json::json!({ - "name": "gitlab", - }), - parents: vec![], - }); - entities.push(CedarEntity { uid: CedarUid { entity_type: "Project".to_string(), @@ -132,7 +119,7 @@ impl EntitiesRepository { project.id ); - let members: Vec = client + let members: Vec = http .get(&members_url) .header("PRIVATE-TOKEN", &self.token) .send() @@ -160,12 +147,12 @@ impl EntitiesRepository { if project.namespace.kind == "group" { fetch_group_hierarchy( - &client, + &http, &self.host, &self.token, project.namespace.id, &mut entities, - &mut processed_groups, + &mut groups, ) .await?; } @@ -206,7 +193,6 @@ pub fn fetch_group_hierarchy<'a>( .await?; let parents = if let Some(parent_id) = group.parent_id { - // Recursively fetch parent group fetch_group_hierarchy( client, api_url, @@ -221,11 +207,7 @@ pub fn fetch_group_hierarchy<'a>( id: parent_id.to_string(), }] } else { - // Top-level group belongs to organization - vec![CedarParent { - parent_type: "Organization".to_string(), - id: "1".to_string(), - }] + vec![] }; entities.push(CedarEntity { -- cgit v1.2.3 From afe29973c577b32de20d876d2dd30e3a6adcc1af Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 13:43:20 -0600 Subject: refactor: rename to fetch_hierarchy --- src/authorization/entities.rs | 132 +++++++++++++++++++----------------------- 1 file changed, 59 insertions(+), 73 deletions(-) (limited to 'src') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index 8ff4e5bd..e47cf00f 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -146,83 +146,69 @@ impl EntitiesRepository { } if project.namespace.kind == "group" { - fetch_group_hierarchy( - &http, - &self.host, - &self.token, - project.namespace.id, - &mut entities, - &mut groups, - ) - .await?; + self.fetch_hierarchy(&http, project.namespace.id, &mut entities, &mut 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, - processed_groups: &'a mut HashSet, -) -> std::pin::Pin>> + '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 { - 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 { - 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, - }); + fn fetch_hierarchy<'a>( + &'a self, + client: &'a reqwest::Client, + 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(()); + } - Ok(()) - }) + groups.insert(group_id); + + let group_url = format!( + "{}/api/v4/groups/{}", + self.host.trim_end_matches('/'), + group_id + ); + + let group: Group = client + .get(&group_url) + .header("PRIVATE-TOKEN", &self.token) + .send() + .await? + .error_for_status()? + .json() + .await?; + + let parents = if let Some(parent_id) = group.parent_id { + self.fetch_hierarchy(client, 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(()) + }) + } } -- cgit v1.2.3 From 460a463d15e76fe9e3e7ceac1c2afc6c0069a5d2 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 13:48:51 -0600 Subject: refactor: move gitlab types to a separate module --- src/authorization/entities.rs | 38 +------------------------------------- src/gitlab/group.rs | 10 ++++++++++ src/gitlab/member.rs | 10 ++++++++++ src/gitlab/mod.rs | 9 +++++++++ src/gitlab/namespace.rs | 11 +++++++++++ src/gitlab/project.rs | 11 +++++++++++ src/lib.rs | 1 + 7 files changed, 53 insertions(+), 37 deletions(-) create mode 100644 src/gitlab/group.rs create mode 100644 src/gitlab/member.rs create mode 100644 src/gitlab/mod.rs create mode 100644 src/gitlab/namespace.rs create mode 100644 src/gitlab/project.rs (limited to 'src') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index e47cf00f..b4f5a762 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -1,3 +1,4 @@ +use crate::gitlab::{Group, Member, Project}; use serde::Serialize; use std::collections::HashSet; @@ -23,43 +24,6 @@ pub struct CedarParent { 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, -} - -#[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, -} - pub struct EntitiesRepository { pub token: String, pub host: String, diff --git a/src/gitlab/group.rs b/src/gitlab/group.rs new file mode 100644 index 00000000..6b00e87d --- /dev/null +++ b/src/gitlab/group.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Group { + pub id: u64, + pub name: String, + pub path: String, + pub full_path: String, + pub parent_id: Option, +} diff --git a/src/gitlab/member.rs b/src/gitlab/member.rs new file mode 100644 index 00000000..0b4997e9 --- /dev/null +++ b/src/gitlab/member.rs @@ -0,0 +1,10 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Member { + pub id: u64, + pub username: String, + pub name: String, + pub state: String, + pub access_level: u8, +} diff --git a/src/gitlab/mod.rs b/src/gitlab/mod.rs new file mode 100644 index 00000000..04e85786 --- /dev/null +++ b/src/gitlab/mod.rs @@ -0,0 +1,9 @@ +pub mod group; +pub mod member; +pub mod namespace; +pub mod project; + +pub use group::Group; +pub use member::Member; +pub use namespace::Namespace; +pub use project::Project; diff --git a/src/gitlab/namespace.rs b/src/gitlab/namespace.rs new file mode 100644 index 00000000..d4a1e8f4 --- /dev/null +++ b/src/gitlab/namespace.rs @@ -0,0 +1,11 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Namespace { + pub id: u64, + pub name: String, + pub path: String, + pub kind: String, + pub full_path: String, + pub parent_id: Option, +} diff --git a/src/gitlab/project.rs b/src/gitlab/project.rs new file mode 100644 index 00000000..ba88c2e3 --- /dev/null +++ b/src/gitlab/project.rs @@ -0,0 +1,11 @@ +use serde::Deserialize; + +use super::Namespace; + +#[derive(Debug, Deserialize)] +pub struct Project { + pub id: u64, + pub name: String, + pub path: String, + pub namespace: Namespace, +} diff --git a/src/lib.rs b/src/lib.rs index e70e64f7..3681a859 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod authorization; +pub mod gitlab; pub use authorization::{ Authorizer, CedarAuthorizer, CedarEntity, CheckService, EntitiesRepository, Server, -- cgit v1.2.3 From 155895d188ad6199247f1831b7188018532385d7 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 13:58:58 -0600 Subject: refactor: extract an api type --- src/authorization/entities.rs | 62 ++++++--------------------------- src/gitlab/api.rs | 81 +++++++++++++++++++++++++++++++++++++++++++ src/gitlab/mod.rs | 2 ++ 3 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 src/gitlab/api.rs (limited to 'src') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index b4f5a762..6e7fd568 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -1,4 +1,4 @@ -use crate::gitlab::{Group, Member, Project}; +use crate::gitlab::Api; use serde::Serialize; use std::collections::HashSet; @@ -25,37 +25,23 @@ pub struct CedarParent { } pub struct EntitiesRepository { - pub token: String, - pub host: String, - pub project_url: String, + api: Api, + project: String, } impl EntitiesRepository { pub fn new(token: String, host: String, project: String) -> EntitiesRepository { EntitiesRepository { - token: token, - host: host.clone(), - project_url: format!( - "{}/api/v4/projects/{}", - host.trim_end_matches('/'), - urlencoding::encode(&project) - ), + api: Api::new(token, host), + project, } } pub async fn all(&self) -> Result, Box> { - let http = reqwest::Client::new(); let mut entities = Vec::new(); let mut groups = HashSet::new(); - let project: Project = http - .get(&self.project_url) - .header("PRIVATE-TOKEN", &self.token) - .send() - .await? - .error_for_status()? - .json() - .await?; + let project = self.api.get_project(&self.project).await?; entities.push(CedarEntity { uid: CedarUid { @@ -77,20 +63,7 @@ impl EntitiesRepository { }, }); - let members_url = format!( - "{}/api/v4/projects/{}/members/all", - self.host.trim_end_matches('/'), - project.id - ); - - let members: Vec = http - .get(&members_url) - .header("PRIVATE-TOKEN", &self.token) - .send() - .await? - .error_for_status()? - .json() - .await?; + let members = self.api.get_project_members(project.id).await?; for member in members { if member.state == "active" { @@ -110,7 +83,7 @@ impl EntitiesRepository { } if project.namespace.kind == "group" { - self.fetch_hierarchy(&http, project.namespace.id, &mut entities, &mut groups) + self.fetch_hierarchy(project.namespace.id, &mut entities, &mut groups) .await?; } @@ -119,7 +92,6 @@ impl EntitiesRepository { fn fetch_hierarchy<'a>( &'a self, - client: &'a reqwest::Client, group_id: u64, entities: &'a mut Vec, groups: &'a mut HashSet, @@ -133,24 +105,10 @@ impl EntitiesRepository { groups.insert(group_id); - let group_url = format!( - "{}/api/v4/groups/{}", - self.host.trim_end_matches('/'), - group_id - ); - - let group: Group = client - .get(&group_url) - .header("PRIVATE-TOKEN", &self.token) - .send() - .await? - .error_for_status()? - .json() - .await?; + let group = self.api.get_group(group_id).await?; let parents = if let Some(parent_id) = group.parent_id { - self.fetch_hierarchy(client, parent_id, entities, groups) - .await?; + self.fetch_hierarchy(parent_id, entities, groups).await?; vec![CedarParent { parent_type: "Group".to_string(), id: parent_id.to_string(), diff --git a/src/gitlab/api.rs b/src/gitlab/api.rs new file mode 100644 index 00000000..7f733b4c --- /dev/null +++ b/src/gitlab/api.rs @@ -0,0 +1,81 @@ +use crate::gitlab::{Group, Member, Project}; +use reqwest::Client; + +pub struct Api { + pub token: String, + pub host: String, + client: Client, +} + +impl Api { + pub fn new(token: String, host: String) -> Api { + Api { + token, + host, + client: Client::new(), + } + } + + pub async fn get_project(&self, project: &str) -> Result> { + let url = format!( + "{}/api/v4/projects/{}", + self.host.trim_end_matches('/'), + urlencoding::encode(project) + ); + + let project = self + .client + .get(&url) + .header("PRIVATE-TOKEN", &self.token) + .send() + .await? + .error_for_status()? + .json() + .await?; + + Ok(project) + } + + pub async fn get_project_members( + &self, + project_id: u64, + ) -> Result, Box> { + let url = format!( + "{}/api/v4/projects/{}/members/all", + self.host.trim_end_matches('/'), + project_id + ); + + let members = self + .client + .get(&url) + .header("PRIVATE-TOKEN", &self.token) + .send() + .await? + .error_for_status()? + .json() + .await?; + + Ok(members) + } + + pub async fn get_group(&self, group_id: u64) -> Result> { + let url = format!( + "{}/api/v4/groups/{}", + self.host.trim_end_matches('/'), + group_id + ); + + let group = self + .client + .get(&url) + .header("PRIVATE-TOKEN", &self.token) + .send() + .await? + .error_for_status()? + .json() + .await?; + + Ok(group) + } +} diff --git a/src/gitlab/mod.rs b/src/gitlab/mod.rs index 04e85786..e1993d81 100644 --- a/src/gitlab/mod.rs +++ b/src/gitlab/mod.rs @@ -1,8 +1,10 @@ +pub mod api; pub mod group; pub mod member; pub mod namespace; pub mod project; +pub use api::Api; pub use group::Group; pub use member::Member; pub use namespace::Namespace; -- 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') 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') 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') 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 From b7338b400eea2ce06de362f046da927ed135d048 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 14:27:31 -0600 Subject: refactor: remove unused context value --- src/authorization/cedar_authorizer.rs | 14 -------------- 1 file changed, 14 deletions(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index ceaee51c..0f53dacb 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -113,7 +113,6 @@ impl CedarAuthorizer { ) -> Result> { 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)); @@ -123,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()) } -- cgit v1.2.3 From 501fbdd53312a2a449891386a7982f324ccfe23a Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 14:28:12 -0600 Subject: feat: provide the http method and path as the action and resource --- src/authorization/cedar_authorizer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 0f53dacb..662aafeb 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -89,21 +89,21 @@ impl CedarAuthorizer { 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> { 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> { 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)?, )) } -- cgit v1.2.3 From 5d7f37c6508c7308c17659630cff35f4ead6dae4 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 14:58:52 -0600 Subject: fix: allow authenticated+authorized users to create Sparkles --- etc/authzd/policy1.cedar | 12 ++++++++++++ .../authorization/sparkle/team/entities.json | 8 ++------ src/authorization/entities.rs | 2 -- src/gitlab/member.rs | 2 -- tests/authorization/cedar_authorizer_test.rs | 14 ++++++++++++-- tests/support/factory_bot.rs | 7 +++++-- 6 files changed, 31 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/etc/authzd/policy1.cedar b/etc/authzd/policy1.cedar index 2306aaae..15776ab7 100644 --- a/etc/authzd/policy1.cedar +++ b/etc/authzd/policy1.cedar @@ -16,3 +16,15 @@ when context.path == "/sparkles")) || (context.method == "POST" && (context.path == "/sparkles/restore")))) }; + +permit ( + principal is User, + action == Action::"POST", + resource == Resource::"/sparkles" +) +when +{ + context has host && + context.host == "sparkle.staging.runway.gitlab.net" && + principal has username +}; diff --git a/etc/authzd/staging.gitlab.com/authorization/sparkle/team/entities.json b/etc/authzd/staging.gitlab.com/authorization/sparkle/team/entities.json index ef479736..72d50bce 100644 --- a/etc/authzd/staging.gitlab.com/authorization/sparkle/team/entities.json +++ b/etc/authzd/staging.gitlab.com/authorization/sparkle/team/entities.json @@ -22,9 +22,7 @@ "id": "1675940" }, "attrs": { - "username": "mokhax", - "name": "mo khan", - "access_level": 50 + "username": "mokhax" }, "parents": [] }, @@ -34,9 +32,7 @@ "id": "1676317" }, "attrs": { - "username": "jayswain", - "name": "Jay Swain", - "access_level": 30 + "username": "jayswain" }, "parents": [] }, diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index a26cace2..dd5894f8 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -75,8 +75,6 @@ impl EntitiesRepository { }, attrs: serde_json::json!({ "username": member.username, - "name": member.name, - "access_level": member.access_level, }), parents: vec![], }); diff --git a/src/gitlab/member.rs b/src/gitlab/member.rs index 0b4997e9..7e7f212e 100644 --- a/src/gitlab/member.rs +++ b/src/gitlab/member.rs @@ -4,7 +4,5 @@ use serde::Deserialize; pub struct Member { pub id: u64, pub username: String, - pub name: String, pub state: String, - pub access_level: u8, } diff --git a/tests/authorization/cedar_authorizer_test.rs b/tests/authorization/cedar_authorizer_test.rs index f2dfebd4..f056c8c7 100644 --- a/tests/authorization/cedar_authorizer_test.rs +++ b/tests/authorization/cedar_authorizer_test.rs @@ -91,12 +91,22 @@ mod tests { ]); }); - let user = build_user("1675940"); + let mut attrs = std::collections::HashMap::new(); + attrs.insert( + "username".to_string(), + cedar_policy::RestrictedExpression::new_string("tanuki".to_string()), + ); + let user = build_user("1675940", attrs); let entities = cedar_policy::Entities::from_entities([user], None).unwrap(); let authorizer = subject_with(entities); assert!(authorizer.authorize(request.clone())); - let user = build_user("1"); + let mut attrs = std::collections::HashMap::new(); + attrs.insert( + "username".to_string(), + cedar_policy::RestrictedExpression::new_string("root".to_string()), + ); + let user = build_user("1", attrs); let entities = cedar_policy::Entities::from_entities([user], None).unwrap(); let authorizer = subject_with(entities); assert!(!authorizer.authorize(request.clone())); diff --git a/tests/support/factory_bot.rs b/tests/support/factory_bot.rs index 969080a3..ba0d9c38 100644 --- a/tests/support/factory_bot.rs +++ b/tests/support/factory_bot.rs @@ -58,13 +58,16 @@ where f(build_channel(addr).await) } -pub fn build_user(id: &str) -> cedar_policy::Entity { +pub fn build_user( + id: &str, + attrs: std::collections::HashMap, +) -> cedar_policy::Entity { cedar_policy::Entity::new( cedar_policy::EntityUid::from_type_name_and_id( cedar_policy::EntityTypeName::from_str("User").unwrap(), cedar_policy::EntityId::from_str(id).unwrap(), ), - std::collections::HashMap::new(), + attrs, std::collections::HashSet::new(), ) .unwrap() -- cgit v1.2.3 From fc1889b176bf6472c5d762fb2861b79961b40cc8 Mon Sep 17 00:00:00 2001 From: mo khan Date: Thu, 10 Jul 2025 15:59:50 -0600 Subject: feat: add access_level to entities.json --- src/authorization/entities.rs | 1 + src/gitlab/member.rs | 1 + 2 files changed, 2 insertions(+) (limited to 'src') diff --git a/src/authorization/entities.rs b/src/authorization/entities.rs index dd5894f8..fc1246d7 100644 --- a/src/authorization/entities.rs +++ b/src/authorization/entities.rs @@ -75,6 +75,7 @@ impl EntitiesRepository { }, attrs: serde_json::json!({ "username": member.username, + "access_level": member.access_level, }), parents: vec![], }); diff --git a/src/gitlab/member.rs b/src/gitlab/member.rs index 7e7f212e..b44b88f2 100644 --- a/src/gitlab/member.rs +++ b/src/gitlab/member.rs @@ -5,4 +5,5 @@ pub struct Member { pub id: u64, pub username: String, pub state: String, + pub access_level: u64, } -- cgit v1.2.3