mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-07-19 22:42:25 +00:00
fix(plc): allow arbitrary services to sign
Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
@@ -3,5 +3,5 @@ mod sign;
|
||||
mod submit;
|
||||
|
||||
pub use request::request_plc_operation_signature;
|
||||
pub use sign::{ServiceInput, SignPlcOperationInput, SignPlcOperationOutput, sign_plc_operation};
|
||||
pub use sign::{SignPlcOperationInput, SignPlcOperationOutput, sign_plc_operation};
|
||||
pub use submit::{SubmitPlcOperationInput, submit_plc_operation};
|
||||
|
||||
@@ -9,7 +9,7 @@ use tranquil_pds::api::ApiError;
|
||||
use tranquil_pds::api::error::DbResultExt;
|
||||
use tranquil_pds::auth::{Auth, Permissive};
|
||||
use tranquil_pds::circuit_breaker::with_circuit_breaker;
|
||||
use tranquil_pds::plc::{PlcError, PlcService, ServiceType, create_update_op, sign_operation};
|
||||
use tranquil_pds::plc::{PlcError, PlcService, create_update_op, sign_operation};
|
||||
use tranquil_pds::state::AppState;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -19,14 +19,7 @@ pub struct SignPlcOperationInput {
|
||||
pub rotation_keys: Option<Vec<String>>,
|
||||
pub also_known_as: Option<Vec<String>>,
|
||||
pub verification_methods: Option<HashMap<String, String>>,
|
||||
pub services: Option<HashMap<String, ServiceInput>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ServiceInput {
|
||||
#[serde(rename = "type")]
|
||||
pub service_type: ServiceType,
|
||||
pub endpoint: String,
|
||||
pub services: Option<HashMap<String, PlcService>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -106,25 +99,12 @@ pub async fn sign_plc_operation(
|
||||
if last_op.is_tombstone() {
|
||||
return Err(ApiError::from(PlcError::Tombstoned));
|
||||
}
|
||||
let services = input.services.map(|s| {
|
||||
s.into_iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
PlcService {
|
||||
service_type: v.service_type,
|
||||
endpoint: v.endpoint,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
let unsigned_op = create_update_op(
|
||||
&last_op,
|
||||
input.rotation_keys,
|
||||
input.verification_methods,
|
||||
input.also_known_as,
|
||||
services,
|
||||
input.services,
|
||||
)
|
||||
.map_err(|e| match e {
|
||||
PlcError::Tombstoned => ApiError::InvalidRequest("Cannot update tombstoned DID".into()),
|
||||
|
||||
@@ -39,27 +39,77 @@ pub enum PlcOpType {
|
||||
Tombstone,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Error)]
|
||||
#[error("service type must not be empty")]
|
||||
pub struct EmptyServiceType;
|
||||
|
||||
mod custom_service_type {
|
||||
use super::EmptyServiceType;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct CustomServiceType(String);
|
||||
|
||||
impl CustomServiceType {
|
||||
pub(super) fn new(name: String) -> Result<Self, EmptyServiceType> {
|
||||
match name.as_str() {
|
||||
"" => Err(EmptyServiceType),
|
||||
_ => Ok(Self(name)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use custom_service_type::CustomServiceType;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ServiceType {
|
||||
#[serde(rename = "AtprotoPersonalDataServer")]
|
||||
Pds,
|
||||
#[serde(rename = "AtprotoAppView")]
|
||||
AppView,
|
||||
#[serde(rename = "AtprotoLabeler")]
|
||||
Labeler,
|
||||
Other(CustomServiceType),
|
||||
}
|
||||
|
||||
impl ServiceType {
|
||||
pub fn as_str(self) -> &'static str {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Pds => "AtprotoPersonalDataServer",
|
||||
Self::AppView => "AtprotoAppView",
|
||||
Self::Labeler => "AtprotoLabeler",
|
||||
Self::Other(name) => name.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_pds(self) -> bool {
|
||||
matches!(self, Self::Pds)
|
||||
impl TryFrom<String> for ServiceType {
|
||||
type Error = EmptyServiceType;
|
||||
|
||||
fn try_from(name: String) -> Result<Self, Self::Error> {
|
||||
match name.as_str() {
|
||||
"AtprotoPersonalDataServer" => Ok(Self::Pds),
|
||||
"AtprotoLabeler" => Ok(Self::Labeler),
|
||||
_ => CustomServiceType::new(name).map(Self::Other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for ServiceType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for ServiceType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let name = String::deserialize(deserializer)?;
|
||||
Self::try_from(name).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -625,4 +675,80 @@ mod tests {
|
||||
let signed = sign_operation(&op, &key).unwrap();
|
||||
assert!(signed.get("sig").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_type_known_round_trip() {
|
||||
let cases = [
|
||||
(ServiceType::Pds, "\"AtprotoPersonalDataServer\""),
|
||||
(ServiceType::Labeler, "\"AtprotoLabeler\""),
|
||||
];
|
||||
cases.iter().for_each(|(variant, encoded)| {
|
||||
assert_eq!(serde_json::to_string(variant).unwrap(), *encoded);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<ServiceType>(encoded).unwrap(),
|
||||
*variant
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_type_custom_round_trips() {
|
||||
let parsed: ServiceType = serde_json::from_str("\"ConchFeedGenerator\"").unwrap();
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ServiceType::try_from("ConchFeedGenerator".to_string()).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&parsed).unwrap(),
|
||||
"\"ConchFeedGenerator\""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_type_custom_normalizes_known_names() {
|
||||
assert_eq!(
|
||||
ServiceType::try_from("AtprotoPersonalDataServer".to_string()).unwrap(),
|
||||
ServiceType::Pds
|
||||
);
|
||||
assert_eq!(
|
||||
ServiceType::try_from("AtprotoLabeler".to_string()).unwrap(),
|
||||
ServiceType::Labeler
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_appview_is_not_a_named_type() {
|
||||
assert_eq!(
|
||||
ServiceType::try_from("AtprotoAppView".to_string()).unwrap(),
|
||||
ServiceType::Other(CustomServiceType::new("AtprotoAppView".to_string()).unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_service_type_rejects_empty() {
|
||||
assert!(ServiceType::try_from(String::new()).is_err());
|
||||
assert!(serde_json::from_str::<ServiceType>("\"\"").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plc_operation_with_custom_service_round_trips() {
|
||||
let op_json = json!({
|
||||
"type": "plc_operation",
|
||||
"rotationKeys": ["did:key:zScallop"],
|
||||
"verificationMethods": { "atproto": "did:key:zUni" },
|
||||
"alsoKnownAs": ["at://whelk.nel.pet"],
|
||||
"services": {
|
||||
"atproto_pds": { "type": "AtprotoPersonalDataServer", "endpoint": "https://nel.pet" },
|
||||
"custom_feedgen": { "type": "ConchFeedGenerator", "endpoint": "https://feed.nel.pet" }
|
||||
},
|
||||
"prev": null
|
||||
});
|
||||
let op: PlcOperation = serde_json::from_value(op_json.clone()).unwrap();
|
||||
assert_eq!(
|
||||
op.services["custom_feedgen"].service_type,
|
||||
ServiceType::try_from("ConchFeedGenerator".to_string()).unwrap()
|
||||
);
|
||||
assert_eq!(op.services["atproto_pds"].service_type, ServiceType::Pds);
|
||||
assert_eq!(serde_json::to_value(&op).unwrap(), op_json);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user