Skip to content
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
64 changes: 64 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ use std::fmt::Result as FormatResult;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::num::{NonZeroU16, NonZeroU32};
use std::ops::Deref;
use std::str::FromStr;
use tufaceous_artifact::ArtifactHash;
use uuid::Uuid;
Expand Down Expand Up @@ -1008,6 +1009,7 @@ pub enum ResourceType {
ProjectImage,
Instance,
LoopbackAddress,
SiloAuthSettings,
SwitchPortSettings,
SupportBundle,
IpPool,
Expand Down Expand Up @@ -3277,6 +3279,68 @@ pub enum ImportExportPolicy {
Allow(Vec<oxnet::IpNet>),
}

/// Use instead of Option in API request body structs to get a field that can
/// be null (parsed as `None`) but is not optional. Unlike Option, Nullable
/// will fail to parse if the key is not present. The JSON Schema in the
/// OpenAPI definition will also reflect that the field is required. See
/// <https://github.com/serde-rs/serde/issues/2753>.
#[derive(Clone, Debug, Serialize)]
pub struct Nullable<T>(pub Option<T>);

impl<T> From<Option<T>> for Nullable<T> {
fn from(option: Option<T>) -> Self {
Nullable(option)
}
}

impl<T> Deref for Nullable<T> {
type Target = Option<T>;

fn deref(&self) -> &Self::Target {
&self.0
}
}

// it looks like we're just using Option's impl here, so why not derive instead?
// For some reason, deriving JsonSchema + #[serde(transparent)] doesn't work --
// it almost does, but the field does not end up marked required in the schema.
// There must be some special handling of Option somewhere causing it to be
// marked optional rather than nullable + required.

impl<T: JsonSchema> JsonSchema for Nullable<T> {
fn schema_name() -> String {
T::schema_name()
}

fn json_schema(
generator: &mut schemars::r#gen::SchemaGenerator,
) -> schemars::schema::Schema {
Option::<T>::json_schema(generator)
}

fn is_referenceable() -> bool {
Option::<T>::is_referenceable()
}
}

impl<'de, T: serde::Deserialize<'de>> serde::Deserialize<'de> for Nullable<T> {
fn deserialize<D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Self, D::Error> {
// This line is required to get a parse error on missing fields.
// It seems that when the field is missing in the JSON, struct
// deserialization produces an error before this function is even hit,
// and that error is passed in here inside `deserializer`. If we don't
// do this Value::deserialize to cause that error to be returned as a
// missing field error, Option's deserialize will eat it by turning it
// into a successful parse as None.
let value = serde_json::Value::deserialize(deserializer)?;

use serde::de::Error;
Option::<T>::deserialize(value).map_err(D::Error::custom).map(Nullable)
}
}

