add RFC8590 implementation

Implements the Change Poll Extension for the Extensible Provisioning Protocol.
Test cases are taken from the spec.
This commit is contained in:
Rudi Floren 2024-12-05 19:22:26 +01:00 committed by Dirkjan Ochtman
parent fac8c173c9
commit 2188df56c3
9 changed files with 633 additions and 1 deletions

View File

@ -0,0 +1,359 @@
//! Types for the EPP change poll extention
//!
//! As described in RFC8590: [Change Poll Extension for the Extensible Provisioning Protocol (EPP)](https://www.rfc-editor.org/rfc/rfc8590.html).
//! Tests cases in `tests/resources/response/extensions/changepoll`` are taken from the RFC.
use std::borrow::Cow;
use instant_xml::{Error, FromXml, ToXml};
use crate::{
poll::Poll,
request::{Extension, Transaction},
};
pub const XMLNS: &str = "urn:ietf:params:xml:ns:changePoll-1.0";
impl Transaction<ChangePoll<'_>> for Poll {}
impl Extension for ChangePoll<'_> {
type Response = ChangePoll<'static>;
}
/// Type for EPP XML `<changePoll>` extension
///
/// Attributes associated with the change
#[derive(Debug, FromXml, ToXml)]
#[xml(rename = "changeData", ns(XMLNS))]
pub struct ChangePoll<'a> {
/// Transform operation executed on the object
pub operation: Operation<'a>,
/// Date and time when the operation was executed
pub date: Cow<'a, str>,
/// Server transaction identifier of the operation
#[xml(rename = "svTRID")]
pub server_tr_id: Cow<'a, str>,
/// Who executed the operation
pub who: Cow<'a, str>,
/// Case identifier associated with the operation
pub case_id: Option<CaseIdentifier<'a>>,
/// Reason for executing the operation
pub reason: Option<Reason>,
/// Enumerated state of the object in the poll message
#[xml(attribute)]
// todo: State should utilize the Default impl,
// but instant-xml does not support it yet.
state: Option<State>,
}
impl ChangePoll<'_> {
/// State reflects if the `infData` describes the object before or after the operation
pub fn state(&self) -> State {
self.state.unwrap_or_default()
}
}
/// Transform operation type for `<changePoll:operation>`
// todo: Allow struct enum variants with #[xml(attribute, rename = "op")] in instant-xml,
// to make this struct more ergonomic.
#[derive(Debug, FromXml, ToXml)]
#[xml(rename = "operation", ns(XMLNS))]
pub struct Operation<'a> {
/// Custom value for`OperationKind::Custom`
#[xml(attribute, rename = "op")]
op: Option<Cow<'a, str>>,
/// The operation
#[xml(direct)]
kind: OperationType,
}
impl Operation<'_> {
pub fn kind(&self) -> Result<OperationKind, Error> {
Ok(match self.kind {
OperationType::Create => OperationKind::Create,
OperationType::Delete => OperationKind::Delete,
OperationType::Renew => OperationKind::Renew,
OperationType::Transfer => OperationKind::Transfer,
OperationType::Update => OperationKind::Update,
OperationType::Restore => OperationKind::Restore,
OperationType::AutoRenew => OperationKind::AutoRenew,
OperationType::AutoDelete => OperationKind::AutoDelete,
OperationType::AutoPurge => OperationKind::AutoPurge,
OperationType::Custom => match self.op.as_deref() {
Some(op) => OperationKind::Custom(op),
None => {
return Err(Error::Other(
"invariant error: Missing op attribute for custom operation".to_string(),
))
}
},
})
}
}
/// Enumerated list of operations
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum OperationKind<'a> {
Create,
Delete,
Renew,
Transfer,
Update,
Restore,
AutoRenew,
AutoDelete,
AutoPurge,
Custom(&'a str),
}
/// Internal Enumerated list of operations, with extensibility via "custom"
// See todo on `Operation` struct for reason why this is internal only.
#[derive(Debug, Copy, Clone, FromXml, ToXml)]
#[xml(scalar, rename_all = "camelCase", ns(XMLNS))]
enum OperationType {
Create,
Delete,
Renew,
Transfer,
Update,
Restore,
AutoRenew,
AutoDelete,
AutoPurge,
Custom,
}
/// Case identifier type for `<changePoll:caseId>`
// todo: Allow struct enum variants with #[xml(attribute, rename = "op")] in instant-xml,
// to make this struct more ergonomic.
#[derive(Debug, FromXml, ToXml)]
#[xml(rename = "caseId", ns(XMLNS))]
pub struct CaseIdentifier<'a> {
#[xml(attribute, rename = "type")]
id_type: CaseIdentifierType,
#[xml(attribute)]
name: Option<Cow<'a, str>>,
#[xml(direct)]
pub id: Cow<'a, str>,
}
impl CaseIdentifier<'_> {
pub fn kind(&self) -> Result<CaseIdentifierKind, Error> {
Ok(match self.id_type {
CaseIdentifierType::Udrp => CaseIdentifierKind::Udrp,
CaseIdentifierType::Urs => CaseIdentifierKind::Urs,
CaseIdentifierType::Custom => match self.name.as_deref() {
Some(name) => CaseIdentifierKind::Custom(name),
None => {
return Err(Error::Other(
"invariant error: Missing name attribute for custom case identifier"
.to_string(),
))
}
},
})
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CaseIdentifierKind<'a> {
Udrp,
Urs,
Custom(&'a str),
}
/// Internal enumerated list of case identifier types
// See todo on `CaseIdentifier` struct for reason why this is internal only.
#[derive(Debug, Copy, Clone, FromXml, ToXml)]
#[xml(scalar, rename_all = "camelCase")]
enum CaseIdentifierType {
Udrp,
Urs,
Custom,
}
/// Reason type for `<changePoll:reason>`
///
/// A human-readable message that describes the reason for the encapsulating element.
/// The language of the response is identified via the "lang" attribute.
///
/// Schema defined in the `eppcom-1.0` XML schema
// todo: while this is defined in `eppcom` schema, it is used with different
// namespaces in additional specs (for example in RFC8590).
// Currently, instant-xml strongly ties namespaces to schemas and does not allow
// a way out of it for this particular case.
#[derive(Debug, Eq, FromXml, PartialEq, ToXml)]
#[xml(rename = "reason", ns(XMLNS))]
pub struct Reason {
/// The language of the response. If not specified, assume "en" (English).
#[xml(attribute)]
pub lang: Option<String>,
#[xml(direct)]
pub inner: String,
}
/// Enumerated state of the object in the poll message
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, FromXml, ToXml)]
#[xml(scalar, rename_all = "camelCase")]
pub enum State {
Before,
#[default]
After,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::poll::Poll;
use crate::response::ResultCode;
use crate::tests::{response_from_file_with_ext, CLTRID, SVTRID};
#[test]
fn urs_lock_before() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
"response/extensions/change_poll/urs_lock_before.xml",
);
assert_eq!(
object.result.code,
ResultCode::CommandCompletedSuccessfullyAckToDequeue
);
assert_eq!(
object.result.message,
"Command completed successfully; ack to dequeue"
);
assert_eq!(object.extension().unwrap().state.unwrap(), State::Before);
assert_eq!(
object.extension().unwrap().operation.kind().unwrap(),
OperationKind::Update
);
assert_eq!(object.extension().unwrap().date, "2013-10-22T14:25:57.0Z");
assert_eq!(object.extension().unwrap().server_tr_id, "12345-XYZ");
assert_eq!(object.extension().unwrap().who, "URS Admin");
assert_eq!(
object
.extension()
.unwrap()
.case_id
.as_ref()
.unwrap()
.kind()
.unwrap(),
CaseIdentifierKind::Urs
);
assert_eq!(
object.extension().unwrap().reason.as_ref().unwrap().inner,
"URS Lock"
);
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
}
#[test]
fn urs_lock_after() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
"response/extensions/change_poll/urs_lock_after.xml",
);
assert_eq!(
object.result.code,
ResultCode::CommandCompletedSuccessfullyAckToDequeue
);
assert_eq!(
object.result.message,
"Command completed successfully; ack to dequeue"
);
assert_eq!(object.extension().unwrap().state.unwrap(), State::After);
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
}
#[test]
fn custom_sync_after() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
"response/extensions/change_poll/custom_sync_after.xml",
);
assert_eq!(
object.result.code,
ResultCode::CommandCompletedSuccessfullyAckToDequeue
);
assert_eq!(
object.result.message,
"Command completed successfully; ack to dequeue"
);
assert_eq!(
object.extension().unwrap().operation.kind().unwrap(),
OperationKind::Custom("sync")
);
assert_eq!(object.extension().unwrap().who, "CSR");
assert_eq!(
object.extension().unwrap().reason.as_ref().unwrap().inner,
"Customer sync request"
);
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
}
#[test]
fn delete_before() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
"response/extensions/change_poll/delete_before.xml",
);
assert_eq!(
object.result.code,
ResultCode::CommandCompletedSuccessfullyAckToDequeue
);
assert_eq!(
object.result.message,
"Command completed successfully; ack to dequeue"
);
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
}
#[test]
fn autopurge_before() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
"response/extensions/change_poll/autopurge_before.xml",
);
assert_eq!(
object.result.code,
ResultCode::CommandCompletedSuccessfullyAckToDequeue
);
assert_eq!(
object.result.message,
"Command completed successfully; ack to dequeue"
);
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
}
#[test]
fn update_after() {
let object = response_from_file_with_ext::<Poll, ChangePoll>(
"response/extensions/change_poll/update_after.xml",
);
assert_eq!(
object.result.code,
ResultCode::CommandCompletedSuccessfullyAckToDequeue
);
assert_eq!(
object.result.message,
"Command completed successfully; ack to dequeue"
);
assert_eq!(object.tr_ids.client_tr_id.unwrap(), CLTRID);
assert_eq!(object.tr_ids.server_tr_id, SVTRID);
}
}

