From 04e851025b322bb508f0157e9067a2914d036d12 Mon Sep 17 00:00:00 2001 From: silverpill Date: Wed, 20 Apr 2022 13:47:04 +0000 Subject: [PATCH] Enforce uniqueness of actor ID --- README.md | 2 +- .../V0022__actor_profile__add_actor_id.sql | 1 + migrations/schema.sql | 1 + src/activitypub/activity.rs | 1 + src/models/profiles/queries.rs | 35 ++++++++++++++++++- src/models/profiles/types.rs | 5 +++ 6 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 migrations/V0022__actor_profile__add_actor_id.sql diff --git a/README.md b/README.md index 2e045c1..f44724d 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Matrix chat: [#mitra:halogen.city](https://matrix.to/#/#mitra:halogen.city) ## Requirements - Rust 1.54+ -- PostgreSQL 10.2+ +- PostgreSQL 12+ - IPFS node (optional, see [guide](./docs/ipfs.md)) - Ethereum node (optional) diff --git a/migrations/V0022__actor_profile__add_actor_id.sql b/migrations/V0022__actor_profile__add_actor_id.sql new file mode 100644 index 0000000..03a2f4c --- /dev/null +++ b/migrations/V0022__actor_profile__add_actor_id.sql @@ -0,0 +1 @@ +ALTER TABLE actor_profile ADD COLUMN actor_id VARCHAR(200) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED; diff --git a/migrations/schema.sql b/migrations/schema.sql index c58f93a..d082be3 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -12,6 +12,7 @@ CREATE TABLE actor_profile ( following_count INTEGER NOT NULL CHECK (following_count >= 0) DEFAULT 0, post_count INTEGER NOT NULL CHECK (post_count >= 0) DEFAULT 0, actor_json JSONB, + actor_id VARCHAR(200) UNIQUE GENERATED ALWAYS AS (actor_json ->> 'id') STORED, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now() ); diff --git a/src/activitypub/activity.rs b/src/activitypub/activity.rs index 99bf94f..ddb21e6 100644 --- a/src/activitypub/activity.rs +++ b/src/activitypub/activity.rs @@ -572,6 +572,7 @@ mod tests { url: Some(parent_author_actor_url.to_string()), ..Default::default() }), + actor_id: Some(parent_author_actor_id.to_string()), ..Default::default() }; let parent = Post { diff --git a/src/models/profiles/queries.rs b/src/models/profiles/queries.rs index 2cf3155..7dad2e2 100644 --- a/src/models/profiles/queries.rs +++ b/src/models/profiles/queries.rs @@ -116,7 +116,7 @@ pub async fn get_profile_by_actor_id( " SELECT actor_profile FROM actor_profile - WHERE actor_profile.actor_json ->> 'id' = $1 + WHERE actor_id = $1 ", &[&actor_id], ).await?; @@ -471,6 +471,7 @@ pub async fn update_post_count( #[cfg(test)] mod tests { + use serde_json::json; use serial_test::serial; use crate::database::test_utils::create_test_database; use crate::models::profiles::queries::create_profile; @@ -491,6 +492,38 @@ mod tests { assert_eq!(profile.username, "test"); } + #[tokio::test] + #[serial] + async fn test_actor_id_unique() { + let db_client = create_test_database().await; + let actor_id = "https://example.com/users/test"; + let create_actor_value = |actor_id| { + json!({ + "id": actor_id, + "type": "Person", + "preferredUsername": "test", + "inbox": "https://test", + "outbox": "https://test", + "publicKey": {"id": "test", "owner": "test", "publicKeyPem": "test"}, + }) + }; + let profile_data_1 = ProfileCreateData { + username: "test-1".to_string(), + acct: "test-1@example.com".to_string(), + actor_json: Some(create_actor_value(actor_id)), + ..Default::default() + }; + create_profile(&db_client, profile_data_1).await.unwrap(); + let profile_data_2 = ProfileCreateData { + username: "test-2".to_string(), + acct: "test-2@example.com".to_string(), + actor_json: Some(create_actor_value(actor_id)), + ..Default::default() + }; + let error = create_profile(&db_client, profile_data_2).await.err().unwrap(); + assert_eq!(error.to_string(), "profile"); + } + #[tokio::test] #[serial] async fn test_delete_profile() { diff --git a/src/models/profiles/types.rs b/src/models/profiles/types.rs index ceea604..3e8a3f8 100644 --- a/src/models/profiles/types.rs +++ b/src/models/profiles/types.rs @@ -86,6 +86,9 @@ pub struct DbActorProfile { pub post_count: i32, pub created_at: DateTime, pub actor_json: Option, + + // auto-generated database fields + pub actor_id: Option, } impl DbActorProfile { @@ -94,6 +97,7 @@ impl DbActorProfile { } pub fn actor_id(&self, instance_url: &str) -> String { + // TODO: use actor_id field match self.actor_json { Some(ref actor) => actor.id.clone(), None => get_actor_url(instance_url, &self.username), @@ -137,6 +141,7 @@ impl Default for DbActorProfile { post_count: 0, created_at: Utc::now(), actor_json: None, + actor_id: None, } } }