diff options
| -rw-r--r-- | .gitlab-ci.yml | 1 | ||||
| -rw-r--r-- | Makefile | 4 | ||||
| -rw-r--r-- | etc/authzd/policy0.cedar | 56 | ||||
| -rw-r--r-- | src/authorization/authorizer.rs | 2 | ||||
| -rw-r--r-- | src/authorization/cedar_authorizer.rs | 207 | ||||
| -rw-r--r-- | src/authorization/check_service.rs | 2 | ||||
| -rw-r--r-- | src/authorization/server.rs | 12 | ||||
| -rw-r--r-- | src/main.rs | 3 | ||||
| -rw-r--r-- | tests/authorization/cedar_authorizer_test.rs | 60 | ||||
| -rw-r--r-- | tests/authorization/check_service_test.rs | 127 | ||||
| -rw-r--r-- | tests/support/factory_bot.rs | 10 |
11 files changed, 267 insertions, 217 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 6ae0a526..4968aa7b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -44,5 +44,6 @@ lint: needs: [] before_script: - rustup component add clippy rustfmt + - cargo install --locked cedar-policy-cli script: - make lint @@ -9,6 +9,7 @@ setup: mise install mise exec go -- go install github.com/xlgmokha/minit@latest mise exec rust -- rustup component add clippy rustfmt + mise exec rust -- cargo install --locked cedar-policy-cli # Cargo targets build: @@ -29,9 +30,12 @@ clean: fmt: @cargo fmt + @cedar format --policies etc/authzd/policy0.cedar --write lint: @cargo clippy + @cedar check-parse --policies etc/authzd/policy0.cedar + @cedar format --policies etc/authzd/policy0.cedar --check doc: @cargo doc --open diff --git a/etc/authzd/policy0.cedar b/etc/authzd/policy0.cedar index 034e81b5..9410eced 100644 --- a/etc/authzd/policy0.cedar +++ b/etc/authzd/policy0.cedar @@ -1,20 +1,42 @@ -permit(principal, action == Action::"check", resource) -when { - context has bearer_token && - context.bearer_token == "valid-token" +permit ( + principal, + action == Action::"check", + resource +) +when { context has bearer_token && context.bearer_token == "valid-token" }; + +permit (principal, action, resource) +when +{ + context has path && + context has method && + context.method == "GET" && + (context.path like "*.css" || + context.path like "*.js" || + context.path like "*.ico" || + context.path like "*.png" || + context.path like "*.jpg" || + context.path like "*.jpeg" || + context.path like "*.gif" || + context.path like "*.bmp" || + context.path like "*.html") }; -permit(principal, action == Action::"check", resource) -when { - context has path && ( - context.path like "*.css" || - context.path like "*.js" || - context.path like "*.ico" || - context.path like "*.png" || - context.path like "*.jpg" || - context.path like "*.jpeg" || - context.path like "*.gif" || - context.path like "*.bmp" || - context.path like "*.html" - ) +permit (principal, action, resource) +when +{ + context has host && + context has method && + context has path && + ((context.host == "sparkle.runway.gitlab.net" || + context.host == "sparkle.staging.runway.gitlab.net" || + context.host like "localhost:*") && + ((context.method == "GET" && + (context.path == "/" || + context.path == "/callback" || + context.path == "/dashboard/nav" || + context.path == "/health" || + context.path == "/signout" || + context.path == "/sparkles")) || + (context.method == "POST" && (context.path == "/sparkles/restore")))) }; diff --git a/src/authorization/authorizer.rs b/src/authorization/authorizer.rs index 14a7df27..62733585 100644 --- a/src/authorization/authorizer.rs +++ b/src/authorization/authorizer.rs @@ -1,5 +1,5 @@ use envoy_types::ext_authz::v3::pb::CheckRequest; -pub trait Authorizer: std::fmt::Debug { +pub trait Authorizer: std::fmt::Debug + std::marker::Sync + std::marker::Send + 'static { fn authorize(&self, request: CheckRequest) -> bool; } diff --git a/src/authorization/cedar_authorizer.rs b/src/authorization/cedar_authorizer.rs index 432102ef..64287414 100644 --- a/src/authorization/cedar_authorizer.rs +++ b/src/authorization/cedar_authorizer.rs @@ -1,45 +1,48 @@ use super::authorizer::Authorizer; -use cedar_policy::{ - Authorizer as CedarAuth, Context, Entities, EntityId, EntityTypeName, EntityUid, PolicySet, - Request as CedarRequest, RestrictedExpression, -}; -use envoy_types::ext_authz::v3::pb::CheckRequest; -use std::collections::HashMap; use std::fs; use std::str::FromStr; #[derive(Debug)] pub struct CedarAuthorizer { - policies: PolicySet, - authorizer: CedarAuth, + authorizer: cedar_policy::Authorizer, + entities: cedar_policy::Entities, + policies: cedar_policy::PolicySet, } impl CedarAuthorizer { - pub fn new(policies: cedar_policy::PolicySet) -> CedarAuthorizer { + pub fn new( + policies: cedar_policy::PolicySet, + entities: cedar_policy::Entities, + ) -> CedarAuthorizer { CedarAuthorizer { policies, - authorizer: CedarAuth::new(), + entities, + authorizer: cedar_policy::Authorizer::new(), } } - pub fn new_from(path: &std::path::Path) -> CedarAuthorizer { - Self::new(Self::load_from(path).unwrap_or_else(|_| PolicySet::default())) + 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()), + entities, + ) } - fn load_from(path: &std::path::Path) -> Result<PolicySet, Box<dyn std::error::Error>> { + fn load_from( + path: &std::path::Path, + ) -> Result<cedar_policy::PolicySet, Box<dyn std::error::Error>> { if !path.exists() || !path.is_dir() { - return Ok(PolicySet::default()); + return Ok(cedar_policy::PolicySet::default()); } - let mut policies = PolicySet::new(); - + 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 extension == "cedar" { let content = fs::read_to_string(&file_path)?; - let file_policies = PolicySet::from_str(&content)?; + let file_policies = cedar_policy::PolicySet::from_str(&content)?; for policy in file_policies.policies() { policies.add(policy.clone())?; @@ -50,16 +53,96 @@ impl CedarAuthorizer { Ok(policies) } + + fn map_from( + &self, + http_request: envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, + ) -> Result<cedar_policy::Request, Box<dyn std::error::Error>> { + let principal = self.principal_from(&http_request)?; + let permission = self.permission_from(&http_request)?; + let resource = self.resource_from(&http_request)?; + let context = self.context_from(http_request)?; + + Ok(cedar_policy::Request::new( + principal, permission, resource, context, None, + )?) + } + + fn principal_from( + &self, + _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, + ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> { + Ok(cedar_policy::EntityUid::from_type_name_and_id( + cedar_policy::EntityTypeName::from_str("User")?, + cedar_policy::EntityId::from_str("client")?, + )) + } + + fn permission_from( + &self, + _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, + ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> { + Ok(cedar_policy::EntityUid::from_type_name_and_id( + cedar_policy::EntityTypeName::from_str("Action")?, + cedar_policy::EntityId::from_str("check")?, + )) + } + + fn resource_from( + &self, + _http_request: &envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, + ) -> Result<cedar_policy::EntityUid, Box<dyn std::error::Error>> { + Ok(cedar_policy::EntityUid::from_type_name_and_id( + cedar_policy::EntityTypeName::from_str("Resource")?, + cedar_policy::EntityId::from_str("resource")?, + )) + } + + fn context_from( + &self, + http_request: envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest, + ) -> Result<cedar_policy::Context, Box<dyn std::error::Error>> { + 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)); + + Ok(cedar_policy::Context::from_pairs( + items.into_iter().collect::<Vec<_>>(), + )?) + } + + 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()) + } } impl Default for CedarAuthorizer { fn default() -> Self { - Self::new_from(std::path::Path::new("/etc/authzd")) + Self::new_from( + std::path::Path::new("/etc/authzd"), + cedar_policy::Entities::empty(), + ) } } impl Authorizer for CedarAuthorizer { - fn authorize(&self, request: CheckRequest) -> bool { + fn authorize(&self, request: envoy_types::ext_authz::v3::pb::CheckRequest) -> bool { let http_request = match request .attributes .as_ref() @@ -70,37 +153,26 @@ impl Authorizer for CedarAuthorizer { None => return false, }; - tracing::info!( - method = %http_request.method, - host = %http_request.host, - path = %http_request.path, - scheme = %http_request.scheme, - protocol = %http_request.protocol, - "Processing HTTP request" - ); - - if http_request.host == "sparkle.staging.runway.gitlab.net" - && http_request.method == "GET" - && http_request.path == "/" - { - return true; - } - - let headers = &http_request.headers; - - let bearer_token = headers - .get("authorization") - .and_then(|auth| auth.strip_prefix("Bearer ")) - .unwrap_or(""); - - match self.create_cedar_request(bearer_token, &http_request.path.to_string()) { + match self.map_from(http_request.clone()) { Ok(cedar_request) => { - let entities = Entities::empty(); let response = self.authorizer - .is_authorized(&cedar_request, &self.policies, &entities); + .is_authorized(&cedar_request, &self.policies, &self.entities); + + let decision = response.decision(); - matches!(response.decision(), cedar_policy::Decision::Allow) + tracing::info!( + method = %http_request.method, + host = %http_request.host, + path = %http_request.path, + scheme = %http_request.scheme, + protocol = %http_request.protocol, + decision = ?decision, + diagnostics = ?response.diagnostics(), + "Processing HTTP request" + ); + + matches!(decision, cedar_policy::Decision::Allow) } Err(e) => { tracing::error!( @@ -113,46 +185,3 @@ impl Authorizer for CedarAuthorizer { } } } - -impl CedarAuthorizer { - fn create_cedar_request( - &self, - bearer_token: &str, - path: &str, - ) -> Result<CedarRequest, Box<dyn std::error::Error>> { - // Create principal entity - let principal_id = EntityId::from_str("client")?; - let principal_type = EntityTypeName::from_str("User")?; - let principal = EntityUid::from_type_name_and_id(principal_type, principal_id); - - // Create action entity - let action_id = EntityId::from_str("check")?; - let action_type = EntityTypeName::from_str("Action")?; - let action = EntityUid::from_type_name_and_id(action_type, action_id); - - // Create resource entity - let resource_id = EntityId::from_str("resource")?; - let resource_type = EntityTypeName::from_str("Resource")?; - let resource = EntityUid::from_type_name_and_id(resource_type, resource_id); - - // Create context with bearer token and path - let mut context_map = HashMap::new(); - if !bearer_token.is_empty() { - context_map.insert( - "bearer_token".to_string(), - RestrictedExpression::from_str(&format!("\"{bearer_token}\""))?, - ); - } - if !path.is_empty() { - context_map.insert( - "path".to_string(), - RestrictedExpression::from_str(&format!("\"{path}\""))?, - ); - } - - let context = Context::from_pairs(context_map.into_iter().collect::<Vec<_>>())?; - - CedarRequest::new(principal, action, resource, context, None) - .map_err(|e| Box::new(e) as Box<dyn std::error::Error>) - } -} diff --git a/src/authorization/check_service.rs b/src/authorization/check_service.rs index 57f7b5d5..f8c7577f 100644 --- a/src/authorization/check_service.rs +++ b/src/authorization/check_service.rs @@ -23,10 +23,8 @@ impl envoy_types::ext_authz::v3::pb::Authorization for CheckService { request: Request<CheckRequest>, ) -> Result<Response<CheckResponse>, Status> { if self.authorizer.authorize(request.into_inner()) { - log::info!("OK"); Ok(Response::new(CheckResponse::with_status(Status::ok("OK")))) } else { - log::info!("Unauthorized"); Ok(Response::new(CheckResponse::with_status( Status::unauthenticated("Unauthorized"), ))) diff --git a/src/authorization/server.rs b/src/authorization/server.rs index 23b7720e..759a550d 100644 --- a/src/authorization/server.rs +++ b/src/authorization/server.rs @@ -8,13 +8,13 @@ pub struct Server { } impl Server { - pub fn new() -> Result<Server, Box<dyn std::error::Error>> { + pub fn new<T: super::Authorizer>(authorizer: T) -> Result<Server, Box<dyn std::error::Error>> { let (health_reporter, health_service) = tonic_health::server::health_reporter(); std::mem::drop( health_reporter.set_service_status("", tonic_health::ServingStatus::Serving), ); let authorization_service = - AuthorizationServer::new(CheckService::new(Arc::new(CedarAuthorizer::default()))); + AuthorizationServer::new(CheckService::new(Arc::new(authorizer))); let reflection_service = tonic_reflection::server::Builder::configure() .register_encoded_file_descriptor_set(tonic_health::pb::FILE_DESCRIPTOR_SET) @@ -40,9 +40,9 @@ impl Server { tracing::info!( method = %req.method(), path = %req.uri().path(), - content_type = ?req.headers().get("content-type").and_then(|v| v.to_str().ok()), - user_agent = ?req.headers().get("user-agent").and_then(|v| v.to_str().ok()), - x_request_id = ?req.headers().get("x-request-id").and_then(|v| v.to_str().ok()), + content_type = req.headers().get("content-type").map_or("unknown", |v| v.to_str().unwrap_or("unknown")), + user_agent = req.headers().get("user-agent").map_or("unknown", |v| v.to_str().unwrap_or("unknown")), + x_request_id = req.headers().get("x-request-id").map_or("none", |v| v.to_str().unwrap_or("none")), "gRPC request" ); @@ -64,6 +64,6 @@ impl Server { impl Default for Server { fn default() -> Self { - Self::new().unwrap() + Self::new(CedarAuthorizer::default()).unwrap() } } diff --git a/src/main.rs b/src/main.rs index 511d3d04..add0d88d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,7 +17,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> { .parse()?; tracing::info!(address = %addr, "Starting authorization server"); - let server = authzd::authorization::Server::new()?; + let cedar = authzd::authorization::CedarAuthorizer::default(); + let server = authzd::authorization::Server::new(cedar)?; server.serve(addr).await?; Ok(()) diff --git a/tests/authorization/cedar_authorizer_test.rs b/tests/authorization/cedar_authorizer_test.rs index 79f83c00..490a0107 100644 --- a/tests/authorization/cedar_authorizer_test.rs +++ b/tests/authorization/cedar_authorizer_test.rs @@ -5,46 +5,44 @@ mod tests { use envoy_types::pb::envoy::service::auth::v3::attribute_context::HttpRequest; use std::collections::HashMap; + fn subject() -> authzd::CedarAuthorizer { + build_cedar_authorizer() + } + #[test] fn test_cedar_authorizer_allows_valid_token() { - let request = build_request(|item: &mut HttpRequest| { - item.headers = build_with(|item: &mut HashMap<String, String>| { - item.insert( - String::from("authorization"), - String::from("Bearer valid-token"), - ); - }); - }); - - assert!(build_cedar_authorizer().authorize(request)); + assert!(subject().authorize(build_request(|item: &mut HttpRequest| { + item.headers = build_headers(vec![( + "authorization".to_string(), + "Bearer valid-token".to_string(), + )]); + }))); } #[test] fn test_cedar_authorizer_denies_invalid_token() { - let request = build_request(|item: &mut HttpRequest| { - item.headers = build_with(|item: &mut HashMap<String, String>| { - item.insert( - String::from("authorization"), - String::from("Bearer invalid-token"), - ); - }); - }); - - assert!(!build_cedar_authorizer().authorize(request)); + assert!( + !subject().authorize(build_request(|item: &mut HttpRequest| { + item.headers = build_headers(vec![( + "authorization".to_string(), + "Bearer invalid-token".to_string(), + )]); + })) + ); } #[test] fn test_cedar_authorizer_denies_missing_header() { - let request = build_request(|item: &mut HttpRequest| { - item.headers = HashMap::new(); - }); - - assert!(!build_cedar_authorizer().authorize(request)); + assert!( + !subject().authorize(build_request(|item: &mut HttpRequest| { + item.headers = HashMap::new(); + })) + ); } #[test] fn test_cedar_authorizer_allows_static_assets() { - let request = build_request(|item: &mut HttpRequest| { + assert!(subject().authorize(build_request(|item: &mut HttpRequest| { let method = String::from("GET"); let host = String::from("sparkle.staging.runway.gitlab.net"); let path = "/public/style.css"; @@ -57,14 +55,12 @@ mod tests { (String::from(":method"), method), (String::from(":authority"), host), ]); - }); - - assert!(build_cedar_authorizer().authorize(request)); + }))); } #[test] fn test_cedar_authorizer_allows_js_assets() { - let request = build_request(|item: &mut HttpRequest| { + assert!(subject().authorize(build_request(|item: &mut HttpRequest| { let method = String::from("GET"); let host = String::from("sparkle.staging.runway.gitlab.net"); let path = "/app.js"; @@ -77,8 +73,6 @@ mod tests { (String::from(":method"), method), (String::from(":authority"), host), ]); - }); - - assert!(build_cedar_authorizer().authorize(request)); + }))); } } diff --git a/tests/authorization/check_service_test.rs b/tests/authorization/check_service_test.rs index a4b8f2ee..c5c824fc 100644 --- a/tests/authorization/check_service_test.rs +++ b/tests/authorization/check_service_test.rs @@ -14,6 +14,7 @@ mod tests { #[tokio::test] async fn test_check_allows_valid_bearer_token() { let request = tonic::Request::new(build_request(|item: &mut HttpRequest| { + item.path = String::from("/"); item.headers = build_headers(vec![( "authorization".to_string(), format!("Bearer {}", String::from("valid-token")), @@ -27,7 +28,7 @@ mod tests { assert!(check_response.status.is_some()); let status = check_response.status.unwrap(); - assert_eq!(status.code, tonic::Code::Ok as i32); + assert_eq!(tonic::Code::from_i32(status.code), tonic::Code::Ok); } #[tokio::test] @@ -43,7 +44,10 @@ mod tests { assert!(check_response.status.is_some()); let status = check_response.status.unwrap(); - assert_eq!(status.code, tonic::Code::Unauthenticated as i32); + assert_eq!( + tonic::Code::from_i32(status.code), + tonic::Code::Unauthenticated + ); } #[tokio::test] @@ -79,7 +83,7 @@ mod tests { assert!(check_response.status.is_some()); let status = check_response.status.unwrap(); - assert_eq!(status.code, tonic::Code::Ok as i32); + assert_eq!(tonic::Code::from_i32(status.code), tonic::Code::Ok); } } @@ -94,19 +98,22 @@ mod tests { assert!(check_response.status.is_some()); let status = check_response.status.unwrap(); - assert_eq!(status.code, tonic::Code::Unauthenticated as i32); + assert_eq!( + tonic::Code::from_i32(status.code), + tonic::Code::Unauthenticated + ); } #[tokio::test] async fn test_table() { let test_cases = vec![ - ("Bearer valid-token", true), - ("Bearer invalid-token", false), - ("Basic valid-token", false), - ("", false), + ("Bearer valid-token", tonic::Code::Ok), + ("Bearer invalid-token", tonic::Code::Unauthenticated), + ("Basic valid-token", tonic::Code::Unauthenticated), + ("", tonic::Code::Unauthenticated), ]; - for (auth_value, should_succeed) in test_cases { + for (auth_value, expected_status_code) in test_cases { let request = tonic::Request::new(build_request(|item: &mut HttpRequest| { item.headers = build_headers(vec![("authorization".to_string(), auth_value.to_string())]); @@ -118,70 +125,64 @@ mod tests { let check_response = response.unwrap().into_inner(); let status = check_response.status.unwrap(); - if should_succeed { - assert_eq!(status.code, tonic::Code::Ok as i32); - } else { - assert_eq!(status.code, tonic::Code::Unauthenticated as i32); - } + assert_eq!(tonic::Code::from_i32(status.code), expected_status_code); } } #[tokio::test] async fn test_public_sparkle_endpoints() { - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/application.js"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/callback"}}, // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/dashboard", Headers: loggedInHeaders}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/dashboard/nav"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/favicon.ico"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/favicon.png"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/favicon.png"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/health"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/htmx.js"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/index.html"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/logo.png"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/pico.min.css"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/signout"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/sparkles"}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "GET", Path: "/vue.global.js"}}, // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "POST", Path: "/sparkles", Headers: loggedInHeaders}}, - // {status: tonic::Code::Ok, http: &HTTPRequest{Method: "POST", Path: "/sparkles/restore"}}, - // {status: tonic::Code::PermissionDenied, http: &HTTPRequest{Method: "GET", Path: "/dashboard"}}, // {status: tonic::Code::PermissionDenied, http: &HTTPRequest{Method: "GET", Path: "/dashboard", Headers: invalidHeaders}}, - // {status: tonic::Code::PermissionDenied, http: &HTTPRequest{Method: "POST", Path: "/sparkles"}}, - // - // http: - // method: \"GET\", - // headers: { - // \":method\": \"GET\", - // \":authority\": \"localhost:10000\", - // \":path\": \"/sparkles\", - // }, - // path: \"/sparkles\", - // host: \"localhost:10000\", - - let request = tonic::Request::new(build_request(|item: &mut HttpRequest| { - let path = String::from("/"); - let method = String::from("GET"); - let host = String::from("sparkle.staging.runway.gitlab.net"); - - item.method = method.clone(); - item.path = path.clone(); - item.host = host.clone(); - item.headers = build_headers(vec![ - (String::from(":path"), path), - (String::from(":method"), method), - (String::from(":authority"), host), - ]); - })); - let response = subject().check(request).await; - assert!(response.is_ok()); + let hosts = vec![ + "localhost:10000", + "sparkle.runway.gitlab.net", + "sparkle.staging.runway.gitlab.net", + ]; - let check_response = response.unwrap().into_inner(); - assert!(check_response.status.is_some()); + let routes = vec![ + ("GET", "/", tonic::Code::Ok), + ("GET", "/application.js", tonic::Code::Ok), + ("GET", "/callback", tonic::Code::Ok), + ("GET", "/dashboard/nav", tonic::Code::Ok), + ("GET", "/favicon.ico", tonic::Code::Ok), + ("GET", "/favicon.png", tonic::Code::Ok), + ("GET", "/health", tonic::Code::Ok), + ("GET", "/htmx.js", tonic::Code::Ok), + ("GET", "/index.html", tonic::Code::Ok), + ("GET", "/logo.png", tonic::Code::Ok), + ("GET", "/pico.min.css", tonic::Code::Ok), + ("GET", "/signout", tonic::Code::Ok), + ("GET", "/sparkles", tonic::Code::Ok), + ("GET", "/vue.global.js", tonic::Code::Ok), + ("POST", "/sparkles/restore", tonic::Code::Ok), + ("GET", "/dashboard", tonic::Code::Unauthenticated), + ("POST", "/sparkles", tonic::Code::Unauthenticated), + ]; - let status = check_response.status.unwrap(); - assert_eq!(status.code, tonic::Code::Ok as i32); + for host in hosts { + for (method, path, expected_status_code) in &routes { + let request = tonic::Request::new(build_request(|item: &mut HttpRequest| { + item.method = method.to_string(); + item.path = path.to_string(); + item.host = host.to_string(); + item.headers = build_headers(vec![ + (String::from(":path"), path.to_string()), + (String::from(":method"), method.to_string()), + (String::from(":authority"), host.to_string()), + ]); + })); + + let response = subject().check(request).await; + assert!(response.is_ok()); + + let check_response = response.unwrap().into_inner(); + assert!(check_response.status.is_some()); + + let status = check_response.status.unwrap(); + assert_eq!(tonic::Code::from_i32(status.code), *expected_status_code); + } + } } } diff --git a/tests/support/factory_bot.rs b/tests/support/factory_bot.rs index 15c6f1f3..007f0cb7 100644 --- a/tests/support/factory_bot.rs +++ b/tests/support/factory_bot.rs @@ -7,7 +7,7 @@ use tonic::transport::Channel; #[allow(dead_code)] pub fn build<T: Default>() -> T { - return please::build(); + please::build() } pub fn build_with<T, F>(initializer: F) -> T @@ -15,7 +15,7 @@ where T: Default, F: std::ops::FnOnce(&mut T), { - return please::build_with(initializer); + please::build_with(initializer) } pub fn build_request(f: impl std::ops::FnOnce(&mut HttpRequest)) -> CheckRequest { @@ -29,17 +29,17 @@ pub fn build_request(f: impl std::ops::FnOnce(&mut HttpRequest)) -> CheckRequest } pub fn build_headers(headers: Vec<(String, String)>) -> HashMap<String, String> { - return build_with(|item: &mut HashMap<String, String>| { + build_with(|item: &mut HashMap<String, String>| { for (key, value) in headers { item.insert(key, value); } - }); + }) } pub fn build_cedar_authorizer() -> authzd::CedarAuthorizer { let realpath = std::fs::canonicalize("./etc/authzd").unwrap(); let path = realpath.as_path(); - authzd::CedarAuthorizer::new_from(path) + authzd::CedarAuthorizer::new_from(path, cedar_policy::Entities::empty()) } pub async fn build_channel(addr: SocketAddr) -> Channel { |