View File

@ -51,6 +51,7 @@ pub mod response;
pub mod xml;
pub mod extensions {
pub mod change_poll;
pub mod consolidate;
pub mod frnic;
pub mod low_balance;

View File

@ -1,6 +1,7 @@
use instant_xml::{FromXml, ToXml};
use crate::common::{NoExtension, EPP_XMLNS};
use crate::domain;
use crate::domain::transfer::TransferData;
use crate::extensions::low_balance::LowBalance;
use crate::extensions::rgp::poll::RgpPollData;
@ -61,12 +62,14 @@ impl ToXml for Ack<'_> {
// Response
/// Type that represents the `<trnData>` tag for message poll response
/// Type that represents the `<resData>` tag for message poll response
#[derive(Debug, FromXml)]
#[xml(forward)]
pub enum PollData {
/// Data under the `<domain:trnData>` tag
DomainTransfer(TransferData),
/// Data under the `<domain:infData>` tag
DomainInfo(domain::InfoData),
/// Data under the `<host:infData>` tag
HostInfo(host::InfoData),
/// Data under the `<lowbalance>` tag

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
An example poll <info> response with the
<changePoll:changeData> extension for an "autoPurge" operation on the
domain.example domain name that previously had the "pendingDelete"
status, with the "before" state. The "before" state is reflected in
the <resData> block
-->
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ id="200" count="1">
<qDate>2013-10-22T14:25:57.0Z</qDate>
<msg>Registry purged domain with pendingDelete status.</msg>
</msgQ>
<resData>
<domain:infData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>domain.example</domain:name>
<domain:roid>EXAMPLE1-REP</domain:roid>
<domain:status s="pendingDelete"/>
<domain:clID>ClientX</domain:clID>
</domain:infData>
</resData>
<extension>
<changePoll:changeData xmlns:changePoll="urn:ietf:params:xml:ns:changePoll-1.0" state="before">
<changePoll:operation>autoPurge</changePoll:operation>
<changePoll:date>2013-10-22T14:25:57.0Z</changePoll:date>
<changePoll:svTRID>12345-XYZ</changePoll:svTRID>
<changePoll:who>Batch</changePoll:who>
<changePoll:reason>Past pendingDelete 5 day period</changePoll:reason>
</changePoll:changeData>
</extension>
<trID>
<clTRID>cltrid:1626454866</clTRID>
<svTRID>RO-6879-1627224678242975</svTRID>
</trID>
</response>
</epp>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
An example poll <info> response with the
<changePoll:changeData> extension for a custom "sync" operation on
the domain.example domain name, with the default "after" state. The
"after" state is reflected in the <resData> block
-->
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ id="201" count="1">
<qDate>2013-10-22T14:25:57.0Z</qDate>
<msg>Registry initiated Sync of Domain Expiration Date</msg>
</msgQ>
<resData>
<domain:infData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>domain.example</domain:name>
<domain:roid>EXAMPLE1-REP</domain:roid>
<domain:status s="ok"/>
<domain:registrant>jd1234</domain:registrant>
<domain:contact type="admin">sh8013</domain:contact>
<domain:contact type="tech">sh8013</domain:contact>
<domain:clID>ClientX</domain:clID>
<domain:crID>ClientY</domain:crID>
<domain:crDate>2012-04-03T22:00:00.0Z</domain:crDate>
<domain:upID>ClientZ</domain:upID>
<domain:upDate>2013-10-22T14:25:57.0Z</domain:upDate>
<domain:exDate>2014-04-03T22:00:00.0Z</domain:exDate>
</domain:infData>
</resData>
<extension>
<changePoll:changeData xmlns:changePoll="urn:ietf:params:xml:ns:changePoll-1.0">
<changePoll:operation op="sync">custom</changePoll:operation>
<changePoll:date>2013-10-22T14:25:57.0Z</changePoll:date>
<changePoll:svTRID>12345-XYZ</changePoll:svTRID>
<changePoll:who>CSR</changePoll:who>
<changePoll:reason lang="en">Customer sync request</changePoll:reason>
</changePoll:changeData>
</extension>
<trID>
<clTRID>cltrid:1626454866</clTRID>
<svTRID>RO-6879-1627224678242975</svTRID>
</trID>
</response>
</epp>

View File

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
An example poll <info> response with the
<changePoll:changeData> extension for a "delete" operation on the
domain.example domain name that is immediately purged, with the
"before" state. The "before" state is reflected in the <resData>
block
-->
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ id="200" count="1">
<qDate>2013-10-22T14:25:57.0Z</qDate>
<msg>Registry initiated delete of domain resulting in immediate purge.</msg>
</msgQ>
<resData>
<domain:infData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>domain.example</domain:name>
<domain:roid>EXAMPLE1-REP</domain:roid>
<domain:clID>ClientX</domain:clID>
</domain:infData>
</resData>
<extension>
<changePoll:changeData xmlns:changePoll="urn:ietf:params:xml:ns:changePoll-1.0" state="before">
<changePoll:operation op="purge">delete</changePoll:operation>
<changePoll:date>2013-10-22T14:25:57.0Z</changePoll:date>
<changePoll:svTRID>12345-XYZ</changePoll:svTRID>
<changePoll:who>ClientZ</changePoll:who>
<changePoll:reason>Court order</changePoll:reason>
</changePoll:changeData>
</extension>
<trID>
<clTRID>cltrid:1626454866</clTRID>
<svTRID>RO-6879-1627224678242975</svTRID>
</trID>
</response>
</epp>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
An example poll <info> response with the
<changePoll:changeData> extension for an "update" operation on the
ns1.domain.example host, with the default "after" state. The "after"
state is reflected in the <resData> block
-->
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1301">
<msg>Command completed successfully; ack to dequeue</msg>
</result>
<msgQ id="201" count="1">
<qDate>2013-10-22T14:25:57.0Z</qDate>
<msg>Registry initiated update of host.</msg>
</msgQ>
<resData>
<host:infData xmlns:host="urn:ietf:params:xml:ns:host-1.0">
<host:name>ns1.domain.example</host:name>
<host:roid>NS1_EXAMPLE1-REP</host:roid>
<host:status s="linked"/>
<host:status s="serverUpdateProhibited"/>
<host:status s="serverDeleteProhibited"/>
<host:addr ip="v4">192.0.2.2</host:addr>
<host:addr ip="v6">2001:db8:0:0:1:0:0:1</host:addr>
<host:clID>ClientX</host:clID>
<host:crID>ClientY</host:crID>
<host:crDate>2012-04-03T22:00:00.0Z</host:crDate>
<host:upID>ClientY</host:upID>
<host:upDate>2013-10-22T14:25:57.0Z</host:upDate>
</host:infData>
</resData>
<extension>
<changePoll:changeData xmlns:changePoll="urn:ietf:params:xml:ns:changePoll-1.0">
<changePoll:operation>update</changePoll:operation>
<changePoll:date>2013-10-22T14:25:57.0Z</changePoll:date>
<changePoll:svTRID>12345-XYZ</changePoll:svTRID>
<changePoll:who>ClientZ</changePoll:who>
<changePoll:reason>Host Lock</changePoll:reason>
</changePoll:changeData>
</extension>
<trID>
<clTRID>cltrid:1626454866</clTRID>
<svTRID>RO-6879-1627224678242975</svTRID>
</trID>
</response>
</epp>

View File

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
An example poll <info> response with the
<changePoll:changeData> extension for a URS lock transaction on the
domain.example domain name, with the "after" state. The "after"
state is reflected in the <resData> block
-->
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1301">
<msg lang="en-US">Command completed successfully; ack to dequeue</msg>
</result>
<msgQ id="202" count="1">
<qDate>2013-10-22T14:25:57.0Z</qDate>
<msg>Registry initiated update of domain.</msg>
</msgQ>
<resData>
<domain:infData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>domain.example</domain:name>
<domain:roid>EXAMPLE1-REP</domain:roid>
<domain:status s="serverUpdateProhibited"/>
<domain:status s="serverDeleteProhibited"/>
<domain:status s="serverTransferProhibited"/>
<domain:registrant>jd1234</domain:registrant>
<domain:contact type="admin">sh8013</domain:contact>
<domain:contact type="tech">sh8013</domain:contact>
<domain:clID>ClientX</domain:clID>
<domain:crID>ClientY</domain:crID>
<domain:crDate>2012-04-03T22:00:00.0Z</domain:crDate>
<domain:upID>ClientZ</domain:upID>
<domain:upDate>2013-10-22T14:25:57.0Z</domain:upDate>
<domain:exDate>2014-04-03T22:00:00.0Z</domain:exDate>
</domain:infData>
</resData>
<extension>
<changePoll:changeData xmlns:changePoll="urn:ietf:params:xml:ns:changePoll-1.0" state="after">
<changePoll:operation>update</changePoll:operation>
<changePoll:date>2013-10-22T14:25:57.0Z</changePoll:date>
<changePoll:svTRID>12345-XYZ</changePoll:svTRID>
<changePoll:who>URS Admin</changePoll:who>
<changePoll:caseId type="urs">urs123</changePoll:caseId>
<changePoll:reason>URS Lock</changePoll:reason>
</changePoll:changeData>
</extension>
<trID>
<clTRID>cltrid:1626454866</clTRID>
<svTRID>RO-6879-1627224678242975</svTRID>
</trID>
</response>
</epp>

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
An example poll <info> response with the
<changePoll:changeData> extension for a URS lock transaction on the
domain.example domain name, with the "before" state. The "before"
state is reflected in the <resData> block
-->
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<response>
<result code="1301">
<msg lang="en-US">Command completed successfully; ack to dequeue</msg>
</result>
<msgQ id="201" count="1">
<qDate>2013-10-22T14:25:57.0Z</qDate>
<msg>Registry initiated update of domain.</msg>
</msgQ>
<resData>
<domain:infData xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>domain.example</domain:name>
<domain:roid>EXAMPLE1-REP</domain:roid>
<domain:status s="ok"/>
<domain:registrant>jd1234</domain:registrant>
<domain:contact type="admin">sh8013</domain:contact>
<domain:contact type="tech">sh8013</domain:contact>
<domain:clID>ClientX</domain:clID>
<domain:crID>ClientY</domain:crID>
<domain:crDate>2012-04-03T22:00:00.0Z</domain:crDate>
<domain:exDate>2014-04-03T22:00:00.0Z</domain:exDate>
</domain:infData>
</resData>
<extension>
<changePoll:changeData xmlns:changePoll="urn:ietf:params:xml:ns:changePoll-1.0" state="before">
<changePoll:operation>update</changePoll:operation>
<changePoll:date>2013-10-22T14:25:57.0Z</changePoll:date>
<changePoll:svTRID>12345-XYZ</changePoll:svTRID>
<changePoll:who>URS Admin</changePoll:who>
<changePoll:caseId type="urs">urs123</changePoll:caseId>
<changePoll:reason>URS Lock</changePoll:reason>
</changePoll:changeData>
</extension>
<trID>
<clTRID>cltrid:1626454866</clTRID>
<svTRID>RO-6879-1627224678242975</svTRID>
</trID>
</response>
</epp>