From 247b2059ea3667bdf4803f763560316302489456 Mon Sep 17 00:00:00 2001 From: Adam Leventhal Date: Sun, 5 May 2024 17:10:47 -0700 Subject: [PATCH] system networking allow-list cli (#658) --- cli/docs/cli.json | 29 ++ cli/src/cli_builder.rs | 25 + cli/src/generated_cli.rs | 97 ++++ cli/src/main.rs | 81 ++- ...est_system_networking_allowlist_any.stdout | 7 + ...st_system_networking_allowlist_both.stderr | 5 + ...ystem_networking_allowlist_many_ips.stdout | 14 + ...system_networking_allowlist_neither.stderr | 6 + ..._system_networking_allowlist_one_ip.stdout | 10 + cli/tests/test_system_networking_allowlist.rs | 183 +++++++ oxide.json | 152 +++++- sdk-httpmock/src/generated_httpmock.rs | 150 ++++++ sdk/src/generated_sdk.rs | 468 +++++++++++++++++- 13 files changed, 1219 insertions(+), 8 deletions(-) create mode 100644 cli/tests/data/test_system_networking_allowlist_any.stdout create mode 100644 cli/tests/data/test_system_networking_allowlist_both.stderr create mode 100644 cli/tests/data/test_system_networking_allowlist_many_ips.stdout create mode 100644 cli/tests/data/test_system_networking_allowlist_neither.stderr create mode 100644 cli/tests/data/test_system_networking_allowlist_one_ip.stdout create mode 100644 cli/tests/test_system_networking_allowlist.rs diff --git a/cli/docs/cli.json b/cli/docs/cli.json index b6a807f3..4697c838 100644 --- a/cli/docs/cli.json +++ b/cli/docs/cli.json @@ -3088,6 +3088,35 @@ } ] }, + { + "name": "allow-list", + "subcommands": [ + { + "name": "update", + "about": "Update user-facing services IP allowlist", + "args": [ + { + "long": "any" + }, + { + "long": "ip" + }, + { + "long": "json-body", + "help": "Path to a file that contains the full json body." + }, + { + "long": "json-body-template", + "help": "XXX" + } + ] + }, + { + "name": "view", + "about": "Get user-facing services IP allowlist" + } + ] + }, { "name": "bfd", "subcommands": [ diff --git a/cli/src/cli_builder.rs b/cli/src/cli_builder.rs index 8eb873c0..67fcb408 100644 --- a/cli/src/cli_builder.rs +++ b/cli/src/cli_builder.rs @@ -117,6 +117,28 @@ impl<'a> Default for NewCli<'a> { .multiple(false), ), + CliCommand::NetworkingAllowListUpdate => cmd + .mut_arg("json-body", |arg| arg.required(false)) + .arg( + clap::Arg::new("any") + .long("any") + .action(clap::ArgAction::SetTrue) + .value_parser(clap::value_parser!(bool)), + ) + .arg( + clap::Arg::new("ips") + .long("ip") + .action(clap::ArgAction::Append) + .value_name("IP or IPNET") + .value_parser(clap::value_parser!(crate::IpOrNet)), + ) + .group( + clap::ArgGroup::new("allow-list") + .args(["ips", "any"]) + .required(true) + .multiple(false), + ), + // Command is fine as-is. _ => cmd, }; @@ -467,6 +489,9 @@ fn xxx<'a>(command: CliCommand) -> Option<&'a str> { CliCommand::SystemPolicyView => Some("system policy view"), CliCommand::SystemPolicyUpdate => Some("system policy update"), + CliCommand::NetworkingAllowListView => Some("system networking allow-list view"), + CliCommand::NetworkingAllowListUpdate => Some("system networking allow-list update"), + CliCommand::CurrentUserView => Some("current-user view"), CliCommand::CurrentUserGroups => Some("current-user groups"), CliCommand::CurrentUserSshKeyList => Some("current-user ssh-key list"), diff --git a/cli/src/generated_cli.rs b/cli/src/generated_cli.rs index fbc987c2..12225765 100644 --- a/cli/src/generated_cli.rs +++ b/cli/src/generated_cli.rs @@ -153,6 +153,8 @@ impl Cli { CliCommand::NetworkingAddressLotBlockList => { Self::cli_networking_address_lot_block_list() } + CliCommand::NetworkingAllowListView => Self::cli_networking_allow_list_view(), + CliCommand::NetworkingAllowListUpdate => Self::cli_networking_allow_list_update(), CliCommand::NetworkingBfdDisable => Self::cli_networking_bfd_disable(), CliCommand::NetworkingBfdEnable => Self::cli_networking_bfd_enable(), CliCommand::NetworkingBfdStatus => Self::cli_networking_bfd_status(), @@ -3953,6 +3955,29 @@ impl Cli { .about("List blocks in address lot") } + pub fn cli_networking_allow_list_view() -> clap::Command { + clap::Command::new("").about("Get user-facing services IP allowlist") + } + + pub fn cli_networking_allow_list_update() -> clap::Command { + clap::Command::new("") + .arg( + clap::Arg::new("json-body") + .long("json-body") + .value_name("JSON-FILE") + .required(true) + .value_parser(clap::value_parser!(std::path::PathBuf)) + .help("Path to a file that contains the full json body."), + ) + .arg( + clap::Arg::new("json-body-template") + .long("json-body-template") + .action(clap::ArgAction::SetTrue) + .help("XXX"), + ) + .about("Update user-facing services IP allowlist") + } + pub fn cli_networking_bfd_disable() -> clap::Command { clap::Command::new("") .arg( @@ -5689,6 +5714,12 @@ impl Cli { self.execute_networking_address_lot_block_list(matches) .await } + CliCommand::NetworkingAllowListView => { + self.execute_networking_allow_list_view(matches).await + } + CliCommand::NetworkingAllowListUpdate => { + self.execute_networking_allow_list_update(matches).await + } CliCommand::NetworkingBfdDisable => self.execute_networking_bfd_disable(matches).await, CliCommand::NetworkingBfdEnable => self.execute_networking_bfd_enable(matches).await, CliCommand::NetworkingBfdStatus => self.execute_networking_bfd_status(matches).await, @@ -10021,6 +10052,52 @@ impl Cli { } } + pub async fn execute_networking_allow_list_view( + &self, + matches: &clap::ArgMatches, + ) -> anyhow::Result<()> { + let mut request = self.client.networking_allow_list_view(); + self.config + .execute_networking_allow_list_view(matches, &mut request)?; + let result = request.send().await; + match result { + Ok(r) => { + self.config.item_success(&r); + Ok(()) + } + Err(r) => { + self.config.item_error(&r); + Err(anyhow::Error::new(r)) + } + } + } + + pub async fn execute_networking_allow_list_update( + &self, + matches: &clap::ArgMatches, + ) -> anyhow::Result<()> { + let mut request = self.client.networking_allow_list_update(); + if let Some(value) = matches.get_one::("json-body") { + let body_txt = std::fs::read_to_string(value).unwrap(); + let body_value = serde_json::from_str::(&body_txt).unwrap(); + request = request.body(body_value); + } + + self.config + .execute_networking_allow_list_update(matches, &mut request)?; + let result = request.send().await; + match result { + Ok(r) => { + self.config.item_success(&r); + Ok(()) + } + Err(r) => { + self.config.item_error(&r); + Err(anyhow::Error::new(r)) + } + } + } + pub async fn execute_networking_bfd_disable( &self, matches: &clap::ArgMatches, @@ -12901,6 +12978,22 @@ pub trait CliConfig { Ok(()) } + fn execute_networking_allow_list_view( + &self, + matches: &clap::ArgMatches, + request: &mut builder::NetworkingAllowListView, + ) -> anyhow::Result<()> { + Ok(()) + } + + fn execute_networking_allow_list_update( + &self, + matches: &clap::ArgMatches, + request: &mut builder::NetworkingAllowListUpdate, + ) -> anyhow::Result<()> { + Ok(()) + } + fn execute_networking_bfd_disable( &self, matches: &clap::ArgMatches, @@ -13480,6 +13573,8 @@ pub enum CliCommand { NetworkingAddressLotCreate, NetworkingAddressLotDelete, NetworkingAddressLotBlockList, + NetworkingAllowListView, + NetworkingAllowListUpdate, NetworkingBfdDisable, NetworkingBfdEnable, NetworkingBfdStatus, @@ -13669,6 +13764,8 @@ impl CliCommand { CliCommand::NetworkingAddressLotCreate, CliCommand::NetworkingAddressLotDelete, CliCommand::NetworkingAddressLotBlockList, + CliCommand::NetworkingAllowListView, + CliCommand::NetworkingAllowListUpdate, CliCommand::NetworkingBfdDisable, CliCommand::NetworkingBfdEnable, CliCommand::NetworkingBfdStatus, diff --git a/cli/src/main.rs b/cli/src/main.rs index 34bc00d4..b1a471ff 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -14,7 +14,7 @@ use async_trait::async_trait; use cli_builder::NewCli; use generated_cli::CliConfig; use oxide::context::Context; -use oxide::types::{IdpMetadataSource, IpRange, Ipv4Range, Ipv6Range}; +use oxide::types::{AllowedSourceIps, IdpMetadataSource, IpRange, Ipv4Range, Ipv6Range}; mod cli_builder; mod cmd_api; @@ -215,6 +215,36 @@ impl CliConfig for OxideOverride { } Ok(()) } + + fn execute_networking_allow_list_update( + &self, + matches: &clap::ArgMatches, + request: &mut oxide::builder::NetworkingAllowListUpdate, + ) -> anyhow::Result<()> { + match matches + .get_one::("allow-list") + .map(clap::Id::as_str) + { + Some("any") => { + let value = matches.get_one::("any").unwrap(); + assert!(value); + *request = request + .to_owned() + .body_map(|body| body.allowed_ips(AllowedSourceIps::Any)); + } + Some("ips") => { + let values: Vec = matches.get_many("ips").unwrap().cloned().collect(); + *request = request.to_owned().body_map(|body| { + body.allowed_ips(AllowedSourceIps::List( + values.into_iter().map(IpOrNet::into_ip_net).collect(), + )) + }); + } + _ => unreachable!("invalid value for allow-list group"), + } + + Ok(()) + } } #[cfg(test)] @@ -313,3 +343,52 @@ mod tests { assert_contents("tests/data/json-body-required.txt", &out); } } + +#[derive(Debug, Clone)] +pub(crate) enum IpOrNet { + Ip(std::net::IpAddr), + Net(oxide::types::IpNet), +} + +#[derive(Clone, Debug)] +pub(crate) struct IpOrNetParser; +impl clap::builder::TypedValueParser for IpOrNetParser { + type Value = IpOrNet; + + fn parse_ref( + &self, + cmd: &clap::Command, + arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> std::prelude::v1::Result { + fn parse(value: &str) -> Result { + if let Ok(ip) = value.parse() { + Ok(IpOrNet::Ip(ip)) + } else if let Ok(net) = value.parse() { + Ok(IpOrNet::Net(net)) + } else { + Err("value must be an IP address or subnet".to_string()) + } + } + + parse.parse_ref(cmd, arg, value) + } +} + +impl clap::builder::ValueParserFactory for IpOrNet { + type Parser = IpOrNetParser; + + fn value_parser() -> Self::Parser { + IpOrNetParser + } +} + +impl IpOrNet { + pub fn into_ip_net(self) -> oxide::types::IpNet { + match self { + IpOrNet::Ip(std::net::IpAddr::V4(v4)) => format!("{}/32", v4).parse().unwrap(), + IpOrNet::Ip(std::net::IpAddr::V6(v6)) => format!("{}/128", v6).parse().unwrap(), + IpOrNet::Net(net) => net, + } + } +} diff --git a/cli/tests/data/test_system_networking_allowlist_any.stdout b/cli/tests/data/test_system_networking_allowlist_any.stdout new file mode 100644 index 00000000..808ca05a --- /dev/null +++ b/cli/tests/data/test_system_networking_allowlist_any.stdout @@ -0,0 +1,7 @@ +{ + "allowed_ips": { + "allow": "any" + }, + "time_created": "2018-11-14T19:07:30Z", + "time_modified": "2018-11-14T19:07:30Z" +} diff --git a/cli/tests/data/test_system_networking_allowlist_both.stderr b/cli/tests/data/test_system_networking_allowlist_both.stderr new file mode 100644 index 00000000..64c5c2d4 --- /dev/null +++ b/cli/tests/data/test_system_networking_allowlist_both.stderr @@ -0,0 +1,5 @@ +error: the argument '--any' cannot be used with '--ip ' + +Usage: oxide system networking allow-list update <--ip |--any> + +For more information, try '--help'. diff --git a/cli/tests/data/test_system_networking_allowlist_many_ips.stdout b/cli/tests/data/test_system_networking_allowlist_many_ips.stdout new file mode 100644 index 00000000..7a79c515 --- /dev/null +++ b/cli/tests/data/test_system_networking_allowlist_many_ips.stdout @@ -0,0 +1,14 @@ +{ + "allowed_ips": { + "allow": "list", + "ips": [ + "1.2.3.4/5", + "5.6.7.8/9", + "1.0.0.1/32", + "::1/127", + "::1/128" + ] + }, + "time_created": "2018-11-14T19:07:30Z", + "time_modified": "2018-11-14T19:07:30Z" +} diff --git a/cli/tests/data/test_system_networking_allowlist_neither.stderr b/cli/tests/data/test_system_networking_allowlist_neither.stderr new file mode 100644 index 00000000..348c9199 --- /dev/null +++ b/cli/tests/data/test_system_networking_allowlist_neither.stderr @@ -0,0 +1,6 @@ +error: the following required arguments were not provided: + <--ip |--any> + +Usage: oxide system networking allow-list update <--ip |--any> + +For more information, try '--help'. diff --git a/cli/tests/data/test_system_networking_allowlist_one_ip.stdout b/cli/tests/data/test_system_networking_allowlist_one_ip.stdout new file mode 100644 index 00000000..982b51a9 --- /dev/null +++ b/cli/tests/data/test_system_networking_allowlist_one_ip.stdout @@ -0,0 +1,10 @@ +{ + "allowed_ips": { + "allow": "list", + "ips": [ + "1.2.3.4/5" + ] + }, + "time_created": "2018-11-14T19:07:30Z", + "time_modified": "2018-11-14T19:07:30Z" +} diff --git a/cli/tests/test_system_networking_allowlist.rs b/cli/tests/test_system_networking_allowlist.rs new file mode 100644 index 00000000..e1892b85 --- /dev/null +++ b/cli/tests/test_system_networking_allowlist.rs @@ -0,0 +1,183 @@ +// 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/. + +// Copyright 2024 Oxide Computer Company + +use assert_cmd::Command; +use chrono::{TimeZone, Utc}; +use httpmock::MockServer; +use oxide::types::{AllowList, AllowListUpdate, AllowedSourceIps}; +use oxide_httpmock::MockServerExt; + +// Check that we have at least one of --any or --ip +#[test] +fn test_allowlist_neither() { + #[cfg(not(target_os = "windows"))] + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", "") + .env("OXIDE_TOKEN", "fake-token") + .arg("system") + .arg("networking") + .arg("allow-list") + .arg("update") + .assert() + .failure() + .stderr(expectorate::eq_file_or_panic( + "tests/data/test_system_networking_allowlist_neither.stderr", + )); +} + +// Check that we don't have both --any *and* --ip +#[test] +fn test_allowlist_both() { + #[cfg(not(target_os = "windows"))] + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", "") + .env("OXIDE_TOKEN", "fake-token") + .arg("system") + .arg("networking") + .arg("allow-list") + .arg("update") + .arg("--any") + .arg("--ip") + .arg("1.2.3.4/5") + .assert() + .failure() + .stderr(expectorate::eq_file_or_panic( + "tests/data/test_system_networking_allowlist_both.stderr", + )); +} + +// Just --any +#[test] +fn test_allowlist_any() { + let server = MockServer::start(); + let tt = Utc.timestamp_opt(1542222450, 0).unwrap(); + + let mock = server.networking_allow_list_update(|when, then| { + when.body(&AllowListUpdate { + allowed_ips: AllowedSourceIps::Any, + }); + then.ok(&AllowList { + allowed_ips: AllowedSourceIps::Any, + time_created: tt, + time_modified: tt, + }); + }); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("system") + .arg("networking") + .arg("allow-list") + .arg("update") + .arg("--any") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_system_networking_allowlist_any.stdout", + )); + + mock.assert(); +} + +// One ip +#[test] +fn test_allowlist_one_ip() { + let server = MockServer::start(); + let tt = Utc.timestamp_opt(1542222450, 0).unwrap(); + + let mock = server.networking_allow_list_update(|when, then| { + when.body(&AllowListUpdate { + allowed_ips: AllowedSourceIps::List(vec!["1.2.3.4/5".try_into().unwrap()]), + }); + then.ok(&AllowList { + allowed_ips: AllowedSourceIps::List(vec!["1.2.3.4/5".try_into().unwrap()]), + time_created: tt, + time_modified: tt, + }); + }); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("system") + .arg("networking") + .arg("allow-list") + .arg("update") + .arg("--ip") + .arg("1.2.3.4/5") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_system_networking_allowlist_one_ip.stdout", + )); + + mock.assert(); +} + +#[test] +fn test_allowlist_many_ips() { + let server = MockServer::start(); + let tt = Utc.timestamp_opt(1542222450, 0).unwrap(); + + let mock = server.networking_allow_list_update(|when, then| { + when.body(&AllowListUpdate { + allowed_ips: AllowedSourceIps::List(vec![ + "1.2.3.4/5".try_into().unwrap(), + "5.6.7.8/9".try_into().unwrap(), + "1.0.0.1/32".try_into().unwrap(), + "::1/127".try_into().unwrap(), + "::1/128".try_into().unwrap(), + ]), + }); + then.ok(&AllowList { + allowed_ips: AllowedSourceIps::List(vec![ + "1.2.3.4/5".try_into().unwrap(), + "5.6.7.8/9".try_into().unwrap(), + "1.0.0.1/32".try_into().unwrap(), + "::1/127".try_into().unwrap(), + "::1/128".try_into().unwrap(), + ]), + time_created: tt, + time_modified: tt, + }); + }); + + Command::cargo_bin("oxide") + .unwrap() + .env("RUST_BACKTRACE", "1") + .env("OXIDE_HOST", server.url("")) + .env("OXIDE_TOKEN", "fake-token") + .arg("system") + .arg("networking") + .arg("allow-list") + .arg("update") + .arg("--ip") + .arg("1.2.3.4/5") + .arg("--ip") + .arg("5.6.7.8/9") + .arg("--ip") + .arg("1.0.0.1") + .arg("--ip") + .arg("::1/127") + .arg("--ip") + .arg("::1") + .assert() + .success() + .stdout(expectorate::eq_file_or_panic( + "tests/data/test_system_networking_allowlist_many_ips.stdout", + )); + + mock.assert(); +} diff --git a/oxide.json b/oxide.json index a1254037..74efd47a 100644 --- a/oxide.json +++ b/oxide.json @@ -6305,6 +6305,68 @@ } } }, + "/v1/system/networking/allow-list": { + "get": { + "tags": [ + "system/networking" + ], + "summary": "Get user-facing services IP allowlist", + "operationId": "networking_allow_list_view", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowList" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + }, + "put": { + "tags": [ + "system/networking" + ], + "summary": "Update user-facing services IP allowlist", + "operationId": "networking_allow_list_update", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowListUpdate" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AllowList" + } + } + } + }, + "4XX": { + "$ref": "#/components/responses/Error" + }, + "5XX": { + "$ref": "#/components/responses/Error" + } + } + } + }, "/v1/system/networking/bfd-disable": { "post": { "tags": [ @@ -9187,6 +9249,94 @@ "switch_histories" ] }, + "AllowList": { + "description": "Allowlist of IPs or subnets that can make requests to user-facing services.", + "type": "object", + "properties": { + "allowed_ips": { + "description": "The allowlist of IPs or subnets.", + "allOf": [ + { + "$ref": "#/components/schemas/AllowedSourceIps" + } + ] + }, + "time_created": { + "description": "Time the list was created.", + "type": "string", + "format": "date-time" + }, + "time_modified": { + "description": "Time the list was last modified.", + "type": "string", + "format": "date-time" + } + }, + "required": [ + "allowed_ips", + "time_created", + "time_modified" + ] + }, + "AllowListUpdate": { + "description": "Parameters for updating allowed source IPs", + "type": "object", + "properties": { + "allowed_ips": { + "description": "The new list of allowed source IPs.", + "allOf": [ + { + "$ref": "#/components/schemas/AllowedSourceIps" + } + ] + } + }, + "required": [ + "allowed_ips" + ] + }, + "AllowedSourceIps": { + "description": "Description of source IPs allowed to reach rack services.", + "oneOf": [ + { + "description": "Allow traffic from any external IP address.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "any" + ] + } + }, + "required": [ + "allow" + ] + }, + { + "description": "Restrict access to a specific set of source IP addresses or subnets.\n\nAll others are prevented from reaching rack services.", + "type": "object", + "properties": { + "allow": { + "type": "string", + "enum": [ + "list" + ] + }, + "ips": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IpNet" + } + } + }, + "required": [ + "allow", + "ips" + ] + } + ] + }, "Baseboard": { "description": "Properties that uniquely identify an Oxide hardware component", "type": "object", @@ -14490,7 +14640,7 @@ "title": "An IPv6 subnet", "description": "An IPv6 subnet, including prefix and subnet mask", "type": "string", - "pattern": "^([fF][dD])[0-9a-fA-F]{2}:(([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,6}:)([0-9a-fA-F]{1,4})?\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" + "pattern": "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" }, "Ipv6Range": { "description": "A non-decreasing IPv6 address range, inclusive of both ends.\n\nThe first address must be less than or equal to the last address.", diff --git a/sdk-httpmock/src/generated_httpmock.rs b/sdk-httpmock/src/generated_httpmock.rs index ed05a97f..5f0e243f 100644 --- a/sdk-httpmock/src/generated_httpmock.rs +++ b/sdk-httpmock/src/generated_httpmock.rs @@ -10179,6 +10179,120 @@ pub mod operations { } } + pub struct NetworkingAllowListViewWhen(httpmock::When); + impl NetworkingAllowListViewWhen { + pub fn new(inner: httpmock::When) -> Self { + Self( + inner + .method(httpmock::Method::GET) + .path_matches(regex::Regex::new("^/v1/system/networking/allow-list$").unwrap()), + ) + } + + pub fn into_inner(self) -> httpmock::When { + self.0 + } + } + + pub struct NetworkingAllowListViewThen(httpmock::Then); + impl NetworkingAllowListViewThen { + pub fn new(inner: httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> httpmock::Then { + self.0 + } + + pub fn ok(self, value: &types::AllowList) -> Self { + Self( + self.0 + .status(200u16) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + + pub fn client_error(self, status: u16, value: &types::Error) -> Self { + assert_eq!(status / 100u16, 4u16); + Self( + self.0 + .status(status) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + + pub fn server_error(self, status: u16, value: &types::Error) -> Self { + assert_eq!(status / 100u16, 5u16); + Self( + self.0 + .status(status) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + } + + pub struct NetworkingAllowListUpdateWhen(httpmock::When); + impl NetworkingAllowListUpdateWhen { + pub fn new(inner: httpmock::When) -> Self { + Self( + inner + .method(httpmock::Method::PUT) + .path_matches(regex::Regex::new("^/v1/system/networking/allow-list$").unwrap()), + ) + } + + pub fn into_inner(self) -> httpmock::When { + self.0 + } + + pub fn body(self, value: &types::AllowListUpdate) -> Self { + Self(self.0.json_body_obj(value)) + } + } + + pub struct NetworkingAllowListUpdateThen(httpmock::Then); + impl NetworkingAllowListUpdateThen { + pub fn new(inner: httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> httpmock::Then { + self.0 + } + + pub fn ok(self, value: &types::AllowList) -> Self { + Self( + self.0 + .status(200u16) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + + pub fn client_error(self, status: u16, value: &types::Error) -> Self { + assert_eq!(status / 100u16, 4u16); + Self( + self.0 + .status(status) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + + pub fn server_error(self, status: u16, value: &types::Error) -> Self { + assert_eq!(status / 100u16, 5u16); + Self( + self.0 + .status(status) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + } + pub struct NetworkingBfdDisableWhen(httpmock::When); impl NetworkingBfdDisableWhen { pub fn new(inner: httpmock::When) -> Self { @@ -14958,6 +15072,15 @@ pub trait MockServerExt { operations::NetworkingAddressLotBlockListWhen, operations::NetworkingAddressLotBlockListThen, ); + fn networking_allow_list_view(&self, config_fn: F) -> httpmock::Mock + where + F: FnOnce(operations::NetworkingAllowListViewWhen, operations::NetworkingAllowListViewThen); + fn networking_allow_list_update(&self, config_fn: F) -> httpmock::Mock + where + F: FnOnce( + operations::NetworkingAllowListUpdateWhen, + operations::NetworkingAllowListUpdateThen, + ); fn networking_bfd_disable(&self, config_fn: F) -> httpmock::Mock where F: FnOnce(operations::NetworkingBfdDisableWhen, operations::NetworkingBfdDisableThen); @@ -16776,6 +16899,33 @@ impl MockServerExt for httpmock::MockServer { }) } + fn networking_allow_list_view(&self, config_fn: F) -> httpmock::Mock + where + F: FnOnce(operations::NetworkingAllowListViewWhen, operations::NetworkingAllowListViewThen), + { + self.mock(|when, then| { + config_fn( + operations::NetworkingAllowListViewWhen::new(when), + operations::NetworkingAllowListViewThen::new(then), + ) + }) + } + + fn networking_allow_list_update(&self, config_fn: F) -> httpmock::Mock + where + F: FnOnce( + operations::NetworkingAllowListUpdateWhen, + operations::NetworkingAllowListUpdateThen, + ), + { + self.mock(|when, then| { + config_fn( + operations::NetworkingAllowListUpdateWhen::new(when), + operations::NetworkingAllowListUpdateThen::new(then), + ) + }) + } + fn networking_bfd_disable(&self, config_fn: F) -> httpmock::Mock where F: FnOnce(operations::NetworkingBfdDisableWhen, operations::NetworkingBfdDisableThen), diff --git a/sdk/src/generated_sdk.rs b/sdk/src/generated_sdk.rs index a4ced4eb..825179ce 100644 --- a/sdk/src/generated_sdk.rs +++ b/sdk/src/generated_sdk.rs @@ -690,6 +690,184 @@ pub mod types { } } + /// Allowlist of IPs or subnets that can make requests to user-facing + /// services. + /// + ///
JSON schema + /// + /// ```json + /// { + /// "description": "Allowlist of IPs or subnets that can make requests to + /// user-facing services.", + /// "type": "object", + /// "required": [ + /// "allowed_ips", + /// "time_created", + /// "time_modified" + /// ], + /// "properties": { + /// "allowed_ips": { + /// "description": "The allowlist of IPs or subnets.", + /// "allOf": [ + /// { + /// "$ref": "#/components/schemas/AllowedSourceIps" + /// } + /// ] + /// }, + /// "time_created": { + /// "description": "Time the list was created.", + /// "type": "string", + /// "format": "date-time" + /// }, + /// "time_modified": { + /// "description": "Time the list was last modified.", + /// "type": "string", + /// "format": "date-time" + /// } + /// } + /// } + /// ``` + ///
+ #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] + pub struct AllowList { + /// The allowlist of IPs or subnets. + pub allowed_ips: AllowedSourceIps, + /// Time the list was created. + pub time_created: chrono::DateTime, + /// Time the list was last modified. + pub time_modified: chrono::DateTime, + } + + impl From<&AllowList> for AllowList { + fn from(value: &AllowList) -> Self { + value.clone() + } + } + + impl AllowList { + pub fn builder() -> builder::AllowList { + Default::default() + } + } + + /// Parameters for updating allowed source IPs + /// + ///
JSON schema + /// + /// ```json + /// { + /// "description": "Parameters for updating allowed source IPs", + /// "type": "object", + /// "required": [ + /// "allowed_ips" + /// ], + /// "properties": { + /// "allowed_ips": { + /// "description": "The new list of allowed source IPs.", + /// "allOf": [ + /// { + /// "$ref": "#/components/schemas/AllowedSourceIps" + /// } + /// ] + /// } + /// } + /// } + /// ``` + ///
+ #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] + pub struct AllowListUpdate { + /// The new list of allowed source IPs. + pub allowed_ips: AllowedSourceIps, + } + + impl From<&AllowListUpdate> for AllowListUpdate { + fn from(value: &AllowListUpdate) -> Self { + value.clone() + } + } + + impl AllowListUpdate { + pub fn builder() -> builder::AllowListUpdate { + Default::default() + } + } + + /// Description of source IPs allowed to reach rack services. + /// + ///
JSON schema + /// + /// ```json + /// { + /// "description": "Description of source IPs allowed to reach rack + /// services.", + /// "oneOf": [ + /// { + /// "description": "Allow traffic from any external IP address.", + /// "type": "object", + /// "required": [ + /// "allow" + /// ], + /// "properties": { + /// "allow": { + /// "type": "string", + /// "enum": [ + /// "any" + /// ] + /// } + /// } + /// }, + /// { + /// "description": "Restrict access to a specific set of source IP + /// addresses or subnets.\n\nAll others are prevented from reaching rack + /// services.", + /// "type": "object", + /// "required": [ + /// "allow", + /// "ips" + /// ], + /// "properties": { + /// "allow": { + /// "type": "string", + /// "enum": [ + /// "list" + /// ] + /// }, + /// "ips": { + /// "type": "array", + /// "items": { + /// "$ref": "#/components/schemas/IpNet" + /// } + /// } + /// } + /// } + /// ] + /// } + /// ``` + ///
+ #[derive(Clone, Debug, Deserialize, Serialize, schemars :: JsonSchema)] + #[serde(tag = "allow", content = "ips")] + pub enum AllowedSourceIps { + #[serde(rename = "any")] + Any, + /// Restrict access to a specific set of source IP addresses or subnets. + /// + /// All others are prevented from reaching rack services. + #[serde(rename = "list")] + List(Vec), + } + + impl From<&AllowedSourceIps> for AllowedSourceIps { + fn from(value: &AllowedSourceIps) -> Self { + value.clone() + } + } + + impl From> for AllowedSourceIps { + fn from(value: Vec) -> Self { + Self::List(value) + } + } + /// Properties that uniquely identify an Oxide hardware component /// ///
JSON schema @@ -12140,8 +12318,16 @@ pub mod types { /// ], /// "type": "string", /// "pattern": - /// "^([fF][dD])[0-9a-fA-F]{2}:(([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1, - /// 4}|([0-9a-fA-F]{1,4}:){1,6}:)([0-9a-fA-F]{1,4})?\\/ + /// "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}: + /// |([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(: + /// [0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1, + /// 3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}: + /// ){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1, + /// 6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}% + /// [0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0, + /// 1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0, + /// 1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0, + /// 1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/ /// ([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$" /// } /// ``` @@ -12173,16 +12359,33 @@ pub mod types { type Err = self::error::ConversionError; fn from_str(value: &str) -> Result { if regress::Regex::new( - "^([fF][dD])[0-9a-fA-F]{2}:(([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,\ - 4}:){1,6}:)([0-9a-fA-F]{1,4})?\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$", + "^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:\ + |([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:\ + [0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,\ + 3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:\ + [0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:\ + [0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:\ + 0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,\ + 3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:\ + ((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,\ + 1}[0-9]){0,1}[0-9]))\\/([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$", ) .unwrap() .find(value) .is_none() { return Err("doesn't match pattern \ - \"^([fF][dD])[0-9a-fA-F]{2}:(([0-9a-fA-F]{1,4}:){6}[0-9a-fA-F]{1,\ - 4}|([0-9a-fA-F]{1,4}:){1,6}:)([0-9a-fA-F]{1,4})?\\/\ + \"^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,\ + 7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,\ + 5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,\ + 4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,\ + 4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,\ + 4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:\ + [0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,\ + 1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,\ + 3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:\ + ((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,\ + 3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))\\/\ ([0-9]|[1-9][0-9]|1[0-1][0-9]|12[0-8])$\"" .into()); } @@ -24330,6 +24533,120 @@ pub mod types { } } + #[derive(Clone, Debug)] + pub struct AllowList { + allowed_ips: Result, + time_created: Result, String>, + time_modified: Result, String>, + } + + impl Default for AllowList { + fn default() -> Self { + Self { + allowed_ips: Err("no value supplied for allowed_ips".to_string()), + time_created: Err("no value supplied for time_created".to_string()), + time_modified: Err("no value supplied for time_modified".to_string()), + } + } + } + + impl AllowList { + pub fn allowed_ips(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.allowed_ips = value + .try_into() + .map_err(|e| format!("error converting supplied value for allowed_ips: {}", e)); + self + } + pub fn time_created(mut self, value: T) -> Self + where + T: std::convert::TryInto>, + T::Error: std::fmt::Display, + { + self.time_created = value.try_into().map_err(|e| { + format!("error converting supplied value for time_created: {}", e) + }); + self + } + pub fn time_modified(mut self, value: T) -> Self + where + T: std::convert::TryInto>, + T::Error: std::fmt::Display, + { + self.time_modified = value.try_into().map_err(|e| { + format!("error converting supplied value for time_modified: {}", e) + }); + self + } + } + + impl std::convert::TryFrom for super::AllowList { + type Error = super::error::ConversionError; + fn try_from(value: AllowList) -> Result { + Ok(Self { + allowed_ips: value.allowed_ips?, + time_created: value.time_created?, + time_modified: value.time_modified?, + }) + } + } + + impl From for AllowList { + fn from(value: super::AllowList) -> Self { + Self { + allowed_ips: Ok(value.allowed_ips), + time_created: Ok(value.time_created), + time_modified: Ok(value.time_modified), + } + } + } + + #[derive(Clone, Debug)] + pub struct AllowListUpdate { + allowed_ips: Result, + } + + impl Default for AllowListUpdate { + fn default() -> Self { + Self { + allowed_ips: Err("no value supplied for allowed_ips".to_string()), + } + } + } + + impl AllowListUpdate { + pub fn allowed_ips(mut self, value: T) -> Self + where + T: std::convert::TryInto, + T::Error: std::fmt::Display, + { + self.allowed_ips = value + .try_into() + .map_err(|e| format!("error converting supplied value for allowed_ips: {}", e)); + self + } + } + + impl std::convert::TryFrom for super::AllowListUpdate { + type Error = super::error::ConversionError; + fn try_from(value: AllowListUpdate) -> Result { + Ok(Self { + allowed_ips: value.allowed_ips?, + }) + } + } + + impl From for AllowListUpdate { + fn from(value: super::AllowListUpdate) -> Self { + Self { + allowed_ips: Ok(value.allowed_ips), + } + } + } + #[derive(Clone, Debug)] pub struct Baseboard { part: Result, @@ -43521,6 +43838,27 @@ pub trait ClientSystemNetworkingExt { /// .await; /// ``` fn networking_address_lot_block_list(&self) -> builder::NetworkingAddressLotBlockList; + /// Get user-facing services IP allowlist + /// + /// Sends a `GET` request to `/v1/system/networking/allow-list` + /// + /// ```ignore + /// let response = client.networking_allow_list_view() + /// .send() + /// .await; + /// ``` + fn networking_allow_list_view(&self) -> builder::NetworkingAllowListView; + /// Update user-facing services IP allowlist + /// + /// Sends a `PUT` request to `/v1/system/networking/allow-list` + /// + /// ```ignore + /// let response = client.networking_allow_list_update() + /// .body(body) + /// .send() + /// .await; + /// ``` + fn networking_allow_list_update(&self) -> builder::NetworkingAllowListUpdate; /// Disable a BFD session /// /// Sends a `POST` request to `/v1/system/networking/bfd-disable` @@ -43873,6 +44211,14 @@ impl ClientSystemNetworkingExt for Client { builder::NetworkingAddressLotBlockList::new(self) } + fn networking_allow_list_view(&self) -> builder::NetworkingAllowListView { + builder::NetworkingAllowListView::new(self) + } + + fn networking_allow_list_update(&self) -> builder::NetworkingAllowListUpdate { + builder::NetworkingAllowListUpdate::new(self) + } + fn networking_bfd_disable(&self) -> builder::NetworkingBfdDisable { builder::NetworkingBfdDisable::new(self) } @@ -57913,6 +58259,116 @@ pub mod builder { } } + /// Builder for [`ClientSystemNetworkingExt::networking_allow_list_view`] + /// + /// [`ClientSystemNetworkingExt::networking_allow_list_view`]: super::ClientSystemNetworkingExt::networking_allow_list_view + #[derive(Debug, Clone)] + pub struct NetworkingAllowListView<'a> { + client: &'a super::Client, + } + + impl<'a> NetworkingAllowListView<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { client: client } + } + + /// Sends a `GET` request to `/v1/system/networking/allow-list` + pub async fn send(self) -> Result, Error> { + let Self { client } = self; + let url = format!("{}/v1/system/networking/allow-list", client.baseurl,); + #[allow(unused_mut)] + let mut request = client + .client + .get(url) + .header( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => ResponseValue::from_response(response).await, + 400u16..=499u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + 500u16..=599u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + /// Builder for [`ClientSystemNetworkingExt::networking_allow_list_update`] + /// + /// [`ClientSystemNetworkingExt::networking_allow_list_update`]: super::ClientSystemNetworkingExt::networking_allow_list_update + #[derive(Debug, Clone)] + pub struct NetworkingAllowListUpdate<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> NetworkingAllowListUpdate<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + body: Ok(types::builder::AllowListUpdate::default()), + } + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + >::Error: std::fmt::Display, + { + self.body = value + .try_into() + .map(From::from) + .map_err(|s| format!("conversion to `AllowListUpdate` for body failed: {}", s)); + self + } + + pub fn body_map(mut self, f: F) -> Self + where + F: std::ops::FnOnce(types::builder::AllowListUpdate) -> types::builder::AllowListUpdate, + { + self.body = self.body.map(f); + self + } + + /// Sends a `PUT` request to `/v1/system/networking/allow-list` + pub async fn send(self) -> Result, Error> { + let Self { client, body } = self; + let body = body + .and_then(|v| types::AllowListUpdate::try_from(v).map_err(|e| e.to_string())) + .map_err(Error::InvalidRequest)?; + let url = format!("{}/v1/system/networking/allow-list", client.baseurl,); + #[allow(unused_mut)] + let mut request = client + .client + .put(url) + .header( + reqwest::header::ACCEPT, + reqwest::header::HeaderValue::from_static("application/json"), + ) + .json(&body) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => ResponseValue::from_response(response).await, + 400u16..=499u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + 500u16..=599u16 => Err(Error::ErrorResponse( + ResponseValue::from_response(response).await?, + )), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + /// Builder for [`ClientSystemNetworkingExt::networking_bfd_disable`] /// /// [`ClientSystemNetworkingExt::networking_bfd_disable`]: super::ClientSystemNetworkingExt::networking_bfd_disable