aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2021-08-22 10:57:02 +0300
committerGravatar Vitaly Takmazov2021-08-22 10:57:02 +0300
commitd918967281652ead0130c5dbef663e82003d4393 (patch)
treeb4bdd09ee9a2f86e0aae7ee6cdf21672a2f0fd31
parent1d395b762746948c4ba9897c0ff1e5be0aaaf6db (diff)
ActivityPub: handle user deletion for suspended users
-rw-r--r--src/main/java/com/juick/SignatureManager.java54
-rw-r--r--src/main/java/com/juick/www/api/activity/Profile.java2
-rw-r--r--src/main/java/com/juick/www/api/activity/model/objects/Actor.java10
-rw-r--r--src/test/java/com/juick/server/MockDeleteListener.java30
-rw-r--r--src/test/java/com/juick/server/tests/ServerTests.java40
-rw-r--r--src/test/resources/snapshots/activity/test_suspended_user.json47
-rw-r--r--src/test/resources/snapshots/activity/testdelete.json8
7 files changed, 164 insertions, 27 deletions
diff --git a/src/main/java/com/juick/SignatureManager.java b/src/main/java/com/juick/SignatureManager.java
index 668669f1..e9cb4f6a 100644
--- a/src/main/java/com/juick/SignatureManager.java
+++ b/src/main/java/com/juick/SignatureManager.java
@@ -21,6 +21,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.juick.model.AnonymousUser;
import com.juick.model.User;
import com.juick.service.UserService;
+import com.juick.service.activities.DeleteUserEvent;
import com.juick.util.DateFormattersHolder;
import com.juick.www.api.activity.model.Context;
import com.juick.www.api.activity.model.objects.Person;
@@ -30,6 +31,7 @@ import com.juick.www.api.webfinger.model.Link;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.context.ApplicationEventPublisher;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
@@ -69,6 +71,8 @@ public class SignatureManager {
private UserService userService;
@Inject
private RestTemplate apClient;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
public void post(Person from, Person to, Context data) throws IOException, NoSuchAlgorithmException {
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(to.getInbox());
@@ -94,21 +98,17 @@ public class SignatureManager {
logger.info("Remote response: {}", response.getStatusCodeValue());
}
- public String addSignature(Actor from, String host, String method,
- String path, String dateString, String digestHeader)
- throws IOException {
- return addSignature(from, host, method, path, dateString,
- digestHeader, keystoreManager);
+ public String addSignature(Actor from, String host, String method, String path, String dateString,
+ String digestHeader) throws IOException {
+ return addSignature(from, host, method, path, dateString, digestHeader, keystoreManager);
}
- public String addSignature(Actor from, String host, String method,
- String path, String dateString, String digestHeader,
- KeystoreManager keystoreManager) throws IOException {
- List<String> requiredHeaders = StringUtils.isEmpty(digestHeader) ?
- Arrays.asList("(request-target)", "host", "date")
+ public String addSignature(Actor from, String host, String method, String path, String dateString,
+ String digestHeader, KeystoreManager keystoreManager) throws IOException {
+ List<String> requiredHeaders = StringUtils.isEmpty(digestHeader)
+ ? Arrays.asList("(request-target)", "host", "date")
: Arrays.asList("(request-target)", "host", "date", "digest");
- Signature templateSignature = new Signature(from.getPublicKey().getId(),
- "rsa-sha256", null, requiredHeaders);
+ Signature templateSignature = new Signature(from.getPublicKey().getId(), "rsa-sha256", null, requiredHeaders);
Map<String, String> headers = new HashMap<>();
headers.put("host", host);
headers.put("date", dateString);
@@ -125,11 +125,11 @@ public class SignatureManager {
String signatureString = headers.get("signature");
logger.info("Signature: {}", signatureString);
Signature signature = Signature.fromString(signatureString);
- Optional<Context> context = getContext(UriComponentsBuilder.fromUriString(signature.getKeyId())
- .fragment(null).build().toUri());
+ Optional<Context> context = getContext(
+ UriComponentsBuilder.fromUriString(signature.getKeyId()).fragment(null).build().toUri());
if (context.isPresent() && context.get() instanceof Actor) {
- Actor securityObject = (Actor) context.get();
- Key key = KeystoreManager.publicKeyOf(securityObject);
+ Actor actor = (Actor) context.get();
+ Key key = KeystoreManager.publicKeyOf(actor);
Verifier verifier = new Verifier(key, signature);
try {
@@ -137,22 +137,27 @@ public class SignatureManager {
logger.info("signature of {} is valid: {}", signature.getKeyId(), result);
if (result) {
User user = new User();
- user.setUri(URI.create(securityObject.getId()));
+ user.setUri(URI.create(actor.getId()));
if (key.equals(keystoreManager.getPublicKey())) {
- return userService.getUserByName(securityObject.getName());
+ return userService.getUserByName(actor.getName());
+ }
+ if (actor.isSuspended()) {
+ logger.info("{} is suspended, deleting", actor.getId());
+ applicationEventPublisher.publishEvent(new DeleteUserEvent(this, actor.getId()));
}
return user;
} else {
return AnonymousUser.INSTANCE;
}
} catch (NoSuchAlgorithmException | SignatureException | MissingRequiredHeaderException | IOException e) {
- logger.warn("Invalid signature {}", signatureString);
+ logger.warn("Invalid signature {}: {}", signatureString, e.getMessage());
}
} else {
logger.warn("Unknown keyId");
}
return AnonymousUser.INSTANCE;
}
+
public Optional<Context> getContext(URI contextUri) {
try {
Context context = apClient.getForEntity(contextUri, Context.class).getBody();
@@ -166,17 +171,16 @@ public class SignatureManager {
}
return Optional.empty();
}
+
public Optional<Context> discoverPerson(String acct) {
Jid acctId = Jid.of(acct);
- URI resourceUri = UriComponentsBuilder.fromPath("/.well-known/webfinger")
- .host(acctId.getDomain())
- .scheme("https")
- .queryParam("resource", String.format("%s", acctId.toEscapedString())).build().toUri();
+ URI resourceUri = UriComponentsBuilder.fromPath("/.well-known/webfinger").host(acctId.getDomain())
+ .scheme("https").queryParam("resource", String.format("%s", acctId.toEscapedString())).build().toUri();
HttpHeaders headers = new HttpHeaders();
headers.add("Accept", "application/jrd+json");
HttpEntity<Void> webfingerRequest = new HttpEntity<>(headers);
- ResponseEntity<Account> response = apClient.exchange(
- resourceUri, HttpMethod.GET, webfingerRequest, Account.class);
+ ResponseEntity<Account> response = apClient.exchange(resourceUri, HttpMethod.GET, webfingerRequest,
+ Account.class);
if (response.getStatusCode().is2xxSuccessful()) {
Account acctData = response.getBody();
if (acctData != null) {
diff --git a/src/main/java/com/juick/www/api/activity/Profile.java b/src/main/java/com/juick/www/api/activity/Profile.java
index 724e0747..cf5fb843 100644
--- a/src/main/java/com/juick/www/api/activity/Profile.java
+++ b/src/main/java/com/juick/www/api/activity/Profile.java
@@ -376,7 +376,7 @@ public class Profile {
if (activity instanceof Delete) {
// Delete gone user
// TODO: check if it is really deleted and remove copy-paste
- if (activity.getActor().equals(activity.getObject().getUrl())) {
+ if (activity.getActor().equals(activity.getObject().getId())) {
return new ResponseEntity<>(CommandResult.fromString("Delete request accepted"),
HttpStatus.ACCEPTED);
}
diff --git a/src/main/java/com/juick/www/api/activity/model/objects/Actor.java b/src/main/java/com/juick/www/api/activity/model/objects/Actor.java
index f8bd63e0..7e799a95 100644
--- a/src/main/java/com/juick/www/api/activity/model/objects/Actor.java
+++ b/src/main/java/com/juick/www/api/activity/model/objects/Actor.java
@@ -23,6 +23,8 @@ import com.juick.www.api.activity.model.Context;
public class Actor extends Context {
private String preferredUsername;
+ private boolean suspended;
+
private String inbox;
private String outbox;
private String following;
@@ -78,4 +80,12 @@ public class Actor extends Context {
public void setPreferredUsername(String preferredUsername) {
this.preferredUsername = preferredUsername;
}
+
+ public boolean isSuspended() {
+ return suspended;
+ }
+
+ public void setSuspended(boolean suspended) {
+ this.suspended = suspended;
+ }
}
diff --git a/src/test/java/com/juick/server/MockDeleteListener.java b/src/test/java/com/juick/server/MockDeleteListener.java
new file mode 100644
index 00000000..d060d865
--- /dev/null
+++ b/src/test/java/com/juick/server/MockDeleteListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2008-2021, Juick
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package com.juick.server;
+
+import com.juick.service.activities.DeleteUserEvent;
+import org.springframework.context.ApplicationListener;
+
+import javax.annotation.Nonnull;
+
+public class MockDeleteListener implements ApplicationListener<DeleteUserEvent> {
+ @Override
+ public void onApplicationEvent(@Nonnull DeleteUserEvent event) {
+
+ }
+}
diff --git a/src/test/java/com/juick/server/tests/ServerTests.java b/src/test/java/com/juick/server/tests/ServerTests.java
index 2f60f5be..db1449f2 100644
--- a/src/test/java/com/juick/server/tests/ServerTests.java
+++ b/src/test/java/com/juick/server/tests/ServerTests.java
@@ -48,6 +48,7 @@ import com.juick.www.api.webfinger.model.Account;
import com.juick.www.api.xnodeinfo2.model.NodeInfo;
import com.juick.www.WebApp;
import com.juick.service.*;
+import com.juick.service.activities.DeleteUserEvent;
import com.juick.service.activities.UpdateEvent;
import com.juick.service.component.SystemEvent;
import com.juick.test.util.MockUtils;
@@ -229,6 +230,10 @@ public class ServerTests {
private Resource testappResponse;
@Value("classpath:snapshots/activity/testfollow.json")
private Resource testfollowRequest;
+ @Value("classpath:snapshots/activity/testdelete.json")
+ private Resource testDeleteRequest;
+ @Value("classpath:snapshots/activity/test_suspended_user.json")
+ private Resource testSuspendedUserResponse;
@Value("classpath:snapshots/email/subscription.html")
private Resource testSubscriptionHtmlEmail;
@Value("classpath:snapshots/email/private.html")
@@ -2077,7 +2082,7 @@ public class ServerTests {
}
@Test
- public void federatedUserDeletionFlow() throws Exception {
+ public void federatedUserDeletionFlowWhenItIsGone() throws Exception {
String deleteJsonStr = IOUtils.toString(new ClassPathResource("delete_user.json").getURI(),
StandardCharsets.UTF_8);
Delete delete = jsonMapper.readValue(deleteJsonStr, Delete.class);
@@ -2096,6 +2101,39 @@ public class ServerTests {
apClient.setRequestFactory(originalRequestFactory);
}
+ @MockBean
+ private MockDeleteListener deleteListener;
+ @Captor
+ protected ArgumentCaptor<DeleteUserEvent> deleteEventCaptor;
+
+ @Test
+ public void federatedUserDeletionFlowWhenItIsSuspended() throws Exception {
+ String deleteJsonStr = IOUtils.toString(testDeleteRequest.getInputStream(), StandardCharsets.UTF_8);
+ Delete delete = jsonMapper.readValue(deleteJsonStr, Delete.class);
+ ClientHttpRequestFactory originalRequestFactory = apClient.getRequestFactory();
+ MockRestServiceServer restServiceServer = MockRestServiceServer.createServer(apClient);
+ restServiceServer.expect(times(2), requestTo(delete.getObject().getId()))
+ .andRespond(withSuccess(IOUtils.toString(testSuspendedUserResponse.getInputStream(), StandardCharsets.UTF_8), MediaType.APPLICATION_JSON));
+ Person testuser = (Person) signatureManager.getContext(URI.create(delete.getObject().getId())).get();
+ Instant now = Instant.now();
+ String testRequestDate = DateFormattersHolder.getHttpDateFormatter().format(now);
+ String inboxUri = "/api/inbox";
+ byte[] digest = MessageDigest.getInstance("SHA-256").digest(deleteJsonStr.getBytes());
+ String digestHeader = "SHA-256=" + new String(Base64.encodeBase64(digest));
+ String testSignatureString = signatureManager.addSignature(testuser, "localhost", "POST", inboxUri,
+ testRequestDate, digestHeader, testKeystoreManager);
+ mockMvc.perform(post(inboxUri).contentType(ACTIVITY_MEDIA_TYPE).content(deleteJsonStr)
+ .header("Host", "localhost")
+ .header("Date", testRequestDate)
+ .header("Digest", digestHeader)
+ .header("Signature", testSignatureString))
+ .andExpect(status().isAccepted());
+ apClient.setRequestFactory(originalRequestFactory);
+ Mockito.verify(deleteListener, Mockito.times(1)).onApplicationEvent(deleteEventCaptor.capture());
+ DeleteUserEvent receivedEvent = deleteEventCaptor.getValue();
+ assertThat(receivedEvent.getUserUri(), is(testuser.getId()));
+ }
+
@Test
@Order(2)
public void handleIncorrectCertificates() throws Exception {
diff --git a/src/test/resources/snapshots/activity/test_suspended_user.json b/src/test/resources/snapshots/activity/test_suspended_user.json
new file mode 100644
index 00000000..b0d8e97b
--- /dev/null
+++ b/src/test/resources/snapshots/activity/test_suspended_user.json
@@ -0,0 +1,47 @@
+{
+ "@context": [
+ "https://www.w3.org/ns/activitystreams",
+ "https://w3id.org/security/v1",
+ {
+ "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+ "toot": "http://joinmastodon.org/ns#",
+ "featured": { "@id": "toot:featured", "@type": "@id" },
+ "featuredTags": { "@id": "toot:featuredTags", "@type": "@id" },
+ "alsoKnownAs": { "@id": "as:alsoKnownAs", "@type": "@id" },
+ "movedTo": { "@id": "as:movedTo", "@type": "@id" },
+ "schema": "http://schema.org#",
+ "PropertyValue": "schema:PropertyValue",
+ "value": "schema:value",
+ "IdentityProof": "toot:IdentityProof",
+ "discoverable": "toot:discoverable",
+ "Device": "toot:Device",
+ "Ed25519Signature": "toot:Ed25519Signature",
+ "Ed25519Key": "toot:Ed25519Key",
+ "Curve25519Key": "toot:Curve25519Key",
+ "EncryptedMessage": "toot:EncryptedMessage",
+ "publicKeyBase64": "toot:publicKeyBase64",
+ "deviceId": "toot:deviceId",
+ "claim": { "@type": "@id", "@id": "toot:claim" },
+ "fingerprintKey": { "@type": "@id", "@id": "toot:fingerprintKey" },
+ "identityKey": { "@type": "@id", "@id": "toot:identityKey" },
+ "devices": { "@type": "@id", "@id": "toot:devices" },
+ "messageFranking": "toot:messageFranking",
+ "messageType": "toot:messageType",
+ "cipherText": "toot:cipherText",
+ "suspended": "toot:suspended"
+ }
+ ],
+ "id": "https://example.com/u/testuser",
+ "type": "Person",
+ "url": "https://example.com/u/testuser",
+ "manuallyApprovesFollowers": false,
+ "discoverable": false,
+ "suspended": true,
+ "publicKey": {
+ "id": "https://example.com/u/testuser#main-key",
+ "owner": "https://example.com/u/testuser",
+ "publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiHKRdKFFeT4P/MVlNbxC\nbbgXOkEdeQzvJB/wAJgSYbUwm9SzNFzttePQXk3/MWoK2awWUInZTduVHsWt8zU7\nO3d9PAW6YH6L1oDkjgMLAb9aUWV2ClQWMwsn88WKK9Rb1WOmd8BrXjPfmeFK2ypQ\n9eg8aKpH36WAXiiaTDfBupBZ0Ki2+E87BrWxpbUeDC1dkV+zbl8BMm7X0rp+reoC\nYUWMcjQMzhMmQOXUd4zwJIDPZDMdF4beq/y6WPSUTVgjs4kPDS1HT60ATnsUqyPE\n6tuGxG4j0msb4TTre87PKxMU5YPOxSiqNL0O/3u9/2shVPpjDa/uy9W+VaeBHbFm\nSQIDAQAB\n-----END PUBLIC KEY-----\n"
+ },
+ "tag": [],
+ "attachment": []
+}
diff --git a/src/test/resources/snapshots/activity/testdelete.json b/src/test/resources/snapshots/activity/testdelete.json
new file mode 100644
index 00000000..d7e7af23
--- /dev/null
+++ b/src/test/resources/snapshots/activity/testdelete.json
@@ -0,0 +1,8 @@
+{
+ "@context": "https://www.w3.org/ns/activitystreams",
+ "id": "https://example.com/u/testuser#delete",
+ "type": "Delete",
+ "actor": "https://example.com/u/testuser",
+ "to": ["https://www.w3.org/ns/activitystreams#Public"],
+ "object": "https://example.com/u/testuser"
+}