Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Validate ByteCount ranges during deserialization #5743

Merged
merged 1 commit into from
May 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -527,12 +527,21 @@ impl JsonSchema for RoleName {
// in the database as an i64. Constraining it here ensures that we can't fail
// to serialize the value.
//
// TODO: custom JsonSchema and Deserialize impls to enforce i64::MAX limit
#[derive(
Copy, Clone, Debug, Deserialize, Serialize, JsonSchema, PartialEq, Eq,
)]
// TODO: custom JsonSchema impl to describe i64::MAX limit; this is blocked by
// https://github.com/oxidecomputer/typify/issues/589
Comment on lines +530 to +531
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

huh, that's surprising! This is still worth fixing, but to be clear, we'd rely on deserialize either way, so the behavior should be the "same" to clients afterwards, right? Would the only difference be "a more informative schema file"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this PR fixes the server-side problem. The custom JSON schema would be so (e.g.) the CLI could validate the value even before sending the request.

#[derive(Copy, Clone, Debug, Serialize, JsonSchema, PartialEq, Eq)]
pub struct ByteCount(u64);

impl<'de> Deserialize<'de> for ByteCount {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let bytes = u64::deserialize(deserializer)?;
ByteCount::try_from(bytes).map_err(serde::de::Error::custom)
}
}

#[allow(non_upper_case_globals)]
const KiB: u64 = 1024;
#[allow(non_upper_case_globals)]
Expand Down
67 changes: 67 additions & 0 deletions nexus/tests/integration_tests/quotas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use nexus_types::external_api::views::{Silo, SiloQuotas};
use omicron_common::api::external::ByteCount;
use omicron_common::api::external::IdentityMetadataCreateParams;
use omicron_common::api::external::InstanceCpuCount;
use serde_json::json;

type ControlPlaneTestContext =
nexus_test_utils::ControlPlaneTestContext<omicron_nexus::Server>;
Expand Down Expand Up @@ -315,3 +316,69 @@ async fn test_quotas(cptestctx: &ControlPlaneTestContext) {
.await
.expect("Disk should be provisioned");
}

#[nexus_test]
async fn test_quota_limits(cptestctx: &ControlPlaneTestContext) {
let client = &cptestctx.external_client;

let system = setup_silo_with_quota(
&client,
"quota-test-silo",
params::SiloQuotasCreate::empty(),
)
.await;

// Maximal legal limits should be allowed.
let quota_limit = params::SiloQuotasUpdate {
cpus: Some(i64::MAX),
memory: Some(i64::MAX.try_into().unwrap()),
storage: Some(i64::MAX.try_into().unwrap()),
};
system
.set_quotas(client, quota_limit.clone())
.await
.expect("set max quotas");
let quotas = system.get_quotas(client).await;
assert_eq!(quotas.limits.cpus, quota_limit.cpus.unwrap());
assert_eq!(quotas.limits.memory, quota_limit.memory.unwrap());
assert_eq!(quotas.limits.storage, quota_limit.storage.unwrap());

// Construct a value that fits in a u64 but not an i64.
let out_of_bounds = u64::try_from(i64::MAX).unwrap() + 1;

for key in ["cpus", "memory", "storage"] {
// We can't construct a `SiloQuotasUpdate` with higher-than-maximal
// values, but we can construct the equivalent JSON blob of such a
// request.
let request = json!({ key: out_of_bounds });

let err = NexusRequest::expect_failure_with_body(
client,
http::StatusCode::BAD_REQUEST,
http::Method::PUT,
"/v1/system/silos/quota-test-silo/quotas",
&request,
)
.authn_as(system.auth.clone())
.execute()
.await
.expect("sent quota update")
.parsed_body::<HttpErrorResponseBody>()
.expect("parsed error body");
assert!(
err.message.contains(key)
&& (err.message.contains("invalid value")
|| err
.message
.contains("value is too large for a byte count")),
"Unexpected error: {0}",
err.message
);

// The quota limits we set above should be unchanged.
let quotas = system.get_quotas(client).await;
assert_eq!(quotas.limits.cpus, quota_limit.cpus.unwrap());
assert_eq!(quotas.limits.memory, quota_limit.memory.unwrap());
assert_eq!(quotas.limits.storage, quota_limit.storage.unwrap());
}
}
Loading