Federate identity proofs as actor attachments
https://codeberg.org/silverpill/mitra/issues/7
This commit is contained in:
parent
83fbbefaab
commit
7a47c28034
6 changed files with 155 additions and 47 deletions
|
@ -2,7 +2,13 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_json::{json, Value};
|
||||
|
||||
use crate::config::Instance;
|
||||
use crate::models::profiles::types::ExtraField;
|
||||
use crate::errors::ValidationError;
|
||||
use crate::ethereum::identity::{
|
||||
ETHEREUM_EIP191_PROOF,
|
||||
DidPkh,
|
||||
verify_identity_proof,
|
||||
};
|
||||
use crate::models::profiles::types::{ExtraField, IdentityProof};
|
||||
use crate::models::users::types::User;
|
||||
use crate::utils::crypto::{deserialize_private_key, get_public_key_pem};
|
||||
use crate::utils::files::get_file_url;
|
||||
|
@ -14,7 +20,7 @@ use super::views::{
|
|||
get_followers_url,
|
||||
get_following_url,
|
||||
};
|
||||
use super::vocabulary::{IMAGE, PERSON, PROPERTY_VALUE, SERVICE};
|
||||
use super::vocabulary::{IDENTITY_PROOF, IMAGE, PERSON, PROPERTY_VALUE, SERVICE};
|
||||
|
||||
const W3ID_CONTEXT: &str = "https://w3id.org/security/v1";
|
||||
|
||||
|
@ -43,11 +49,21 @@ pub struct ActorCapabilities {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ActorProperty {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ActorAttachment {
|
||||
name: String,
|
||||
|
||||
#[serde(rename = "type")]
|
||||
object_type: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
value: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
signature_algorithm: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
signature_value: Option<String>,
|
||||
}
|
||||
|
||||
// Clone and Debug traits are required by FromSql
|
||||
|
@ -90,36 +106,99 @@ pub struct Actor {
|
|||
pub summary: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub attachment: Option<Vec<ActorProperty>>,
|
||||
pub attachment: Option<Vec<ActorAttachment>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
}
|
||||
|
||||
fn parse_identity_proof(
|
||||
actor_id: &str,
|
||||
attachment: &ActorAttachment,
|
||||
) -> Result<IdentityProof, ValidationError> {
|
||||
if attachment.object_type != IDENTITY_PROOF {
|
||||
return Err(ValidationError("invalid attachment type"));
|
||||
};
|
||||
let proof_type = attachment.signature_algorithm.as_ref()
|
||||
.ok_or(ValidationError("missing proof type"))?;
|
||||
if proof_type != ETHEREUM_EIP191_PROOF {
|
||||
return Err(ValidationError("unknown proof type"));
|
||||
};
|
||||
let did = attachment.name.parse::<DidPkh>()
|
||||
.map_err(|_| ValidationError("invalid did"))?;
|
||||
let signature = attachment.signature_value.as_ref()
|
||||
.ok_or(ValidationError("missing signature"))?;
|
||||
verify_identity_proof(
|
||||
actor_id,
|
||||
&did,
|
||||
signature,
|
||||
).map_err(|_| ValidationError("invalid identity proof"))?;
|
||||
let proof = IdentityProof {
|
||||
issuer: did,
|
||||
proof_type: proof_type.to_string(),
|
||||
value: signature.to_string(),
|
||||
};
|
||||
Ok(proof)
|
||||
}
|
||||
|
||||
fn parse_extra_field(
|
||||
attachment: &ActorAttachment,
|
||||
) -> Result<ExtraField, ValidationError> {
|
||||
if attachment.object_type != PROPERTY_VALUE {
|
||||
return Err(ValidationError("invalid attachment type"));
|
||||
};
|
||||
let property_value = attachment.value.as_ref()
|
||||
.ok_or(ValidationError("missing property value"))?;
|
||||
let field = ExtraField {
|
||||
name: attachment.name.clone(),
|
||||
value: property_value.to_string(),
|
||||
value_source: None,
|
||||
};
|
||||
Ok(field)
|
||||
}
|
||||
|
||||
impl Actor {
|
||||
/// Parse 'attachment' into ExtraField vector
|
||||
pub fn extra_fields(&self) -> Vec<ExtraField> {
|
||||
|
||||
pub fn parse_attachments(&self) -> (Vec<IdentityProof>, Vec<ExtraField>) {
|
||||
let mut identity_proofs = vec![];
|
||||
let mut extra_fields = vec![];
|
||||
if let Some(properties) = &self.attachment {
|
||||
for property in properties {
|
||||
if property.object_type != PROPERTY_VALUE {
|
||||
log::warn!(
|
||||
"ignoring actor property of type {}",
|
||||
property.object_type,
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if let Some(property_value) = &property.value {
|
||||
let field = ExtraField {
|
||||
name: property.name.clone(),
|
||||
value: property_value.clone(),
|
||||
value_source: None,
|
||||
};
|
||||
extra_fields.push(field);
|
||||
if let Some(attachments) = &self.attachment {
|
||||
for attachment in attachments {
|
||||
match attachment.object_type.as_str() {
|
||||
IDENTITY_PROOF => {
|
||||
match parse_identity_proof(&self.id, attachment) {
|
||||
Ok(proof) => identity_proofs.push(proof),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"ignoring actor attachment of type {}: {}",
|
||||
attachment.object_type,
|
||||
error,
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
PROPERTY_VALUE => {
|
||||
match parse_extra_field(attachment) {
|
||||
Ok(field) => extra_fields.push(field),
|
||||
Err(error) => {
|
||||
log::warn!(
|
||||
"ignoring actor attachment of type {}: {}",
|
||||
attachment.object_type,
|
||||
error,
|
||||
);
|
||||
},
|
||||
};
|
||||
},
|
||||
_ => {
|
||||
log::warn!(
|
||||
"ignoring actor attachment of type {}",
|
||||
attachment.object_type,
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
};
|
||||
extra_fields
|
||||
(identity_proofs, extra_fields)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,15 +262,27 @@ pub fn get_local_actor(
|
|||
},
|
||||
None => None,
|
||||
};
|
||||
let properties = user.profile.extra_fields.clone()
|
||||
.into_inner().into_iter()
|
||||
.map(|field| {
|
||||
ActorProperty {
|
||||
object_type: PROPERTY_VALUE.to_string(),
|
||||
name: field.name,
|
||||
value: Some(field.value),
|
||||
}
|
||||
}).collect();
|
||||
let mut attachments = vec![];
|
||||
for proof in user.profile.identity_proofs.clone().into_inner() {
|
||||
let attachment = ActorAttachment {
|
||||
object_type: IDENTITY_PROOF.to_string(),
|
||||
name: proof.issuer.to_string(),
|
||||
value: None,
|
||||
signature_algorithm: Some(proof.proof_type),
|
||||
signature_value: Some(proof.value),
|
||||
};
|
||||
attachments.push(attachment);
|
||||
};
|
||||
for field in user.profile.extra_fields.clone().into_inner() {
|
||||
let attachment = ActorAttachment {
|
||||
object_type: PROPERTY_VALUE.to_string(),
|
||||
name: field.name,
|
||||
value: Some(field.value),
|
||||
signature_algorithm: None,
|
||||
signature_value: None,
|
||||
};
|
||||
attachments.push(attachment);
|
||||
};
|
||||
let actor = Actor {
|
||||
context: Some(json!([
|
||||
AP_CONTEXT.to_string(),
|
||||
|
@ -210,7 +301,7 @@ pub fn get_local_actor(
|
|||
icon: avatar,
|
||||
image: banner,
|
||||
summary: None,
|
||||
attachment: Some(properties),
|
||||
attachment: Some(attachments),
|
||||
url: Some(actor_id),
|
||||
};
|
||||
Ok(actor)
|
||||
|
@ -279,5 +370,6 @@ mod tests {
|
|||
let actor = get_local_actor(&user, INSTANCE_URL).unwrap();
|
||||
assert_eq!(actor.id, "https://example.com/users/testuser");
|
||||
assert_eq!(actor.preferred_username, user.profile.username);
|
||||
assert_eq!(actor.attachment.unwrap().len(), 0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ pub async fn fetch_profile_by_actor_id(
|
|||
let actor_json = send_request(instance, actor_url, &[]).await?;
|
||||
let actor: Actor = serde_json::from_str(&actor_json)?;
|
||||
let (avatar, banner) = fetch_avatar_and_banner(&actor, media_dir).await?;
|
||||
let extra_fields = actor.extra_fields();
|
||||
let (identity_proofs, extra_fields) = actor.parse_attachments();
|
||||
let actor_address = format!(
|
||||
"{}@{}",
|
||||
actor.preferred_username,
|
||||
|
@ -159,7 +159,7 @@ pub async fn fetch_profile_by_actor_id(
|
|||
bio: actor.summary.clone(),
|
||||
avatar,
|
||||
banner,
|
||||
identity_proofs: vec![],
|
||||
identity_proofs,
|
||||
extra_fields,
|
||||
actor_json: Some(actor),
|
||||
};
|
||||
|
|
|
@ -698,7 +698,7 @@ pub async fn receive_activity(
|
|||
let profile = get_profile_by_actor_id(db_client, &actor.id).await?;
|
||||
let (avatar, banner) = fetch_avatar_and_banner(&actor, &config.media_dir()).await
|
||||
.map_err(|_| ValidationError("failed to fetch image"))?;
|
||||
let extra_fields = actor.extra_fields();
|
||||
let (identity_proofs, extra_fields) = actor.parse_attachments();
|
||||
let actor_old = profile.actor_json.unwrap();
|
||||
if actor_old.id != actor.id {
|
||||
log::warn!(
|
||||
|
@ -720,7 +720,7 @@ pub async fn receive_activity(
|
|||
bio_source: actor.summary.clone(),
|
||||
avatar,
|
||||
banner,
|
||||
identity_proofs: vec![],
|
||||
identity_proofs,
|
||||
extra_fields,
|
||||
actor_json: Some(actor),
|
||||
};
|
||||
|
|
|
@ -29,4 +29,5 @@ pub const ORDERED_COLLECTION_PAGE: &str = "OrderedCollectionPage";
|
|||
|
||||
// Misc
|
||||
pub const HASHTAG: &str = "Hashtag";
|
||||
pub const IDENTITY_PROOF: &str = "IdentityProof";
|
||||
pub const PROPERTY_VALUE: &str = "PropertyValue";
|
||||
|
|
|
@ -1,11 +1,26 @@
|
|||
use tokio_postgres::GenericClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::activitypub::actor::Actor;
|
||||
use crate::errors::DatabaseError;
|
||||
use crate::models::relationships::queries::get_relationships;
|
||||
use crate::models::relationships::queries::{get_followers, get_relationships};
|
||||
use crate::models::relationships::types::RelationshipType;
|
||||
use super::types::RelationshipMap;
|
||||
|
||||
pub async fn get_profile_update_recipients(
|
||||
db_client: &impl GenericClient,
|
||||
current_user_id: &Uuid,
|
||||
) -> Result<Vec<Actor>, DatabaseError> {
|
||||
let followers = get_followers(db_client, current_user_id, None, None).await?;
|
||||
let mut recipients: Vec<Actor> = Vec::new();
|
||||
for profile in followers {
|
||||
if let Some(remote_actor) = profile.actor_json {
|
||||
recipients.push(remote_actor);
|
||||
};
|
||||
};
|
||||
Ok(recipients)
|
||||
}
|
||||
|
||||
pub async fn get_relationship(
|
||||
db_client: &impl GenericClient,
|
||||
source_id: &Uuid,
|
||||
|
|
|
@ -7,7 +7,6 @@ use crate::activitypub::activity::{
|
|||
create_activity_undo_follow,
|
||||
create_activity_update_person,
|
||||
};
|
||||
use crate::activitypub::actor::Actor;
|
||||
use crate::activitypub::deliverer::deliver_activity;
|
||||
use crate::config::Config;
|
||||
use crate::database::{Pool, get_database_client};
|
||||
|
@ -57,7 +56,7 @@ use crate::utils::crypto::{
|
|||
serialize_private_key,
|
||||
};
|
||||
use crate::utils::files::FileError;
|
||||
use super::helpers::get_relationship;
|
||||
use super::helpers::{get_profile_update_recipients, get_relationship};
|
||||
use super::types::{
|
||||
Account,
|
||||
AccountCreateData,
|
||||
|
@ -210,13 +209,7 @@ async fn update_credentials(
|
|||
// Federate
|
||||
let activity = create_activity_update_person(¤t_user, &config.instance_url())
|
||||
.map_err(|_| HttpError::InternalError)?;
|
||||
let followers = get_followers(db_client, ¤t_user.id, None, None).await?;
|
||||
let mut recipients: Vec<Actor> = Vec::new();
|
||||
for follower in followers {
|
||||
if let Some(remote_actor) = follower.actor_json {
|
||||
recipients.push(remote_actor);
|
||||
};
|
||||
};
|
||||
let recipients = get_profile_update_recipients(db_client, ¤t_user.id).await?;
|
||||
deliver_activity(&config, ¤t_user, activity, recipients);
|
||||
|
||||
let account = Account::from_user(current_user, &config.instance_url());
|
||||
|
@ -282,6 +275,13 @@ async fn create_identity_proof(
|
|||
¤t_user.id,
|
||||
profile_data,
|
||||
).await?;
|
||||
|
||||
// Federate
|
||||
let activity = create_activity_update_person(¤t_user, &config.instance_url())
|
||||
.map_err(|_| HttpError::InternalError)?;
|
||||
let recipients = get_profile_update_recipients(db_client, ¤t_user.id).await?;
|
||||
deliver_activity(&config, ¤t_user, activity, recipients);
|
||||
|
||||
let account = Account::from_user(current_user, &config.instance_url());
|
||||
Ok(HttpResponse::Ok().json(account))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue