Skip to content
Open
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
37 changes: 37 additions & 0 deletions crates/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,43 @@ impl fmt::Display for ZoneAddError {
}
}

//----------- ZoneEdit -------------------------------------------------------

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ZoneEdit {
pub source: Option<ZoneSource>,
pub policy: Option<String>,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ZoneEditResult {
pub name: ZoneName,
pub status: String,
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub enum ZoneEditError {
NoSuchZone,
NoSuchPolicy,
NoSuchTsigKey,
PolicyMidDeletion,
Other(String),
}

impl fmt::Display for ZoneEditError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::NoSuchZone => "no zone with that name exists",
Self::NoSuchPolicy => "no policy with that name exists",
Self::NoSuchTsigKey => "no TSIG key with that name exists",
Self::PolicyMidDeletion => "the specified policy is being deleted",
Self::Other(reason) => reason,
})
}
}

//----------- ZoneRemove -----------------------------------------------------

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct ZoneRemoveResult {
pub name: ZoneName,
Expand Down
54 changes: 54 additions & 0 deletions crates/cli/src/commands/zone.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,23 @@ pub enum ZoneCommand {
import_csk_kmip: Vec<String>,
},

/// Edit the configuration for a zone
#[command(name = "edit")]
Edit {
name: ZoneName,

/// The source to obtain the zone content from:
/// `IP:[PORT][^TSIG_KEY_NAME]` (port defaults to 53) or the path to
/// a zone file locally available to the `cascaded` daemon.
// TODO: allow supplying different tcp and/or udp port?
#[arg(long = "source")]
source: Option<ZoneSource>,

/// Policy to use for this zone
#[arg(long = "policy")]
policy: Option<String>,
},

/// Remove a zone
#[command(name = "remove")]
Remove { name: ZoneName },
Expand Down Expand Up @@ -241,6 +258,43 @@ impl Zone {
Err(e) => Err(format!("Failed to add zone: {e}")),
}
}
ZoneCommand::Edit {
name,
mut source,
policy,
} => {
if policy.is_none() && source.is_none() {
return Err("nothing to do".into());
}

if let Some(ZoneSource::Zonefile { path }) = &mut source {
let canonicalized_path = path.canonicalize().map_err(|err| {
format!("Failed to canonicalize zonefile path '{}': {err}", path)
})?;
let path_str = canonicalized_path.to_str().ok_or_else(|| {
format!("Failed to convert path '{}'", canonicalized_path.display())
})?;
*path = Utf8PathBuf::from(path_str).into_boxed_path();
}

let res: Result<ZoneEditResult, ZoneEditError> = client
.post_json_with(
&format!("zone/{name}/edit"),
&ZoneEdit {
source: source.map(|s| s.try_into().unwrap()),
policy,
},
)
.await?;

match res {
Ok(res) => {
println!("Edited zone {}: {}", res.name, res.status);
Ok(())
}
Err(e) => Err(format!("Failed to edit zone: {e}")),
}
}
ZoneCommand::Remove { name } => {
let res: Result<ZoneRemoveResult, ZoneRemoveError> =
client.post_json(&format!("zone/{name}/remove")).await?;
Expand Down
14 changes: 13 additions & 1 deletion src/center.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ use tracing::{debug, error, info, trace};

use crate::api::{self, KeyImport, TsigAddError, TsigAddResult};
use crate::config::RuntimeConfig;
use crate::loader::Loader;
use crate::loader::zone::LoaderZoneHandle;
use crate::loader::{Loader, Source};
use crate::metrics::Metrics;
use crate::persistence::{Persister, Restorer};
use crate::server::{LoadedReviewServer, PublicationServer, SignedReviewServer};
Expand Down Expand Up @@ -241,6 +241,18 @@ pub async fn add_zone(
Ok(())
}

pub fn change_zone_source(center: &Arc<Center>, zone: &Arc<Zone>, source: Source) {
let mut state = zone.write(center);

// Set the source of the zone, and begin loading it.
LoaderZoneHandle {
zone,
state: &mut state,
center,
}
.set_source(source);
}

async fn register_zone(
center: &Arc<Center>,
name: Name<Bytes>,
Expand Down
147 changes: 147 additions & 0 deletions src/units/http_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ use crate::units::key_manager::KmipServerCredentialsFileMode;
use crate::units::key_manager::mk_dnst_keyset_cfg_file_path;
use crate::units::key_manager::mk_dnst_keyset_state_file_path;
use crate::units::zone_signer::KeySetState;
use crate::zone::ZoneByPtr;
use crate::zone::machine::ZoneStateMachine;
use crate::zone::{HistoricalEvent, HistoricalEventType, ZoneByName};

Expand Down Expand Up @@ -82,6 +83,7 @@ impl HttpServer {
.route("/tsig/{name}/remove", post(Self::tsig_key_remove))
.route("/zone/", get(Self::zones_list))
.route("/zone/add", post(Self::zone_add))
.route("/zone/{name}/edit", post(Self::zone_edit))
// TODO: .route("/zone/{name}/", get(Self::zone_get))
.route("/zone/{name}/remove", post(Self::zone_remove))
.route("/zone/{name}/reset", post(Self::zone_reset))
Expand Down Expand Up @@ -300,6 +302,151 @@ impl HttpServer {
}
}

async fn zone_edit(
State(state): State<Arc<HttpServer>>,
Path(name): Path<Name<Bytes>>,
Json(zone_edit): Json<ZoneEdit>,
) -> Json<Result<ZoneEditResult, ZoneEditError>> {
// Poor man's try block
let do_zone_edit = || {
let zone = center::get_zone(&state.center, &name).ok_or(ZoneEditError::NoSuchZone)?;

let center = &state.center;
let mut state = state.center.state.lock().unwrap();
let state = &mut *state;

// To ensure that all changes are made atomically, we do all the operations such as getting
// the zone, policy, tsig key etc. first before we make any changes to the actual zone.
//
// We can then make the required changes without worrying about rolling back anything.

let policy = zone_edit
.policy
.map(|policy| {
let pol = state
.policies
.get_mut(&policy.into_boxed_str())
.ok_or(ZoneEditError::NoSuchPolicy)?;

if pol.mid_deletion {
return Err(ZoneEditError::PolicyMidDeletion);
}

Ok(pol)
})
.transpose()?;

let source = zone_edit
.source
.map(|source| {
Ok(match source {
api::ZoneSource::None => crate::loader::Source::None,
api::ZoneSource::Zonefile { path } => {
crate::loader::Source::Zonefile { path }
}
api::ZoneSource::Server { addr, tsig_key } => {
let tsig_key = if let Some(key_name) = tsig_key {
// Lookup the key in the TSIG key store.
let key = state
.tsig_store
.get_mut(&key_name)
.ok_or(ZoneEditError::NoSuchTsigKey)?;

let key = key.inner.clone();

// Remember the found key.
Some(key)
} else {
None
};

crate::loader::Source::Server { addr, tsig_key }
}
})
})
.transpose()?;

// Now we start making actual changes.
// Anything below this point should be infallible.

let mut changes = Vec::new();

if let Some(policy) = policy {
let mut handle = zone.write_handle(center);

let old = handle
.state
.policy
.replace(policy.latest.clone())
.expect("a policy was in use");

let old_name = old.name.clone();

policy.zones.insert(zone.name.clone());

handle.signer().after_policy_change();

center.key_manager.on_zone_policy_changed(
center,
&zone,
Some(old),
policy.latest.clone(),
);

state
.policies
.get_mut(&old_name)
.expect("policy should exist")
.zones
.remove(&zone.name);

changes.push("policy");
}

if let Some(source) = source {
if let loader::Source::Server {
addr: _,
tsig_key: Some(tsig_key),
} = &source
{
// This seems to be the best way to do this currently.
let key = state
.tsig_store
.get_mut(tsig_key.name())
.expect("we did a lookup above");

// Record that this zone uses this key.
key.zones.insert(ZoneByPtr(zone.clone()));

state.tsig_store.mark_dirty(center);
}

center::change_zone_source(center, &zone, source);

changes.push("source")
}

let last_change = changes.pop();

if let Some(last_change) = last_change {
let changes = changes.join(", ");
let status = if !changes.is_empty() {
format!("updated {changes} and {last_change}")
} else {
format!("updated {last_change}")
};
Ok(ZoneEditResult { name, status })
} else {
Ok(ZoneEditResult {
name,
status: "nothing changed".into(),
})
}
};

Json(do_zone_edit())
}

async fn zone_remove(
State(state): State<Arc<HttpServer>>,
Path(name): Path<Name<Bytes>>,
Expand Down
Loading