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