#[cfg(test)]
mod test {
use serde::Deserialize;
Expand Down
5 changes: 4 additions & 1 deletion nexus/db-model/src/device_auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,12 @@ impl DeviceAccessToken {
device_code: String,
time_requested: DateTime<Utc>,
silo_user_id: Uuid,
time_expires: Option<DateTime<Utc>>,
) -> Self {
let now = Utc::now();
assert!(time_requested <= now);
assert!(time_expires.map_or(true, |t| t > now));

Self {
id: TypedUuid::new_v4().into(),
token: generate_token(),
Expand All @@ -147,7 +150,7 @@ impl DeviceAccessToken {
silo_user_id,
time_requested,
time_created: now,
time_expires: None,
time_expires,
}
}

Expand Down
2 changes: 2 additions & 0 deletions nexus/db-model/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ mod project;
mod rendezvous_debug_dataset;
mod semver_version;
mod serde_time_delta;
mod silo_auth_settings;
mod switch_interface;
mod switch_port;
mod target_release;
Expand Down Expand Up @@ -214,6 +215,7 @@ pub use schema_versions::*;
pub use semver_version::*;
pub use service_kind::*;
pub use silo::*;
pub use silo_auth_settings::*;
pub use silo_group::*;
pub use silo_user::*;
pub use silo_user_password_hash::*;
Expand Down
3 changes: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: Version = Version::new(145, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(146, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(146, "silo-settings-token-expiration"),
KnownVersion::new(145, "token-and-session-ids"),
KnownVersion::new(144, "inventory-omicron-sled-config"),
KnownVersion::new(143, "alerts-renamening"),
Expand Down
70 changes: 70 additions & 0 deletions nexus/db-model/src/silo_auth_settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use crate::SqlU32;
use chrono::{DateTime, Utc};
use nexus_db_schema::schema::silo_auth_settings;
use nexus_types::external_api::{params, views};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

#[derive(
Queryable,
Insertable,
Debug,
Clone,
Selectable,
Serialize,
Deserialize,
AsChangeset,
)]
#[diesel(table_name = silo_auth_settings)]
pub struct SiloAuthSettings {
pub silo_id: Uuid,
pub time_created: DateTime<Utc>,
pub time_modified: DateTime<Utc>,

/// Max token lifetime in seconds. Null means no max: users can create
/// tokens that never expire.
pub device_token_max_ttl_seconds: Option<SqlU32>,
}

impl SiloAuthSettings {
pub fn new(silo_id: Uuid) -> Self {
Self {
silo_id,
time_created: Utc::now(),
time_modified: Utc::now(),
device_token_max_ttl_seconds: None,
}
}
}

impl From<SiloAuthSettings> for views::SiloAuthSettings {
fn from(silo_auth_settings: SiloAuthSettings) -> Self {
Self {
silo_id: silo_auth_settings.silo_id,
device_token_max_ttl_seconds: silo_auth_settings
.device_token_max_ttl_seconds
.map(|ttl| ttl.0),
}
}
}

// Describes a set of updates for the [`SiloAuthSettings`] model.
#[derive(AsChangeset)]
#[diesel(table_name = silo_auth_settings)]
pub struct SiloAuthSettingsUpdate {
// Needs to be double Option so we can set a value of null in the DB by
// passing Some(None). None by itself is ignored by Diesel.
pub device_token_max_ttl_seconds: Option<Option<i64>>,
pub time_modified: DateTime<Utc>,
}

impl From<params::SiloAuthSettingsUpdate> for SiloAuthSettingsUpdate {
fn from(params: params::SiloAuthSettingsUpdate) -> Self {
Self {
device_token_max_ttl_seconds: Some(
params.device_token_max_ttl_seconds.map(|ttl| ttl.get().into()),
),
time_modified: Utc::now(),
}
}
}
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ mod rendezvous_debug_dataset;
mod role;
mod saga;
mod silo;
mod silo_auth_settings;
mod silo_group;
mod silo_user;
mod sled;
Expand Down
28 changes: 22 additions & 6 deletions nexus/db-queries/src/db/datastore/silo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use nexus_db_fixed_data::silo::{DEFAULT_SILO, INTERNAL_SILO};
use nexus_db_lookup::DbConnection;
use nexus_db_model::Certificate;
use nexus_db_model::ServiceKind;
use nexus_db_model::SiloAuthSettings;
use nexus_db_model::SiloQuotas;
use nexus_types::external_api::params;
use nexus_types::external_api::shared;
Expand Down Expand Up @@ -64,24 +65,31 @@ impl DataStore {

debug!(opctx.log, "attempting to create built-in silos");

use nexus_db_schema::schema::silo::dsl;
use nexus_db_schema::schema::silo_quotas::dsl as quotas_dsl;
use nexus_db_schema::schema::silo;
use nexus_db_schema::schema::silo_auth_settings;
use nexus_db_schema::schema::silo_quotas;
let conn = self.pool_connection_authorized(opctx).await?;

let count = self
.transaction_retry_wrapper("load_builtin_silos")
.transaction(&conn, |conn| async move {
diesel::insert_into(quotas_dsl::silo_quotas)
diesel::insert_into(silo_quotas::table)
.values(SiloQuotas::arbitrarily_high_default(
DEFAULT_SILO.id(),
))
.on_conflict(quotas_dsl::silo_id)
.on_conflict(silo_quotas::silo_id)
.do_nothing()
.execute_async(&conn)
.await?;
diesel::insert_into(silo_auth_settings::table)
.values(SiloAuthSettings::new(DEFAULT_SILO.id()))
.on_conflict(silo_auth_settings::silo_id)
.do_nothing()
.execute_async(&conn)
.await?;
let count = diesel::insert_into(dsl::silo)
let count = diesel::insert_into(silo::table)
.values([&*DEFAULT_SILO, &*INTERNAL_SILO])
.on_conflict(dsl::id)
.on_conflict(silo::id)
.do_nothing()
.execute_async(&conn)
.await?;
Expand Down Expand Up @@ -300,6 +308,12 @@ impl DataStore {
),
)
.await?;
self.silo_auth_settings_create(
&conn,
&authz_silo,
SiloAuthSettings::new(authz_silo.id()),
)
.await?;

Ok::<Silo, TransactionError<Error>>(silo)
})
Expand Down Expand Up @@ -451,6 +465,8 @@ impl DataStore {
}

self.silo_quotas_delete(opctx, &conn, &authz_silo).await?;
self.silo_auth_settings_delete(opctx, &conn, &authz_silo)
.await?;

self.virtual_provisioning_collection_delete_on_connection(
&opctx.log, &conn, id,
Expand Down
Loading
Loading