From c66d2f6d47823a3b38b6b983a0380866e97ef362 Mon Sep 17 00:00:00 2001 From: David Pacheco Date: Fri, 18 Mar 2022 09:19:08 -0700 Subject: [PATCH] authz: add test to prevent new endpoints not covered by authz (#785) --- Cargo.lock | 1 + nexus/Cargo.toml | 1 + nexus/tests/integration_tests/endpoints.rs | 594 ++++++++++++++++++ nexus/tests/integration_tests/mod.rs | 4 + nexus/tests/integration_tests/unauthorized.rs | 581 +---------------- .../unauthorized_coverage.rs | 143 +++++ .../output/uncovered-authz-endpoints.txt | 22 + .../output/unexpected-authz-endpoints.txt | 1 + 8 files changed, 768 insertions(+), 579 deletions(-) create mode 100644 nexus/tests/integration_tests/endpoints.rs create mode 100644 nexus/tests/integration_tests/unauthorized_coverage.rs create mode 100644 nexus/tests/output/uncovered-authz-endpoints.txt create mode 100644 nexus/tests/output/unexpected-authz-endpoints.txt diff --git a/Cargo.lock b/Cargo.lock index 30ac3a3eb9..b3e92df11d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2012,6 +2012,7 @@ dependencies = [ "pq-sys", "rand", "ref-cast", + "regex", "reqwest", "ring", "schemars", diff --git a/nexus/Cargo.toml b/nexus/Cargo.toml index f68598337a..87ad139102 100644 --- a/nexus/Cargo.toml +++ b/nexus/Cargo.toml @@ -120,6 +120,7 @@ nexus-test-utils-macros = { path = "test-utils-macros" } nexus-test-utils = { path = "test-utils" } omicron-test-utils = { path = "../test-utils" } openapiv3 = "1.0" +regex = "1.5.5" subprocess = "0.2.8" [dev-dependencies.openapi-lint] diff --git a/nexus/tests/integration_tests/endpoints.rs b/nexus/tests/integration_tests/endpoints.rs new file mode 100644 index 0000000000..6af67d4f04 --- /dev/null +++ b/nexus/tests/integration_tests/endpoints.rs @@ -0,0 +1,594 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +//! Authz-related configuration for API endpoints +//! +//! This is used for various authz-related tests. +//! THERE ARE NO TESTS IN THIS FILE. + +use http::method::Method; +use lazy_static::lazy_static; +use omicron_common::api::external::ByteCount; +use omicron_common::api::external::IdentityMetadataCreateParams; +use omicron_common::api::external::IdentityMetadataUpdateParams; +use omicron_common::api::external::InstanceCpuCount; +use omicron_common::api::external::Ipv4Net; +use omicron_common::api::external::Name; +use omicron_common::api::external::RouteDestination; +use omicron_common::api::external::RouteTarget; +use omicron_common::api::external::RouterRouteCreateParams; +use omicron_common::api::external::RouterRouteUpdateParams; +use omicron_common::api::external::VpcFirewallRuleUpdateParams; +use omicron_nexus::authn; +use omicron_nexus::external_api::params; +use std::net::IpAddr; +use std::net::Ipv4Addr; + +lazy_static! { + // Organization used for testing + pub static ref DEMO_ORG_NAME: Name = "demo-org".parse().unwrap(); + pub static ref DEMO_ORG_URL: String = + format!("/organizations/{}", *DEMO_ORG_NAME); + pub static ref DEMO_ORG_PROJECTS_URL: String = + format!("{}/projects", *DEMO_ORG_URL); + pub static ref DEMO_ORG_CREATE: params::OrganizationCreate = + params::OrganizationCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_ORG_NAME.clone(), + description: String::from(""), + }, + }; + + // Project used for testing + pub static ref DEMO_PROJECT_NAME: Name = "demo-project".parse().unwrap(); + pub static ref DEMO_PROJECT_URL: String = + format!("{}/{}", *DEMO_ORG_PROJECTS_URL, *DEMO_PROJECT_NAME); + pub static ref DEMO_PROJECT_URL_DISKS: String = + format!("{}/disks", *DEMO_PROJECT_URL); + pub static ref DEMO_PROJECT_URL_INSTANCES: String = + format!("{}/instances", *DEMO_PROJECT_URL); + pub static ref DEMO_PROJECT_URL_VPCS: String = + format!("{}/vpcs", *DEMO_PROJECT_URL); + pub static ref DEMO_PROJECT_CREATE: params::ProjectCreate = + params::ProjectCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_PROJECT_NAME.clone(), + description: String::from(""), + }, + }; + + // VPC used for testing + pub static ref DEMO_VPC_NAME: Name = "demo-vpc".parse().unwrap(); + pub static ref DEMO_VPC_URL: String = + format!("{}/{}", *DEMO_PROJECT_URL_VPCS, *DEMO_VPC_NAME); + pub static ref DEMO_VPC_URL_FIREWALL_RULES: String = + format!("{}/firewall/rules", *DEMO_VPC_URL); + pub static ref DEMO_VPC_URL_ROUTERS: String = + format!("{}/routers", *DEMO_VPC_URL); + pub static ref DEMO_VPC_URL_SUBNETS: String = + format!("{}/subnets", *DEMO_VPC_URL); + pub static ref DEMO_VPC_CREATE: params::VpcCreate = + params::VpcCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_NAME.clone(), + description: String::from(""), + }, + ipv6_prefix: None, + dns_name: DEMO_VPC_NAME.clone(), + }; + + // VPC Subnet used for testing + pub static ref DEMO_VPC_SUBNET_NAME: Name = + "demo-vpc-subnet".parse().unwrap(); + pub static ref DEMO_VPC_SUBNET_URL: String = + format!("{}/{}", *DEMO_VPC_URL_SUBNETS, *DEMO_VPC_SUBNET_NAME); + pub static ref DEMO_VPC_SUBNET_CREATE: params::VpcSubnetCreate = + params::VpcSubnetCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_SUBNET_NAME.clone(), + description: String::from(""), + }, + ipv4_block: Ipv4Net("10.1.2.3/8".parse().unwrap()), + ipv6_block: None, + }; + + // VPC Router used for testing + pub static ref DEMO_VPC_ROUTER_NAME: Name = + "demo-vpc-router".parse().unwrap(); + pub static ref DEMO_VPC_ROUTER_URL: String = + format!("{}/{}", *DEMO_VPC_URL_ROUTERS, *DEMO_VPC_ROUTER_NAME); + pub static ref DEMO_VPC_ROUTER_URL_ROUTES: String = + format!("{}/routes", *DEMO_VPC_ROUTER_URL); + pub static ref DEMO_VPC_ROUTER_CREATE: params::VpcRouterCreate = + params::VpcRouterCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_VPC_ROUTER_NAME.clone(), + description: String::from(""), + }, + }; + + // Router Route used for testing + pub static ref DEMO_ROUTER_ROUTE_NAME: Name = + "demo-router-route".parse().unwrap(); + pub static ref DEMO_ROUTER_ROUTE_URL: String = + format!("{}/{}", *DEMO_VPC_ROUTER_URL_ROUTES, *DEMO_ROUTER_ROUTE_NAME); + pub static ref DEMO_ROUTER_ROUTE_CREATE: RouterRouteCreateParams = + RouterRouteCreateParams { + identity: IdentityMetadataCreateParams { + name: DEMO_ROUTER_ROUTE_NAME.clone(), + description: String::from(""), + }, + target: RouteTarget::Ip(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))), + destination: RouteDestination::Subnet("loopback".parse().unwrap()), + }; + + // Disk used for testing + pub static ref DEMO_DISK_NAME: Name = "demo-disk".parse().unwrap(); + pub static ref DEMO_DISK_URL: String = + format!("{}/{}", *DEMO_PROJECT_URL_DISKS, *DEMO_DISK_NAME); + pub static ref DEMO_DISK_CREATE: params::DiskCreate = + params::DiskCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_DISK_NAME.clone(), + description: "".parse().unwrap(), + }, + snapshot_id: None, + size: ByteCount::from_gibibytes_u32(16), + }; + + // Instance used for testing + pub static ref DEMO_INSTANCE_NAME: Name = "demo-instance".parse().unwrap(); + pub static ref DEMO_INSTANCE_URL: String = + format!("{}/{}", *DEMO_PROJECT_URL_INSTANCES, *DEMO_INSTANCE_NAME); + pub static ref DEMO_INSTANCE_START_URL: String = + format!("{}/start", *DEMO_INSTANCE_URL); + pub static ref DEMO_INSTANCE_STOP_URL: String = + format!("{}/stop", *DEMO_INSTANCE_URL); + pub static ref DEMO_INSTANCE_REBOOT_URL: String = + format!("{}/reboot", *DEMO_INSTANCE_URL); + pub static ref DEMO_INSTANCE_MIGRATE_URL: String = + format!("{}/migrate", *DEMO_INSTANCE_URL); + pub static ref DEMO_INSTANCE_DISKS_URL: String = + format!("{}/disks", *DEMO_INSTANCE_URL); + pub static ref DEMO_INSTANCE_DISKS_ATTACH_URL: String = + format!("{}/attach", *DEMO_INSTANCE_DISKS_URL); + pub static ref DEMO_INSTANCE_DISKS_DETACH_URL: String = + format!("{}/detach", *DEMO_INSTANCE_DISKS_URL); + pub static ref DEMO_INSTANCE_CREATE: params::InstanceCreate = + params::InstanceCreate { + identity: IdentityMetadataCreateParams { + name: DEMO_INSTANCE_NAME.clone(), + description: "".parse().unwrap(), + }, + ncpus: InstanceCpuCount(1), + memory: ByteCount::from_gibibytes_u32(16), + hostname: String::from("demo-instance"), + network_interfaces: + params::InstanceNetworkInterfaceAttachment::Default, + }; +} + +/// Describes an API endpoint to be verified by the "unauthorized" test +/// +/// These structs are also used to check whether we're covering all endpoints in +/// the public OpenAPI spec. +pub struct VerifyEndpoint { + /// URL path for the HTTP resource to test + /// + /// Note that we might talk about the "GET organization" endpoint, and might + /// write that "GET /organizations/{organization_name}". But the URL here + /// is for a specific HTTP resource, so it would look like + /// "/organizations/demo-org" rather than + /// "/organizations/{organization_name}". + pub url: &'static str, + + /// Specifies whether an HTTP resource handled by this endpoint is visible + /// to unauthenticated or unauthorized users + /// + /// If it's [`Visibility::Public`] (like "/organizations"), unauthorized + /// users can expect to get back a 401 or 403 when they attempt to access + /// it. If it's [`Visibility::Protected`] (like a specific Organization), + /// unauthorized users will get a 404. + pub visibility: Visibility, + + /// Specifies what HTTP methods are supported for this HTTP resource + /// + /// The test runner tests a variety of HTTP methods. For each method, if + /// it's not in this list, we expect a 405 "Method Not Allowed" response. + /// For `PUT` and `POST`, the item in `allowed_methods` also contains the + /// contents of the body to send with the `PUT` or `POST` request. This + /// should be valid input for the endpoint. Otherwise, Nexus could choose + /// to fail with a 400-level validation error, which would obscure the + /// authn/authz error we're looking for. + pub allowed_methods: Vec, +} + +/// Describes the visibility of an HTTP resource +pub enum Visibility { + /// All users can see the resource (including unauthenticated or + /// unauthorized users) + /// + /// "/organizations" is Public, for example. + Public, + + /// Only users with certain privileges can see this endpoint + /// + /// "/organizations/demo-org" is not public, for example. + Protected, +} + +/// Describes an HTTP method supported by a particular API endpoint +pub enum AllowedMethod { + /// HTTP "DELETE" method + Delete, + /// HTTP "GET" method + Get, + /// HTTP "POST" method, with sample input (which should be valid input for + /// this endpoint) + Post(serde_json::Value), + /// HTTP "PUT" method, with sample input (which should be valid input for + /// this endpoint) + Put(serde_json::Value), +} + +impl AllowedMethod { + /// Returns the [`http::Method`] used to make a request for this HTTP method + pub fn http_method(&self) -> &'static http::Method { + match self { + AllowedMethod::Delete => &Method::DELETE, + AllowedMethod::Get => &Method::GET, + AllowedMethod::Post(_) => &Method::POST, + AllowedMethod::Put(_) => &Method::PUT, + } + } + + /// Returns a JSON value suitable for use as the request body when making a + /// request to a specific endpoint using this HTTP method + /// + /// If this returns `None`, the request body should be empty. + pub fn body(&self) -> Option<&serde_json::Value> { + match self { + AllowedMethod::Delete | AllowedMethod::Get => None, + AllowedMethod::Post(body) => Some(&body), + AllowedMethod::Put(body) => Some(&body), + } + } +} + +lazy_static! { + pub static ref URL_USERS_DB_INIT: String = + format!("/users/{}", authn::USER_DB_INIT.name); + + /// List of endpoints to be verified + pub static ref VERIFY_ENDPOINTS: Vec = vec![ + /* Organizations */ + + VerifyEndpoint { + url: "/organizations", + visibility: Visibility::Public, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Post( + serde_json::to_value(&*DEMO_ORG_CREATE).unwrap() + ) + ], + }, + VerifyEndpoint { + url: &*DEMO_ORG_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Delete, + AllowedMethod::Put( + serde_json::to_value(¶ms::OrganizationUpdate { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some("different".to_string()) + } + }).unwrap() + ), + ], + }, + + /* Projects */ + + // TODO-security TODO-correctness One thing that's a little strange + // here: we currently return a 404 if you attempt to create a Project + // inside an Organization and you're not authorized to do that. In an + // ideal world, we'd return a 403 if you can _see_ the Organization and + // a 404 if not. But we don't really know if you should be able to see + // the Organization. Right now, the only real way to tell that is if + // you have permissions on anything _inside_ the Organization, which is + // incredibly expensive to determine in general. + VerifyEndpoint { + url: &*DEMO_ORG_PROJECTS_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Post( + serde_json::to_value(&*DEMO_PROJECT_CREATE).unwrap() + ), + ], + }, + VerifyEndpoint { + url: &*DEMO_PROJECT_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Delete, + AllowedMethod::Put( + serde_json::to_value(params::ProjectUpdate{ + identity: IdentityMetadataUpdateParams { + name: None, + description: Some("different".to_string()) + }, + }).unwrap() + ), + ], + }, + + /* VPCs */ + VerifyEndpoint { + url: &*DEMO_PROJECT_URL_VPCS, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Post( + serde_json::to_value(&*DEMO_VPC_CREATE).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &*DEMO_VPC_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Put( + serde_json::to_value(¶ms::VpcUpdate { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some("different".to_string()) + }, + dns_name: None, + }).unwrap() + ), + AllowedMethod::Delete, + ], + }, + + /* Firewall rules */ + VerifyEndpoint { + url: &*DEMO_VPC_URL_FIREWALL_RULES, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Put( + serde_json::to_value(VpcFirewallRuleUpdateParams { + rules: vec![], + }).unwrap() + ), + ], + }, + + /* VPC Subnets */ + VerifyEndpoint { + url: &*DEMO_VPC_URL_SUBNETS, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Post( + serde_json::to_value(&*DEMO_VPC_SUBNET_CREATE).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &*DEMO_VPC_SUBNET_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Put( + serde_json::to_value(¶ms::VpcSubnetUpdate { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some("different".to_string()) + }, + ipv4_block: None, + ipv6_block: None, + }).unwrap() + ), + AllowedMethod::Delete, + ], + }, + + /* VPC Routers */ + + VerifyEndpoint { + url: &*DEMO_VPC_URL_ROUTERS, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Post( + serde_json::to_value(&*DEMO_VPC_ROUTER_CREATE).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &*DEMO_VPC_ROUTER_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Put( + serde_json::to_value(¶ms::VpcRouterUpdate { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some("different".to_string()) + }, + }).unwrap() + ), + AllowedMethod::Delete, + ], + }, + + /* Router Routes */ + + VerifyEndpoint { + url: &*DEMO_VPC_ROUTER_URL_ROUTES, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Post( + serde_json::to_value(&*DEMO_ROUTER_ROUTE_CREATE).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &*DEMO_ROUTER_ROUTE_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Put( + serde_json::to_value(&RouterRouteUpdateParams { + identity: IdentityMetadataUpdateParams { + name: None, + description: Some("different".to_string()) + }, + target: RouteTarget::Ip( + IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))), + destination: RouteDestination::Subnet( + "loopback".parse().unwrap()), + }).unwrap() + ), + AllowedMethod::Delete, + ], + }, + + + /* Disks */ + + VerifyEndpoint { + url: &*DEMO_PROJECT_URL_DISKS, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Post( + serde_json::to_value(&*DEMO_DISK_CREATE).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &*DEMO_DISK_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Delete, + ], + }, + + VerifyEndpoint { + url: &*DEMO_INSTANCE_DISKS_ATTACH_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value(params::DiskIdentifier { + disk: DEMO_DISK_NAME.clone() + }).unwrap() + ) + ], + }, + VerifyEndpoint { + url: &*DEMO_INSTANCE_DISKS_DETACH_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Post( + serde_json::to_value(params::DiskIdentifier { + disk: DEMO_DISK_NAME.clone() + }).unwrap() + ) + ], + }, + + /* Instances */ + VerifyEndpoint { + url: &*DEMO_PROJECT_URL_INSTANCES, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Post( + serde_json::to_value(&*DEMO_INSTANCE_CREATE).unwrap() + ), + ], + }, + + VerifyEndpoint { + url: &*DEMO_INSTANCE_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Get, + AllowedMethod::Delete, + ], + }, + + VerifyEndpoint { + url: &*DEMO_INSTANCE_START_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Post(serde_json::Value::Null) + ], + }, + VerifyEndpoint { + url: &*DEMO_INSTANCE_STOP_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Post(serde_json::Value::Null) + ], + }, + VerifyEndpoint { + url: &*DEMO_INSTANCE_REBOOT_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Post(serde_json::Value::Null) + ], + }, + VerifyEndpoint { + url: &*DEMO_INSTANCE_MIGRATE_URL, + visibility: Visibility::Protected, + allowed_methods: vec![ + AllowedMethod::Post(serde_json::to_value( + params::InstanceMigrate { + dst_sled_uuid: uuid::Uuid::new_v4(), + } + ).unwrap()), + ], + }, + + /* IAM */ + + VerifyEndpoint { + url: "/roles", + visibility: Visibility::Public, + allowed_methods: vec![AllowedMethod::Get], + }, + VerifyEndpoint { + url: "/roles/fleet.admin", + visibility: Visibility::Protected, + allowed_methods: vec![AllowedMethod::Get], + }, + + VerifyEndpoint { + url: "/users", + visibility: Visibility::Public, + allowed_methods: vec![AllowedMethod::Get], + }, + VerifyEndpoint { + url: &*URL_USERS_DB_INIT, + visibility: Visibility::Protected, + allowed_methods: vec![AllowedMethod::Get], + }, + ]; +} diff --git a/nexus/tests/integration_tests/mod.rs b/nexus/tests/integration_tests/mod.rs index 939ba538de..5473cae865 100644 --- a/nexus/tests/integration_tests/mod.rs +++ b/nexus/tests/integration_tests/mod.rs @@ -18,6 +18,7 @@ mod router_routes; mod subnet_allocation; mod timeseries; mod unauthorized; +mod unauthorized_coverage; mod updates; mod users_builtin; mod vpc_firewall; @@ -25,3 +26,6 @@ mod vpc_routers; mod vpc_subnets; mod vpcs; mod zpools; + +// This module is used only for shared data, not test cases. +mod endpoints; diff --git a/nexus/tests/integration_tests/unauthorized.rs b/nexus/tests/integration_tests/unauthorized.rs index 0b2c7bd27c..8f6f774511 100644 --- a/nexus/tests/integration_tests/unauthorized.rs +++ b/nexus/tests/integration_tests/unauthorized.rs @@ -5,9 +5,7 @@ //! Verify the behavior of API endpoints when hit by unauthenticated and //! unauthorized users -use std::net::IpAddr; -use std::net::Ipv4Addr; - +use super::endpoints::*; use dropshot::test_util::ClientTestContext; use dropshot::HttpErrorResponseBody; use headers::authorization::Credentials; @@ -21,20 +19,7 @@ use nexus_test_utils::http_testing::TestResponse; use nexus_test_utils::resource_helpers::DiskTest; use nexus_test_utils::ControlPlaneTestContext; use nexus_test_utils_macros::nexus_test; -use omicron_common::api::external::ByteCount; -use omicron_common::api::external::IdentityMetadataCreateParams; -use omicron_common::api::external::IdentityMetadataUpdateParams; -use omicron_common::api::external::InstanceCpuCount; -use omicron_common::api::external::Ipv4Net; -use omicron_common::api::external::Name; -use omicron_common::api::external::RouteDestination; -use omicron_common::api::external::RouteTarget; -use omicron_common::api::external::RouterRouteCreateParams; -use omicron_common::api::external::RouterRouteUpdateParams; -use omicron_common::api::external::VpcFirewallRuleUpdateParams; -use omicron_nexus::authn; use omicron_nexus::authn::external::spoof; -use omicron_nexus::external_api::params; // This test hits a list Nexus API endpoints using both unauthenticated and // unauthorized requests to make sure we get the expected behavior (generally: @@ -87,6 +72,7 @@ async fn test_unauthorized(cptestctx: &ControlPlaneTestContext) { } } +// // SETUP PHASE // @@ -146,569 +132,6 @@ lazy_static! { body: serde_json::to_value(&*DEMO_INSTANCE_CREATE).unwrap(), }, ]; - - // Organization used for testing - static ref DEMO_ORG_NAME: Name = "demo-org".parse().unwrap(); - static ref DEMO_ORG_URL: String = - format!("/organizations/{}", *DEMO_ORG_NAME); - static ref DEMO_ORG_PROJECTS_URL: String = - format!("{}/projects", *DEMO_ORG_URL); - static ref DEMO_ORG_CREATE: params::OrganizationCreate = - params::OrganizationCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_ORG_NAME.clone(), - description: String::from(""), - }, - }; - - // Project used for testing - static ref DEMO_PROJECT_NAME: Name = "demo-project".parse().unwrap(); - static ref DEMO_PROJECT_URL: String = - format!("{}/{}", *DEMO_ORG_PROJECTS_URL, *DEMO_PROJECT_NAME); - static ref DEMO_PROJECT_URL_DISKS: String = - format!("{}/disks", *DEMO_PROJECT_URL); - static ref DEMO_PROJECT_URL_INSTANCES: String = - format!("{}/instances", *DEMO_PROJECT_URL); - static ref DEMO_PROJECT_URL_VPCS: String = - format!("{}/vpcs", *DEMO_PROJECT_URL); - static ref DEMO_PROJECT_CREATE: params::ProjectCreate = - params::ProjectCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_PROJECT_NAME.clone(), - description: String::from(""), - }, - }; - - // VPC used for testing - static ref DEMO_VPC_NAME: Name = "demo-vpc".parse().unwrap(); - static ref DEMO_VPC_URL: String = - format!("{}/{}", *DEMO_PROJECT_URL_VPCS, *DEMO_VPC_NAME); - static ref DEMO_VPC_URL_FIREWALL_RULES: String = - format!("{}/firewall/rules", *DEMO_VPC_URL); - static ref DEMO_VPC_URL_ROUTERS: String = - format!("{}/routers", *DEMO_VPC_URL); - static ref DEMO_VPC_URL_SUBNETS: String = - format!("{}/subnets", *DEMO_VPC_URL); - static ref DEMO_VPC_CREATE: params::VpcCreate = - params::VpcCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_NAME.clone(), - description: String::from(""), - }, - ipv6_prefix: None, - dns_name: DEMO_VPC_NAME.clone(), - }; - - // VPC Subnet used for testing - static ref DEMO_VPC_SUBNET_NAME: Name = "demo-vpc-subnet".parse().unwrap(); - static ref DEMO_VPC_SUBNET_URL: String = - format!("{}/{}", *DEMO_VPC_URL_SUBNETS, *DEMO_VPC_SUBNET_NAME); - static ref DEMO_VPC_SUBNET_CREATE: params::VpcSubnetCreate = - params::VpcSubnetCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_SUBNET_NAME.clone(), - description: String::from(""), - }, - ipv4_block: Ipv4Net("10.1.2.3/8".parse().unwrap()), - ipv6_block: None, - }; - - // VPC Router used for testing - static ref DEMO_VPC_ROUTER_NAME: Name = "demo-vpc-router".parse().unwrap(); - static ref DEMO_VPC_ROUTER_URL: String = - format!("{}/{}", *DEMO_VPC_URL_ROUTERS, *DEMO_VPC_ROUTER_NAME); - static ref DEMO_VPC_ROUTER_URL_ROUTES: String = - format!("{}/routes", *DEMO_VPC_ROUTER_URL); - static ref DEMO_VPC_ROUTER_CREATE: params::VpcRouterCreate = - params::VpcRouterCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_VPC_ROUTER_NAME.clone(), - description: String::from(""), - }, - }; - - // Router Route used for testing - static ref DEMO_ROUTER_ROUTE_NAME: Name = - "demo-router-route".parse().unwrap(); - static ref DEMO_ROUTER_ROUTE_URL: String = - format!("{}/{}", *DEMO_VPC_ROUTER_URL_ROUTES, *DEMO_ROUTER_ROUTE_NAME); - static ref DEMO_ROUTER_ROUTE_CREATE: RouterRouteCreateParams = - RouterRouteCreateParams { - identity: IdentityMetadataCreateParams { - name: DEMO_ROUTER_ROUTE_NAME.clone(), - description: String::from(""), - }, - target: RouteTarget::Ip(IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))), - destination: RouteDestination::Subnet("loopback".parse().unwrap()), - }; - - // Disk used for testing - static ref DEMO_DISK_NAME: Name = "demo-disk".parse().unwrap(); - static ref DEMO_DISK_URL: String = - format!("{}/{}", *DEMO_PROJECT_URL_DISKS, *DEMO_DISK_NAME); - static ref DEMO_DISK_CREATE: params::DiskCreate = - params::DiskCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_DISK_NAME.clone(), - description: "".parse().unwrap(), - }, - snapshot_id: None, - size: ByteCount::from_gibibytes_u32(16), - }; - - // Instance used for testing - static ref DEMO_INSTANCE_NAME: Name = "demo-instance".parse().unwrap(); - static ref DEMO_INSTANCE_URL: String = - format!("{}/{}", *DEMO_PROJECT_URL_INSTANCES, *DEMO_INSTANCE_NAME); - static ref DEMO_INSTANCE_START_URL: String = - format!("{}/start", *DEMO_INSTANCE_URL); - static ref DEMO_INSTANCE_STOP_URL: String = - format!("{}/stop", *DEMO_INSTANCE_URL); - static ref DEMO_INSTANCE_REBOOT_URL: String = - format!("{}/reboot", *DEMO_INSTANCE_URL); - static ref DEMO_INSTANCE_MIGRATE_URL: String = - format!("{}/migrate", *DEMO_INSTANCE_URL); - static ref DEMO_INSTANCE_DISKS_URL: String = - format!("{}/disks", *DEMO_INSTANCE_URL); - static ref DEMO_INSTANCE_DISKS_ATTACH_URL: String = - format!("{}/attach", *DEMO_INSTANCE_DISKS_URL); - static ref DEMO_INSTANCE_DISKS_DETACH_URL: String = - format!("{}/detach", *DEMO_INSTANCE_DISKS_URL); - static ref DEMO_INSTANCE_CREATE: params::InstanceCreate = - params::InstanceCreate { - identity: IdentityMetadataCreateParams { - name: DEMO_INSTANCE_NAME.clone(), - description: "".parse().unwrap(), - }, - ncpus: InstanceCpuCount(1), - memory: ByteCount::from_gibibytes_u32(16), - hostname: String::from("demo-instance"), - network_interfaces: params::InstanceNetworkInterfaceAttachment::Default, - }; -} - -// VERIFY PHASE -// - -/// Describes an API endpoint to be verified -struct VerifyEndpoint { - /// URL path for the HTTP resource to test - /// - /// Note that we might talk about the "GET organization" endpoint, and might - /// write that "GET /organizations/{organization_name}". But the URL here - /// is for a specific HTTP resource, so it would look like - /// "/organizations/demo-org" rather than - /// "/organizations/{organization_name}". - url: &'static str, - - /// Specifies whether an HTTP resource handled by this endpoint is visible - /// to unauthenticated or unauthorized users - /// - /// If it's [`Visibility::Public`] (like "/organizations"), unauthorized - /// users can expect to get back a 401 or 403 when they attempt to access - /// it. If it's [`Visibility::Protected`] (like a specific Organization), - /// unauthorized users will get a 404. - visibility: Visibility, - - /// Specifies what HTTP methods are supported for this HTTP resource - /// - /// The test runner tests a variety of HTTP methods. For each method, if - /// it's not in this list, we expect a 405 "Method Not Allowed" response. - /// For `PUT` and `POST`, the item in `allowed_methods` also contains the - /// contents of the body to send with the `PUT` or `POST` request. This - /// should be valid input for the endpoint. Otherwise, Nexus could choose - /// to fail with a 400-level validation error, which would obscure the - /// authn/authz error we're looking for. - allowed_methods: Vec, -} - -/// Describes the visibility of an HTTP resource -enum Visibility { - /// All users can see the resource (including unauthenticated or - /// unauthorized users) - /// - /// "/organizations" is Public, for example. - Public, - - /// Only users with certain privileges can see this endpoint - /// - /// "/organizations/demo-org" is not public, for example. - Protected, -} - -/// Describes an HTTP method supported by a particular API endpoint -enum AllowedMethod { - /// HTTP "DELETE" method - Delete, - /// HTTP "GET" method - Get, - /// HTTP "POST" method, with sample input (which should be valid input for - /// this endpoint) - Post(serde_json::Value), - /// HTTP "PUT" method, with sample input (which should be valid input for - /// this endpoint) - Put(serde_json::Value), -} - -impl AllowedMethod { - /// Returns the [`http::Method`] used to make a request for this HTTP method - fn http_method(&self) -> &'static http::Method { - match self { - AllowedMethod::Delete => &Method::DELETE, - AllowedMethod::Get => &Method::GET, - AllowedMethod::Post(_) => &Method::POST, - AllowedMethod::Put(_) => &Method::PUT, - } - } - - /// Returns a JSON value suitable for use as the request body when making a - /// request to a specific endpoint using this HTTP method - /// - /// If this returns `None`, the request body should be empty. - fn body(&self) -> Option<&serde_json::Value> { - match self { - AllowedMethod::Delete | AllowedMethod::Get => None, - AllowedMethod::Post(body) => Some(&body), - AllowedMethod::Put(body) => Some(&body), - } - } -} - -lazy_static! { - static ref URL_USERS_DB_INIT: String = - format!("/users/{}", authn::USER_DB_INIT.name); - - /// List of endpoints to be verified - static ref VERIFY_ENDPOINTS: Vec = vec![ - /* Organizations */ - - VerifyEndpoint { - url: "/organizations", - visibility: Visibility::Public, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Post( - serde_json::to_value(&*DEMO_ORG_CREATE).unwrap() - ) - ], - }, - VerifyEndpoint { - url: &*DEMO_ORG_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Delete, - AllowedMethod::Put( - serde_json::to_value(¶ms::OrganizationUpdate { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some("different".to_string()) - } - }).unwrap() - ), - ], - }, - - /* Projects */ - - // TODO-security TODO-correctness One thing that's a little strange - // here: we currently return a 404 if you attempt to create a Project - // inside an Organization and you're not authorized to do that. In an - // ideal world, we'd return a 403 if you can _see_ the Organization and - // a 404 if not. But we don't really know if you should be able to see - // the Organization. Right now, the only real way to tell that is if - // you have permissions on anything _inside_ the Organization, which is - // incredibly expensive to determine in general. - VerifyEndpoint { - url: &*DEMO_ORG_PROJECTS_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Post( - serde_json::to_value(&*DEMO_PROJECT_CREATE).unwrap() - ), - ], - }, - VerifyEndpoint { - url: &*DEMO_PROJECT_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Delete, - AllowedMethod::Put( - serde_json::to_value(params::ProjectUpdate{ - identity: IdentityMetadataUpdateParams { - name: None, - description: Some("different".to_string()) - }, - }).unwrap() - ), - ], - }, - - /* VPCs */ - VerifyEndpoint { - url: &*DEMO_PROJECT_URL_VPCS, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Post( - serde_json::to_value(&*DEMO_VPC_CREATE).unwrap() - ), - ], - }, - - VerifyEndpoint { - url: &*DEMO_VPC_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Put( - serde_json::to_value(¶ms::VpcUpdate { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some("different".to_string()) - }, - dns_name: None, - }).unwrap() - ), - AllowedMethod::Delete, - ], - }, - - /* Firewall rules */ - VerifyEndpoint { - url: &*DEMO_VPC_URL_FIREWALL_RULES, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Put( - serde_json::to_value(VpcFirewallRuleUpdateParams { - rules: vec![], - }).unwrap() - ), - ], - }, - - /* VPC Subnets */ - VerifyEndpoint { - url: &*DEMO_VPC_URL_SUBNETS, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Post( - serde_json::to_value(&*DEMO_VPC_SUBNET_CREATE).unwrap() - ), - ], - }, - - VerifyEndpoint { - url: &*DEMO_VPC_SUBNET_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Put( - serde_json::to_value(¶ms::VpcSubnetUpdate { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some("different".to_string()) - }, - ipv4_block: None, - ipv6_block: None, - }).unwrap() - ), - AllowedMethod::Delete, - ], - }, - - /* VPC Routers */ - - VerifyEndpoint { - url: &*DEMO_VPC_URL_ROUTERS, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Post( - serde_json::to_value(&*DEMO_VPC_ROUTER_CREATE).unwrap() - ), - ], - }, - - VerifyEndpoint { - url: &*DEMO_VPC_ROUTER_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Put( - serde_json::to_value(¶ms::VpcRouterUpdate { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some("different".to_string()) - }, - }).unwrap() - ), - AllowedMethod::Delete, - ], - }, - - /* Router Routes */ - - VerifyEndpoint { - url: &*DEMO_VPC_ROUTER_URL_ROUTES, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Post( - serde_json::to_value(&*DEMO_ROUTER_ROUTE_CREATE).unwrap() - ), - ], - }, - - VerifyEndpoint { - url: &*DEMO_ROUTER_ROUTE_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Put( - serde_json::to_value(&RouterRouteUpdateParams { - identity: IdentityMetadataUpdateParams { - name: None, - description: Some("different".to_string()) - }, - target: RouteTarget::Ip( - IpAddr::from(Ipv4Addr::new(127, 0, 0, 1))), - destination: RouteDestination::Subnet( - "loopback".parse().unwrap()), - }).unwrap() - ), - AllowedMethod::Delete, - ], - }, - - - /* Disks */ - - VerifyEndpoint { - url: &*DEMO_PROJECT_URL_DISKS, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Post( - serde_json::to_value(&*DEMO_DISK_CREATE).unwrap() - ), - ], - }, - - VerifyEndpoint { - url: &*DEMO_DISK_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Delete, - ], - }, - - VerifyEndpoint { - url: &*DEMO_INSTANCE_DISKS_ATTACH_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Post( - serde_json::to_value(params::DiskIdentifier { - disk: DEMO_DISK_NAME.clone() - }).unwrap() - ) - ], - }, - VerifyEndpoint { - url: &*DEMO_INSTANCE_DISKS_DETACH_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Post( - serde_json::to_value(params::DiskIdentifier { - disk: DEMO_DISK_NAME.clone() - }).unwrap() - ) - ], - }, - - /* Instances */ - VerifyEndpoint { - url: &*DEMO_PROJECT_URL_INSTANCES, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Post( - serde_json::to_value(&*DEMO_INSTANCE_CREATE).unwrap() - ), - ], - }, - - VerifyEndpoint { - url: &*DEMO_INSTANCE_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Get, - AllowedMethod::Delete, - ], - }, - - VerifyEndpoint { - url: &*DEMO_INSTANCE_START_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Post(serde_json::Value::Null) - ], - }, - VerifyEndpoint { - url: &*DEMO_INSTANCE_STOP_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Post(serde_json::Value::Null) - ], - }, - VerifyEndpoint { - url: &*DEMO_INSTANCE_REBOOT_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Post(serde_json::Value::Null) - ], - }, - VerifyEndpoint { - url: &*DEMO_INSTANCE_MIGRATE_URL, - visibility: Visibility::Protected, - allowed_methods: vec![ - AllowedMethod::Post(serde_json::to_value( - params::InstanceMigrate { - dst_sled_uuid: uuid::Uuid::new_v4(), - } - ).unwrap()), - ], - }, - - /* IAM */ - - VerifyEndpoint { - url: "/roles", - visibility: Visibility::Public, - allowed_methods: vec![AllowedMethod::Get], - }, - VerifyEndpoint { - url: "/roles/fleet.admin", - visibility: Visibility::Protected, - allowed_methods: vec![AllowedMethod::Get], - }, - - VerifyEndpoint { - url: "/users", - visibility: Visibility::Public, - allowed_methods: vec![AllowedMethod::Get], - }, - VerifyEndpoint { - url: &*URL_USERS_DB_INIT, - visibility: Visibility::Protected, - allowed_methods: vec![AllowedMethod::Get], - }, - ]; } /// Verifies a single API endpoint, described with `endpoint` diff --git a/nexus/tests/integration_tests/unauthorized_coverage.rs b/nexus/tests/integration_tests/unauthorized_coverage.rs new file mode 100644 index 0000000000..6cd74eeabd --- /dev/null +++ b/nexus/tests/integration_tests/unauthorized_coverage.rs @@ -0,0 +1,143 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use super::endpoints::VERIFY_ENDPOINTS; +use expectorate::assert_contents; +use openapiv3::OpenAPI; +use std::collections::BTreeMap; + +/// Checks for uncovered public API endpoints +/// +/// This test compares the endpoints covered by the "unauthorized" test with +/// what's in the OpenAPI spec for the public API to make sure all endpoints are +/// accounted-for. +#[test] +fn test_unauthorized_coverage() { + // Load the OpenAPI schema for Nexus's public API. + let schema_path = "../openapi/nexus.json"; + let schema_contents = std::fs::read_to_string(&schema_path) + .expect("failed to read Nexus OpenAPI spec"); + let spec: OpenAPI = serde_json::from_str(&schema_contents) + .expect("Nexus OpenAPI spec was not valid OpenAPI"); + + // Take each operation that we find in the OpenAPI spec, make a regular + // expression that we can use to match against it (more on this below), and + // throw them into a BTreeMap. + let mut spec_operations: BTreeMap = spec + .operations() + .map(|(path, method, op)| { + // We're going to take URLs from our test cases and match them + // against operations in the API spec. The URLs from the API spec + // contain variables (e.g., "/instances/{instance_name}"). Our test + // cases have those variables filled in already (e.g., + // "/instances/my-instance"). + // + // To match a URL from the test case against one from the OpenAPI + // spec, we're going to: + // + // - use a regular expression to replace `{varname}` in the API + // spec's URL with `[^/]+` (one or more non-slash characters) + // + // - use that string as the basis for a second regex that we'll + // store with the Operation. We'll use this second regex to match + // URLs to their operation. + // + // This is slow (lookups will take time linear in the total number + // of API endpoints) and a little cheesy, but it's expedient and + // robust enough for our purposes. + // + // This will fail badly if it turns out that the URL contains any + // characters that would be interpreted specially by the regular + // expression engine. So let's check up front that those aren't + // present. + assert!( + path.chars().all(|c| c.is_ascii_alphanumeric() + || c == '_' + || c == '-' + || c == '{' + || c == '}' + || c == '/'), + "unexpected character in URL: {:?}", + path + ); + let re = regex::Regex::new("/\\{[^}]+\\}").unwrap(); + let regex_path = re.replace_all(path, "/[^/]+"); + let regex = regex::Regex::new(&format!("^{}$", regex_path)) + .expect("modified URL string was not a valid regex"); + let label = op + .operation_id + .clone() + .unwrap_or(String::from("unknown operation-id")); + (Operation { method, path, label }, regex) + }) + .collect(); + + println!("spec operations: {:?}", spec_operations); + + // Go through each of the authz test cases and match each one against an + // OpenAPI operation. + let mut unexpected_endpoints = String::from( + "API endpoints tested by unauthorized.rs but not found \ + in the OpenAPI spec:\n", + ); + for v in &*VERIFY_ENDPOINTS { + for m in &v.allowed_methods { + let method_string = m.http_method().to_string().to_uppercase(); + let found = spec_operations.iter().find(|(op, regex)| { + op.method.to_uppercase() == method_string + && regex.is_match(v.url) + }); + if let Some((op, _)) = found { + println!( + "covered: {:40} ({:6?} {:?}) (by {:?})", + op.label, op.method, op.path, v.url + ); + let op = op.clone(); + spec_operations.remove(&op); + } else { + unexpected_endpoints + .push_str(&format!("{:6} {:?}", method_string, v.url)); + } + } + } + + println!("-----"); + + // If you're here because this assertion failed, we found an endpoint tested + // by "unauthorized.rs" that's not in the OpenAPI spec. This could happen + // if you're adding a test for an endpoint that's marked "unpublished". In + // that case, you might just allow expectorate to add this endpoint to the + // allowlist here. + assert_contents( + "tests/output/unexpected-authz-endpoints.txt", + &unexpected_endpoints, + ); + + // Check for uncovered endpoints (endpoints that are in the OpenAPI spec but + // not tested by the authz tests). + let mut uncovered_endpoints = + "API endpoints with no coverage in authz tests:\n".to_string(); + for op in spec_operations.keys() { + uncovered_endpoints.push_str(&format!( + "{:40} ({:6} {:?})\n", + op.label, op.method, op.path + )); + } + + // If you're here because this assertion failed, check that if you've added + // any API operations to Nexus, you've also added a corresponding test in + // "unauthorized.rs" so that it will automatically be checked for its + // behavior for unauthenticated and unauthorized users. + assert_contents( + "tests/output/uncovered-authz-endpoints.txt", + &uncovered_endpoints, + ); +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)] +struct Operation<'a> { + method: &'a str, + path: &'a str, + label: String, +} diff --git a/nexus/tests/output/uncovered-authz-endpoints.txt b/nexus/tests/output/uncovered-authz-endpoints.txt new file mode 100644 index 0000000000..426954b0f6 --- /dev/null +++ b/nexus/tests/output/uncovered-authz-endpoints.txt @@ -0,0 +1,22 @@ +API endpoints with no coverage in authz tests: +instance_network_interfaces_delete_interface (delete "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}") +project_snapshots_delete_snapshot (delete "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}") +hardware_racks_get (get "/hardware/racks") +hardware_racks_get_rack (get "/hardware/racks/{rack_id}") +hardware_sleds_get (get "/hardware/sleds") +hardware_sleds_get_sled (get "/hardware/sleds/{sled_id}") +instance_disks_get (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/disks") +instance_network_interfaces_get (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces") +instance_network_interfaces_get_interface (get "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces/{interface_name}") +project_snapshots_get (get "/organizations/{organization_name}/projects/{project_name}/snapshots") +project_snapshots_get_snapshot (get "/organizations/{organization_name}/projects/{project_name}/snapshots/{snapshot_name}") +subnet_network_interfaces_get (get "/organizations/{organization_name}/projects/{project_name}/vpcs/{vpc_name}/subnets/{subnet_name}/network-interfaces") +sagas_get (get "/sagas") +sagas_get_saga (get "/sagas/{saga_id}") +session_me (get "/session/me") +timeseries_schema_get (get "/timeseries/schema") +spoof_login (post "/login") +logout (post "/logout") +instance_network_interfaces_post (post "/organizations/{organization_name}/projects/{project_name}/instances/{instance_name}/network-interfaces") +project_snapshots_post (post "/organizations/{organization_name}/projects/{project_name}/snapshots") +updates_refresh (post "/updates/refresh") diff --git a/nexus/tests/output/unexpected-authz-endpoints.txt b/nexus/tests/output/unexpected-authz-endpoints.txt new file mode 100644 index 0000000000..1d480aa36b --- /dev/null +++ b/nexus/tests/output/unexpected-authz-endpoints.txt @@ -0,0 +1 @@ +API endpoints tested by unauthorized.rs but not found in the OpenAPI spec: