diff --git a/build.gradle b/build.gradle
index a041299..f989cb4 100644
--- a/build.gradle
+++ b/build.gradle
@@ -114,6 +114,7 @@ dependencies {
compile 'org.xerial:sqlite-jdbc:3.20.0'
testCompile 'junit:junit:4.12'
+ testCompile 'com.github.tomakehurst:wiremock:2.8.0'
}
springBoot {
diff --git a/docs/backends/rest.md b/docs/backends/rest.md
new file mode 100644
index 0000000..9ff9c05
--- /dev/null
+++ b/docs/backends/rest.md
@@ -0,0 +1,169 @@
+# REST backend
+The REST backend allows you to query arbitrary REST JSON endpoints as backends for the following flows:
+- Identity lookup
+- Authentication
+
+## Configuration
+| Key | Default | Description |
+---------------------------------|---------------------------------------|------------------------------------------------------|
+| rest.enabled | false | Globally enable/disable the REST backend |
+| rest.host | *empty* | Default base URL to use for the different endpoints. |
+| rest.endpoints.auth | /_mxisd/identity/api/v1/auth | Endpoint to validate credentials |
+| rest.endpoints.identity.single | /_mxisd/identity/api/v1/lookup/single | Endpoint to lookup a single 3PID |
+| rest.endpoints.identity.bulk | /_mxisd/identity/api/v1/lookup/bulk | Endpoint to lookup a list of 3PID |
+
+Endpoint values can handle two formats:
+- URL Path starting with `/` that gets happened to the `rest.host`
+- Full URL, if you want each endpoint to go to a specific server/protocol/port
+
+`rest.host` is only mandatory if at least one endpoint is not a full URL.
+
+## Endpoints
+### Authenticate
+Configured with `rest.endpoints.auth`
+
+HTTP method: `POST`
+Encoding: JSON UTF-8
+
+#### Request Body
+```
+{
+ "auth": {
+ "mxid": "@john.doe:example.org",
+ "localpart": "john.doe",
+ "domain": "example.org",
+ "password": "passwordOfTheUser"
+ }
+}
+```
+
+#### Response Body
+If the authentication fails:
+```
+{
+ "auth": {
+ "success": false
+ }
+}
+```
+
+If the authentication succeed:
+- `auth.id` supported values: `localpart`, `mxid`
+- `auth.profile` and any sub-member are all optional
+```
+{
+ "auth": {
+ "success": true,
+ "id": {
+ "type": "localpart",
+ "value": "john"
+ },
+ "profile": {
+ "display_name": "John Doe",
+ "three_pids": [
+ {
+ "medium": "email",
+ "address": "john.doe@example.org"
+ },
+ {
+ "medium": "msisdn",
+ "address": "123456789"
+ }
+ ]
+ }
+ }
+}
+```
+
+### Lookup
+#### Single
+Configured with `rest.endpoints.identity.single`
+
+HTTP method: `POST`
+Encoding: JSON UTF-8
+
+#### Request Body
+```
+{
+ "lookup": {
+ "medium": "email",
+ "address": "john.doe@example.org"
+ }
+}
+```
+
+#### Response Body
+If a match was found:
+- `lookup.id.type` supported values: `localpart`, `mxid`
+```
+{
+ "lookup": {
+ "medium": "email",
+ "address": "john.doe@example.org",
+ "id": {
+ "type": "mxid",
+ "value": "@john:example.org"
+ }
+ }
+}
+```
+
+If no match was found:
+```
+{}
+```
+
+#### Bulk
+Configured with `rest.endpoints.identity.bulk`
+
+HTTP method: `POST`
+Encoding: JSON UTF-8
+
+#### Request Body
+```
+{
+ "lookup": [
+ {
+ "medium": "email",
+ "address": "john.doe@example.org"
+ },
+ {
+ "medium": "msisdn",
+ "address": "123456789"
+ }
+ ]
+}
+```
+
+#### Response Body
+For all entries where a match was found:
+- `lookup[].id.type` supported values: `localpart`, `mxid`
+```
+{
+ "lookup": [
+ {
+ "medium": "email",
+ "address": "john.doe@example.org",
+ "id": {
+ "type": "localpart",
+ "value": "john"
+ }
+ },
+ {
+ "medium": "msisdn",
+ "address": "123456789",
+ "id": {
+ "type": "mxid",
+ "value": "@jane:example.org"
+ }
+ }
+ ]
+}
+```
+
+If no match was found:
+```
+{
+ "lookup": []
+}
+```
\ No newline at end of file
diff --git a/src/main/groovy/io/kamax/mxisd/UserID.java b/src/main/groovy/io/kamax/mxisd/UserID.java
new file mode 100644
index 0000000..5cf683a
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/UserID.java
@@ -0,0 +1,46 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd;
+
+// FIXME consider integrating in matrix-java-sdk?
+public class UserID {
+
+ private String type;
+ private String value;
+
+ protected UserID() {
+ // stub for (de)serialization
+ }
+
+ public UserID(String type, String value) {
+ this.type = type;
+ this.value = value;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/UserIdType.java b/src/main/groovy/io/kamax/mxisd/UserIdType.java
new file mode 100644
index 0000000..e0625bf
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/UserIdType.java
@@ -0,0 +1,47 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd;
+
+import org.apache.commons.lang.StringUtils;
+
+// FIXME consider integrating in matrix-java-sdk?
+public enum UserIdType {
+
+ Localpart("localpart"),
+ MatrixID("mxid"),
+ EmailLocalpart("email_localpart"),
+ Email("email");
+
+ private String id;
+
+ UserIdType(String id) {
+ this.id = id;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public boolean is(String id) {
+ return StringUtils.equalsIgnoreCase(this.id, id);
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java
index 6dbfff2..8ad8edf 100644
--- a/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java
+++ b/src/main/groovy/io/kamax/mxisd/auth/AuthManager.java
@@ -20,8 +20,13 @@
package io.kamax.mxisd.auth;
+import io.kamax.matrix.MatrixID;
+import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.ThreePid;
+import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
+import io.kamax.mxisd.auth.provider.BackendAuthResult;
+import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.ThreePidMapping;
import org.slf4j.Logger;
@@ -40,26 +45,45 @@ public class AuthManager {
@Autowired
private List providers = new ArrayList<>();
+ @Autowired
+ private MatrixConfig mxCfg;
+
@Autowired
private InvitationManager invMgr;
public UserAuthResult authenticate(String id, String password) {
+ _MatrixID mxid = new MatrixID(id);
for (AuthenticatorProvider provider : providers) {
if (!provider.isEnabled()) {
continue;
}
- UserAuthResult result = provider.authenticate(id, password);
+ BackendAuthResult result = provider.authenticate(mxid, password);
if (result.isSuccess()) {
+
+ String mxId;
+ if (UserIdType.Localpart.is(result.getId().getType())) {
+ mxId = new MatrixID(result.getId().getValue(), mxCfg.getDomain()).getId();
+ } else if (UserIdType.MatrixID.is(result.getId().getType())) {
+ mxId = new MatrixID(result.getId().getValue()).getId();
+ } else {
+ log.warn("Unsupported User ID type {} for backend {}", result.getId().getType(), provider.getClass().getSimpleName());
+ continue;
+ }
+
+ UserAuthResult authResult = new UserAuthResult().success(mxId, result.getProfile().getDisplayName());
+ for (ThreePid pid : result.getProfile().getThreePids()) {
+ authResult.withThreePid(pid.getMedium(), pid.getAddress());
+ }
log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName());
- for (ThreePid pid : result.getThreePids()) {
+ for (ThreePid pid : authResult.getThreePids()) {
log.info("Processing {} for {}", pid, id);
- invMgr.publishMappingIfInvited(new ThreePidMapping(pid, result.getMxid()));
+ invMgr.publishMappingIfInvited(new ThreePidMapping(pid, authResult.getMxid()));
}
invMgr.lookupMappingsForInvites();
- return result;
+ return authResult;
}
}
diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java b/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java
index f9aefb6..c3c58bc 100644
--- a/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java
+++ b/src/main/groovy/io/kamax/mxisd/auth/provider/AuthenticatorProvider.java
@@ -20,12 +20,12 @@
package io.kamax.mxisd.auth.provider;
-import io.kamax.mxisd.auth.UserAuthResult;
+import io.kamax.matrix._MatrixID;
public interface AuthenticatorProvider {
boolean isEnabled();
- UserAuthResult authenticate(String id, String password);
+ BackendAuthResult authenticate(_MatrixID mxid, String password);
}
diff --git a/src/main/groovy/io/kamax/mxisd/auth/provider/BackendAuthResult.java b/src/main/groovy/io/kamax/mxisd/auth/provider/BackendAuthResult.java
new file mode 100644
index 0000000..79c1036
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/auth/provider/BackendAuthResult.java
@@ -0,0 +1,88 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.auth.provider;
+
+import io.kamax.mxisd.ThreePid;
+import io.kamax.mxisd.UserID;
+import io.kamax.mxisd.UserIdType;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BackendAuthResult {
+
+ public static class BackendAuthProfile {
+
+ private String displayName;
+ private List threePids = new ArrayList<>();
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public List getThreePids() {
+ return threePids;
+ }
+ }
+
+ public static BackendAuthResult failure() {
+ BackendAuthResult r = new BackendAuthResult();
+ r.success = false;
+ return r;
+ }
+
+ public static BackendAuthResult success(String id, UserIdType type, String displayName) {
+ return success(id, type.getId(), displayName);
+ }
+
+ public static BackendAuthResult success(String id, String type, String displayName) {
+ BackendAuthResult r = new BackendAuthResult();
+ r.success = true;
+ r.id = new UserID(type, id);
+ r.profile = new BackendAuthProfile();
+ r.profile.displayName = displayName;
+
+ return r;
+ }
+
+ private Boolean success;
+ private UserID id;
+ private BackendAuthProfile profile = new BackendAuthProfile();
+
+ public Boolean isSuccess() {
+ return success;
+ }
+
+ public UserID getId() {
+ return id;
+ }
+
+ public BackendAuthProfile getProfile() {
+ return profile;
+ }
+
+ public BackendAuthResult withThreePid(ThreePid threePid) {
+ this.profile.threePids.add(threePid);
+
+ return this;
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/firebase/GoogleFirebaseAuthenticator.groovy b/src/main/groovy/io/kamax/mxisd/backend/firebase/GoogleFirebaseAuthenticator.groovy
index ea1d843..4533f3a 100644
--- a/src/main/groovy/io/kamax/mxisd/backend/firebase/GoogleFirebaseAuthenticator.groovy
+++ b/src/main/groovy/io/kamax/mxisd/backend/firebase/GoogleFirebaseAuthenticator.groovy
@@ -27,15 +27,17 @@ import com.google.firebase.internal.NonNull
import com.google.firebase.tasks.OnFailureListener
import com.google.firebase.tasks.OnSuccessListener
import io.kamax.matrix.ThreePidMedium
-import io.kamax.mxisd.auth.UserAuthResult
+import io.kamax.matrix._MatrixID
+import io.kamax.mxisd.ThreePid
+import io.kamax.mxisd.UserIdType
import io.kamax.mxisd.auth.provider.AuthenticatorProvider
+import io.kamax.mxisd.auth.provider.BackendAuthResult
import org.apache.commons.lang.StringUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
-import java.util.regex.Matcher
import java.util.regex.Pattern
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
@@ -49,7 +51,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private FirebaseApp fbApp;
private FirebaseAuth fbAuth;
- private void waitOnLatch(UserAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) {
+ private void waitOnLatch(BackendAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) {
try {
l.await(timeout, unit);
} catch (InterruptedException e) {
@@ -108,22 +110,16 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
}
@Override
- public UserAuthResult authenticate(String id, String password) {
+ public BackendAuthResult authenticate(_MatrixID mxid, String password) {
if (!isEnabled()) {
throw new IllegalStateException();
}
- final UserAuthResult result = new UserAuthResult();
+ log.info("Trying to authenticate {}", mxid);
- log.info("Trying to authenticate {}", id);
- Matcher m = matrixIdLaxPattern.matcher(id);
- if (!m.matches()) {
- log.warn("Could not validate {} as a Matrix ID", id);
- result.failure();
- }
+ BackendAuthResult result = BackendAuthResult.failure();
String localpart = m.group(1);
-
CountDownLatch l = new CountDownLatch(1);
fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener() {
@Override
@@ -131,26 +127,26 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
try {
if (!StringUtils.equals(localpart, token.getUid())) {
log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, localpart, token.getUid());
- result.failure();
+ result = BackendAuthResult.failure();
return;
}
- log.info("{} was successfully authenticated", id);
- result.success(id, token.getName());
-
- log.info("Fetching profile for {}", id);
+ result = BackendAuthResult.success(mxid.getId(), UserIdType.MatrixID, token.getName());
+ log.info("{} was successfully authenticated", mxid);
+ log.info("Fetching profile for {}", mxid);
CountDownLatch userRecordLatch = new CountDownLatch(1);
fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener() {
@Override
void onSuccess(UserRecord user) {
try {
if (StringUtils.isNotBlank(user.getEmail())) {
- result.withThreePid(ThreePidMedium.Email, user.getEmail());
+ result.withThreePid(new ThreePid(ThreePidMedium.Email.getId(), user.getEmail()));
}
if (StringUtils.isNotBlank(user.getPhoneNumber())) {
- result.withThreePid(ThreePidMedium.PhoneNumber, user.getPhoneNumber());
+ result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber()));
}
+
} finally {
userRecordLatch.countDown();
}
@@ -159,8 +155,8 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
@Override
void onFailure(@NonNull Exception e) {
try {
- log.warn("Unable to fetch Firebase user profile for {}", id);
- result.failure();
+ log.warn("Unable to fetch Firebase user profile for {}", mxid);
+ result = BackendAuthResult.failure();
} finally {
userRecordLatch.countDown();
}
@@ -177,13 +173,13 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
void onFailure(@NonNull Exception e) {
try {
if (e instanceof IllegalArgumentException) {
- log.info("Failure to authenticate {}: invalid firebase token", id);
+ log.info("Failure to authenticate {}: invalid firebase token", mxid);
} else {
log.info("Failure to authenticate {}: {}", id, e.getMessage(), e);
log.info("Exception", e);
}
- result.failure();
+ result = BackendAuthResult.failure();
} finally {
l.countDown()
}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/ldap/LdapAuthProvider.java b/src/main/groovy/io/kamax/mxisd/backend/ldap/LdapAuthProvider.java
index 0717d20..cf25663 100644
--- a/src/main/groovy/io/kamax/mxisd/backend/ldap/LdapAuthProvider.java
+++ b/src/main/groovy/io/kamax/mxisd/backend/ldap/LdapAuthProvider.java
@@ -20,9 +20,10 @@
package io.kamax.mxisd.backend.ldap;
-import io.kamax.matrix.MatrixID;
-import io.kamax.mxisd.auth.UserAuthResult;
+import io.kamax.matrix._MatrixID;
+import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
+import io.kamax.mxisd.auth.provider.BackendAuthResult;
import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException;
@@ -53,16 +54,15 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato
}
@Override
- public UserAuthResult authenticate(String id, String password) {
- log.info("Performing auth for {}", id);
+ public BackendAuthResult authenticate(_MatrixID mxid, String password) {
+ log.info("Performing auth for {}", mxid);
LdapConnection conn = getConn();
try {
bind(conn);
String uidType = getCfg().getAttribute().getUid().getType();
- MatrixID mxIdExt = new MatrixID(id);
- String userFilterValue = StringUtils.equals(LdapThreePidProvider.UID, uidType) ? mxIdExt.getLocalPart() : mxIdExt.getId();
+ String userFilterValue = StringUtils.equals(LdapThreePidProvider.UID, uidType) ? mxid.getLocalPart() : mxid.getId();
String userFilter = "(" + getCfg().getAttribute().getUid().getValue() + "=" + userFilterValue + ")";
if (!StringUtils.isBlank(getCfg().getAuth().getFilter())) {
userFilter = "(&" + getCfg().getAuth().getFilter() + userFilter + ")";
@@ -91,7 +91,7 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato
conn.bind(entry.getDn(), password);
} catch (LdapException e) {
log.info("Unable to bind using {} because {}", entry.getDn().getName(), e.getMessage());
- return new UserAuthResult().failure();
+ return BackendAuthResult.failure();
}
Attribute nameAttribute = entry.get(getCfg().getAttribute().getName());
@@ -100,16 +100,17 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato
log.info("Authentication successful for {}", entry.getDn().getName());
log.info("DN {} is a valid match", dn);
- return new UserAuthResult().success(mxIdExt.getId(), name);
+ // TODO should we canonicalize the MXID?
+ return BackendAuthResult.success(mxid.getId(), UserIdType.MatrixID, name);
}
} catch (CursorLdapReferralException e) {
- log.warn("Entity for {} is only available via referral, skipping", mxIdExt);
+ log.warn("Entity for {} is only available via referral, skipping", mxid);
} finally {
cursor.close();
}
- log.info("No match were found for {}", id);
- return new UserAuthResult().failure();
+ log.info("No match were found for {}", mxid);
+ return BackendAuthResult.failure();
} catch (LdapException | IOException | CursorException e) {
throw new RuntimeException(e);
} finally {
diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/LookupBulkResponseJson.java b/src/main/groovy/io/kamax/mxisd/backend/rest/LookupBulkResponseJson.java
new file mode 100644
index 0000000..fb0797c
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/backend/rest/LookupBulkResponseJson.java
@@ -0,0 +1,34 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.backend.rest;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class LookupBulkResponseJson {
+
+ private List lookup = new ArrayList<>();
+
+ public List getLookup() {
+ return lookup;
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/LookupSingleRequestJson.java b/src/main/groovy/io/kamax/mxisd/backend/rest/LookupSingleRequestJson.java
new file mode 100644
index 0000000..618f5b3
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/backend/rest/LookupSingleRequestJson.java
@@ -0,0 +1,40 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.backend.rest;
+
+public class LookupSingleRequestJson {
+
+ private String medium;
+ private String address;
+
+ public LookupSingleRequestJson(String medium, String address) {
+ this.medium = medium;
+ this.address = address;
+ }
+
+ public String getMedium() {
+ return medium;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/LookupSingleResponseJson.java b/src/main/groovy/io/kamax/mxisd/backend/rest/LookupSingleResponseJson.java
new file mode 100644
index 0000000..7bc9246
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/backend/rest/LookupSingleResponseJson.java
@@ -0,0 +1,43 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.backend.rest;
+
+import io.kamax.mxisd.UserID;
+
+public class LookupSingleResponseJson {
+
+ private String medium;
+ private String address;
+ private UserID id;
+
+ public String getMedium() {
+ return medium;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public UserID getId() {
+ return id;
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthProvider.java b/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthProvider.java
new file mode 100644
index 0000000..0191c2e
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthProvider.java
@@ -0,0 +1,69 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.backend.rest;
+
+import io.kamax.matrix._MatrixID;
+import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
+import io.kamax.mxisd.auth.provider.BackendAuthResult;
+import io.kamax.mxisd.config.rest.RestBackendConfig;
+import io.kamax.mxisd.util.RestClientUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+public class RestAuthProvider extends RestProvider implements AuthenticatorProvider {
+
+ @Autowired
+ public RestAuthProvider(RestBackendConfig cfg) {
+ super(cfg);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return cfg.isEnabled();
+ }
+
+ @Override
+ public BackendAuthResult authenticate(_MatrixID mxid, String password) {
+ RestAuthRequestJson auth = new RestAuthRequestJson();
+ auth.setMxid(mxid.getId());
+ auth.setLocalpart(mxid.getLocalPart());
+ auth.setDomain(mxid.getDomain());
+ auth.setPassword(password);
+
+ HttpUriRequest req = RestClientUtils.post(cfg.getEndpoints().getAuth(), gson, "auth", auth);
+ try (CloseableHttpResponse res = client.execute(req)) {
+ int status = res.getStatusLine().getStatusCode();
+ if (status < 200 || status >= 300) {
+ return BackendAuthResult.failure();
+ }
+
+ return parser.parse(res, "auth", BackendAuthResult.class);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthRequestJson.java b/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthRequestJson.java
new file mode 100644
index 0000000..92d0a5a
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/backend/rest/RestAuthRequestJson.java
@@ -0,0 +1,62 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.backend.rest;
+
+public class RestAuthRequestJson {
+
+ private String mxid;
+ private String localpart;
+ private String domain;
+ private String password;
+
+ public String getMxid() {
+ return mxid;
+ }
+
+ public void setMxid(String mxid) {
+ this.mxid = mxid;
+ }
+
+ public String getLocalpart() {
+ return localpart;
+ }
+
+ public void setLocalpart(String localpart) {
+ this.localpart = localpart;
+ }
+
+ public String getDomain() {
+ return domain;
+ }
+
+ public void setDomain(String domain) {
+ this.domain = domain;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/RestProvider.java b/src/main/groovy/io/kamax/mxisd/backend/rest/RestProvider.java
new file mode 100644
index 0000000..1c34438
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/backend/rest/RestProvider.java
@@ -0,0 +1,46 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.backend.rest;
+
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import io.kamax.mxisd.config.rest.RestBackendConfig;
+import io.kamax.mxisd.util.GsonParser;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+
+public class RestProvider {
+
+ protected RestBackendConfig cfg;
+ protected Gson gson;
+ protected GsonParser parser;
+ protected CloseableHttpClient client;
+
+ public RestProvider(RestBackendConfig cfg) {
+ this.cfg = cfg;
+
+ client = HttpClients.createDefault();
+ gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
+ parser = new GsonParser(gson);
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/rest/RestThreePidProvider.java b/src/main/groovy/io/kamax/mxisd/backend/rest/RestThreePidProvider.java
new file mode 100644
index 0000000..0434396
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/backend/rest/RestThreePidProvider.java
@@ -0,0 +1,131 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.backend.rest;
+
+import io.kamax.matrix.MatrixID;
+import io.kamax.matrix._MatrixID;
+import io.kamax.mxisd.UserID;
+import io.kamax.mxisd.UserIdType;
+import io.kamax.mxisd.config.MatrixConfig;
+import io.kamax.mxisd.config.rest.RestBackendConfig;
+import io.kamax.mxisd.lookup.SingleLookupReply;
+import io.kamax.mxisd.lookup.SingleLookupRequest;
+import io.kamax.mxisd.lookup.ThreePidMapping;
+import io.kamax.mxisd.lookup.provider.IThreePidProvider;
+import io.kamax.mxisd.util.RestClientUtils;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpUriRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+@Component
+public class RestThreePidProvider extends RestProvider implements IThreePidProvider {
+
+ private Logger log = LoggerFactory.getLogger(RestThreePidProvider.class);
+
+ private MatrixConfig mxCfg; // FIXME should be done in the lookup manager
+
+ @Autowired
+ public RestThreePidProvider(RestBackendConfig cfg, MatrixConfig mxCfg) {
+ super(cfg);
+ this.mxCfg = mxCfg;
+ }
+
+ // TODO refactor in lookup manager with above FIXME
+ private _MatrixID getMxId(UserID id) {
+ if (UserIdType.Localpart.is(id.getType())) {
+ return new MatrixID(id.getValue(), mxCfg.getDomain());
+ } else {
+ return new MatrixID(id.getValue());
+ }
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return cfg.isEnabled();
+ }
+
+ @Override
+ public boolean isLocal() {
+ return true;
+ }
+
+ @Override
+ public int getPriority() {
+ return 20;
+ }
+
+ // TODO refactor common code
+ @Override
+ public Optional find(SingleLookupRequest request) {
+ String endpoint = cfg.getEndpoints().getIdentity().getSingle();
+ HttpUriRequest req = RestClientUtils.post(endpoint, gson, "lookup",
+ new LookupSingleRequestJson(request.getType(), request.getThreePid()));
+
+ try (CloseableHttpResponse res = client.execute(req)) {
+ int status = res.getStatusLine().getStatusCode();
+ if (status < 200 || status >= 300) {
+ log.warn("REST endpoint {} answered with status {}, no binding found", endpoint, status);
+ return Optional.empty();
+ }
+
+ Optional responseOpt = parser.parseOptional(res, "lookup", LookupSingleResponseJson.class);
+ return responseOpt.map(lookupSingleResponseJson -> new SingleLookupReply(request, getMxId(lookupSingleResponseJson.getId())));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // TODO refactor common code
+ @Override
+ public List populate(List mappings) {
+ List ioListRequest = mappings.stream()
+ .map(mapping -> new LookupSingleRequestJson(mapping.getMedium(), mapping.getValue()))
+ .collect(Collectors.toList());
+
+ HttpUriRequest req = RestClientUtils.post(
+ cfg.getEndpoints().getIdentity().getBulk(), gson, "lookup", ioListRequest);
+ try (CloseableHttpResponse res = client.execute(req)) {
+ mappings = new ArrayList<>();
+
+ int status = res.getStatusLine().getStatusCode();
+ if (status < 200 || status >= 300) {
+ return mappings;
+ }
+
+ LookupBulkResponseJson listIo = parser.parse(res, LookupBulkResponseJson.class);
+ return listIo.getLookup().stream()
+ .map(io -> new ThreePidMapping(io.getMedium(), io.getAddress(), getMxId(io.getId()).getId()))
+ .collect(Collectors.toList());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/backend/sql/SqlAuthProvider.java b/src/main/groovy/io/kamax/mxisd/backend/sql/SqlAuthProvider.java
index dd9e6ee..089f4ee 100644
--- a/src/main/groovy/io/kamax/mxisd/backend/sql/SqlAuthProvider.java
+++ b/src/main/groovy/io/kamax/mxisd/backend/sql/SqlAuthProvider.java
@@ -20,8 +20,9 @@
package io.kamax.mxisd.backend.sql;
-import io.kamax.mxisd.auth.UserAuthResult;
+import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
+import io.kamax.mxisd.auth.provider.BackendAuthResult;
import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.sql.SqlProviderConfig;
import io.kamax.mxisd.invitation.InvitationManager;
@@ -50,11 +51,11 @@ public class SqlAuthProvider implements AuthenticatorProvider {
}
@Override
- public UserAuthResult authenticate(String id, String password) {
+ public BackendAuthResult authenticate(_MatrixID mxid, String password) {
log.info("Performing dummy authentication try to force invite mapping refresh");
invMgr.lookupMappingsForInvites();
- return new UserAuthResult().failure();
+ return BackendAuthResult.failure();
}
}
diff --git a/src/main/groovy/io/kamax/mxisd/config/MatrixConfig.java b/src/main/groovy/io/kamax/mxisd/config/MatrixConfig.java
index c6cfd2d..f5acfb9 100644
--- a/src/main/groovy/io/kamax/mxisd/config/MatrixConfig.java
+++ b/src/main/groovy/io/kamax/mxisd/config/MatrixConfig.java
@@ -46,7 +46,7 @@ public class MatrixConfig {
}
@PostConstruct
- private void postConstruct() {
+ public void build() {
log.info("--- Matrix config ---");
if (StringUtils.isBlank(domain)) {
diff --git a/src/main/groovy/io/kamax/mxisd/config/rest/RestBackendConfig.java b/src/main/groovy/io/kamax/mxisd/config/rest/RestBackendConfig.java
new file mode 100644
index 0000000..d9a4295
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/config/rest/RestBackendConfig.java
@@ -0,0 +1,149 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.config.rest;
+
+import io.kamax.mxisd.exception.ConfigurationException;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+import javax.annotation.PostConstruct;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+@Configuration
+@ConfigurationProperties("rest")
+public class RestBackendConfig {
+
+ public static class IdentityEndpoints {
+
+ private String single;
+ private String bulk;
+
+ public String getSingle() {
+ return single;
+ }
+
+ public void setSingle(String single) {
+ this.single = single;
+ }
+
+ public String getBulk() {
+ return bulk;
+ }
+
+ public void setBulk(String bulk) {
+ this.bulk = bulk;
+ }
+
+ }
+
+ public static class Endpoints {
+
+ private IdentityEndpoints identity = new IdentityEndpoints();
+ private String auth;
+
+ public IdentityEndpoints getIdentity() {
+ return identity;
+ }
+
+ public void setIdentity(IdentityEndpoints identity) {
+ this.identity = identity;
+ }
+
+ public String getAuth() {
+ return auth;
+ }
+
+ public void setAuth(String auth) {
+ this.auth = auth;
+ }
+
+ }
+
+ private Logger log = LoggerFactory.getLogger(RestBackendConfig.class);
+
+ private boolean enabled;
+ private String host;
+ private Endpoints endpoints = new Endpoints();
+
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public Endpoints getEndpoints() {
+ return endpoints;
+ }
+
+ public void setEndpoints(Endpoints endpoints) {
+ this.endpoints = endpoints;
+ }
+
+ private String buildEndpointUrl(String endpoint) {
+ if (StringUtils.startsWith(endpoint, "/")) {
+ if (StringUtils.isBlank(getHost())) {
+ throw new ConfigurationException("rest.host");
+ }
+
+ try {
+ new URL(getHost());
+ } catch (MalformedURLException e) {
+ throw new ConfigurationException("rest.host", e.getMessage());
+ }
+
+ return getHost() + endpoint;
+ } else {
+ return endpoint;
+ }
+ }
+
+ @PostConstruct
+ public void build() {
+ log.info("--- REST backend config ---");
+ log.info("Enabled: {}", isEnabled());
+
+ if (isEnabled()) {
+ endpoints.setAuth(buildEndpointUrl(endpoints.getAuth()));
+ endpoints.identity.setSingle(buildEndpointUrl(endpoints.identity.getSingle()));
+ endpoints.identity.setBulk(buildEndpointUrl(endpoints.identity.getBulk()));
+
+ log.info("Host: {}", getHost());
+ log.info("Auth endpoint: {}", endpoints.getAuth());
+ log.info("Identity Single endpoint: {}", endpoints.identity.getSingle());
+ log.info("Identity Bulk endpoint: {}", endpoints.identity.getBulk());
+ }
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/exception/InvalidResponseJsonException.java b/src/main/groovy/io/kamax/mxisd/exception/InvalidResponseJsonException.java
new file mode 100644
index 0000000..fde88e4
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/exception/InvalidResponseJsonException.java
@@ -0,0 +1,29 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.exception;
+
+public class InvalidResponseJsonException extends RuntimeException {
+
+ public InvalidResponseJsonException(String s) {
+ super(s);
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/exception/JsonMemberNotFoundException.java b/src/main/groovy/io/kamax/mxisd/exception/JsonMemberNotFoundException.java
new file mode 100644
index 0000000..06a0885
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/exception/JsonMemberNotFoundException.java
@@ -0,0 +1,29 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.exception;
+
+public class JsonMemberNotFoundException extends RuntimeException {
+
+ public JsonMemberNotFoundException(String s) {
+ super(s);
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/util/GsonParser.java b/src/main/groovy/io/kamax/mxisd/util/GsonParser.java
new file mode 100644
index 0000000..ec311c8
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/util/GsonParser.java
@@ -0,0 +1,91 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.util;
+
+import com.google.gson.*;
+import io.kamax.mxisd.exception.InvalidResponseJsonException;
+import io.kamax.mxisd.exception.JsonMemberNotFoundException;
+import org.apache.commons.io.IOUtils;
+import org.apache.http.HttpResponse;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Optional;
+
+public class GsonParser {
+
+ private JsonParser parser = new JsonParser();
+ private Gson gson;
+
+ public GsonParser() {
+ this(new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create());
+ }
+
+ public GsonParser(Gson gson) {
+ this.gson = gson;
+ }
+
+ public JsonObject parse(InputStream stream) throws IOException {
+ JsonElement el = parser.parse(IOUtils.toString(stream, StandardCharsets.UTF_8));
+ if (!el.isJsonObject()) {
+ throw new InvalidResponseJsonException("Response body is not a JSON object");
+ }
+
+ return el.getAsJsonObject();
+ }
+
+ public T parse(HttpResponse res, Class type) throws IOException {
+ return gson.fromJson(parse(res.getEntity().getContent()), type);
+ }
+
+ public JsonObject parse(InputStream stream, String property) throws IOException {
+ JsonObject obj = parse(stream);
+ if (!obj.has(property)) {
+ throw new JsonMemberNotFoundException("Member " + property + " does not exist");
+ }
+
+ JsonElement el = obj.get(property);
+ if (!el.isJsonObject()) {
+ throw new InvalidResponseJsonException("Member " + property + " is not a JSON object");
+ }
+
+ return el.getAsJsonObject();
+ }
+
+ public T parse(InputStream stream, String memberName, Class type) throws IOException {
+ JsonObject obj = parse(stream, memberName);
+ return gson.fromJson(obj, type);
+ }
+
+ public T parse(HttpResponse res, String memberName, Class type) throws IOException {
+ return parse(res.getEntity().getContent(), memberName, type);
+ }
+
+ public Optional parseOptional(HttpResponse res, String memberName, Class type) throws IOException {
+ try {
+ return Optional.of(parse(res.getEntity().getContent(), memberName, type));
+ } catch (JsonMemberNotFoundException e) {
+ return Optional.empty();
+ }
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/util/JsonUtils.java b/src/main/groovy/io/kamax/mxisd/util/JsonUtils.java
new file mode 100644
index 0000000..1edf26a
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/util/JsonUtils.java
@@ -0,0 +1,38 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.util;
+
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+
+public class JsonUtils {
+
+ public static JsonObject getObj(Gson gson, String property, Object value) {
+ JsonObject obj = new JsonObject();
+ obj.add(property, gson.toJsonTree(value));
+ return obj;
+ }
+
+ public static String getObjAsString(Gson gson, String property, Object value) {
+ return gson.toJson(getObj(gson, property, value));
+ }
+
+}
diff --git a/src/main/groovy/io/kamax/mxisd/util/RestClientUtils.java b/src/main/groovy/io/kamax/mxisd/util/RestClientUtils.java
new file mode 100644
index 0000000..c521008
--- /dev/null
+++ b/src/main/groovy/io/kamax/mxisd/util/RestClientUtils.java
@@ -0,0 +1,48 @@
+/*
+ * mxisd - Matrix Identity Server Daemon
+ * Copyright (C) 2017 Maxime Dor
+ *
+ * https://max.kamax.io/
+ *
+ * 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 .
+ */
+
+package io.kamax.mxisd.util;
+
+import com.google.gson.Gson;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.StringEntity;
+
+import java.nio.charset.StandardCharsets;
+
+public class RestClientUtils {
+
+ public static HttpPost post(String url, String body) {
+ StringEntity entity = new StringEntity(body, StandardCharsets.UTF_8);
+ entity.setContentType(ContentType.APPLICATION_JSON.toString());
+ HttpPost req = new HttpPost(url);
+ req.setEntity(entity);
+ return req;
+ }
+
+ public static HttpPost post(String url, Gson gson, String member, Object o) {
+ return post(url, JsonUtils.getObjAsString(gson, member, o));
+ }
+
+ public static HttpPost post(String url, Gson gson, Object o) {
+ return post(url, gson.toJson(o));
+ }
+
+}
diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml
index 41ffe4b..90c8ff3 100644
--- a/src/main/resources/application.yaml
+++ b/src/main/resources/application.yaml
@@ -23,6 +23,13 @@ lookup:
enabled: false
recursiveOnly: true
+rest:
+ endpoints:
+ auth: "/_mxisd/identity/api/v1/auth"
+ identity:
+ single: "/_mxisd/identity/api/v1/lookup/single"
+ bulk: "/_mxisd/identity/api/v1/lookup/bulk"
+
ldap:
enabled: false
connection:
diff --git a/src/test/java/io/kamax/mxisd/backend/rest/RestThreePidProviderTest.java b/src/test/java/io/kamax/mxisd/backend/rest/RestThreePidProviderTest.java
new file mode 100644
index 0000000..0289b12
--- /dev/null
+++ b/src/test/java/io/kamax/mxisd/backend/rest/RestThreePidProviderTest.java
@@ -0,0 +1,147 @@
+package io.kamax.mxisd.backend.rest;
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import io.kamax.matrix.ThreePidMedium;
+import io.kamax.mxisd.config.MatrixConfig;
+import io.kamax.mxisd.config.rest.RestBackendConfig;
+import io.kamax.mxisd.lookup.SingleLookupReply;
+import io.kamax.mxisd.lookup.SingleLookupRequest;
+import io.kamax.mxisd.lookup.ThreePidMapping;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+public class RestThreePidProviderTest {
+
+ @Rule
+ public WireMockRule wireMockRule = new WireMockRule(65000);
+
+ private RestThreePidProvider p;
+
+ private String lookupSinglePath = "/lookup/single";
+ private SingleLookupRequest lookupSingleRequest;
+ private String lookupSingleRequestBody = "{\"lookup\":{\"medium\":\"email\",\"address\":\"john.doe@example.org\"}}";
+ private String lookupSingleFoundBody = "{\"lookup\":{\"medium\":\"email\",\"address\":\"john.doe@example.org\"" +
+ ",\"id\":{\"type\":\"mxid\",\"value\":\"@john:example.org\"}}}";
+ private String lookupSingleNotFoundBody = "{}";
+
+ private String lookupBulkPath = "/lookup/bulk";
+ private List lookupBulkList;
+ private String lookupBulkRequestBody = "{\"lookup\":[{\"medium\":\"email\",\"address\":\"john.doe@example.org\"}," +
+ "{\"medium\":\"msisdn\",\"address\":\"123456789\"}]}";
+ private String lookupBulkFoundBody = "{\"lookup\":[{\"medium\":\"email\",\"address\":\"john.doe@example.org\"," +
+ "\"id\":{\"type\":\"localpart\",\"value\":\"john\"}},{\"medium\":\"msisdn\",\"address\":\"123456789\"," +
+ "\"id\":{\"type\":\"mxid\",\"value\":\"@jane:example.org\"}}]}";
+ private String lookupBulkNotFoundBody = "{\"lookup\":[]}";
+
+ @Before
+ public void before() {
+ MatrixConfig mxCfg = new MatrixConfig();
+ mxCfg.setDomain("example.org");
+ mxCfg.build();
+
+ RestBackendConfig cfg = new RestBackendConfig();
+ cfg.setEnabled(true);
+ cfg.setHost("http://localhost:65000");
+ cfg.getEndpoints().getIdentity().setSingle(lookupSinglePath);
+ cfg.getEndpoints().getIdentity().setBulk("/lookup/bulk");
+ cfg.build();
+
+ p = new RestThreePidProvider(cfg, mxCfg);
+
+ lookupSingleRequest = new SingleLookupRequest();
+ lookupSingleRequest.setType(ThreePidMedium.Email.getId());
+ lookupSingleRequest.setThreePid("john.doe@example.org");
+
+ ThreePidMapping m1 = new ThreePidMapping();
+ m1.setMedium(ThreePidMedium.Email.getId());
+ m1.setValue("john.doe@example.org");
+
+ ThreePidMapping m2 = new ThreePidMapping();
+ m1.setMedium(ThreePidMedium.PhoneNumber.getId());
+ m1.setValue("123456789");
+ lookupBulkList = new ArrayList<>();
+ lookupBulkList.add(m1);
+ lookupBulkList.add(m2);
+ }
+
+ @Test
+ public void lookupSingleFound() {
+ stubFor(post(urlEqualTo(lookupSinglePath))
+ .willReturn(aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(lookupSingleFoundBody)
+ )
+ );
+
+ Optional rep = p.find(lookupSingleRequest);
+ assertTrue(rep.isPresent());
+ rep.ifPresent(data -> {
+ assertNotNull(data.getMxid());
+ assertTrue(data.getMxid().getId(), StringUtils.equals(data.getMxid().getId(), "@john:example.org"));
+ });
+
+ verify(postRequestedFor(urlMatching("/lookup/single"))
+ .withHeader("Content-Type", containing("application/json"))
+ .withRequestBody(equalTo(lookupSingleRequestBody))
+ );
+ }
+
+ @Test
+ public void lookupSingleNotFound() {
+ stubFor(post(urlEqualTo(lookupSinglePath))
+ .willReturn(aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(lookupSingleNotFoundBody)
+ )
+ );
+
+ Optional rep = p.find(lookupSingleRequest);
+ assertTrue(!rep.isPresent());
+
+ verify(postRequestedFor(urlMatching("/lookup/single"))
+ .withHeader("Content-Type", containing("application/json"))
+ .withRequestBody(equalTo(lookupSingleRequestBody))
+ );
+ }
+
+ @Test
+ public void lookupBulkFound() {
+ stubFor(post(urlEqualTo(lookupBulkPath))
+ .willReturn(aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(lookupBulkFoundBody)
+ )
+ );
+
+ List mappings = p.populate(lookupBulkList);
+ assertNotNull(mappings);
+ assertTrue(mappings.size() == 2);
+ assertTrue(StringUtils.equals(mappings.get(0).getMxid(), "@john:example.org"));
+ assertTrue(StringUtils.equals(mappings.get(1).getMxid(), "@jane:example.org"));
+ }
+
+ @Test
+ public void lookupBulkNotFound() {
+ stubFor(post(urlEqualTo(lookupBulkPath))
+ .willReturn(aResponse()
+ .withHeader("Content-Type", "application/json")
+ .withBody(lookupBulkNotFoundBody)
+ )
+ );
+
+ List mappings = p.populate(lookupBulkList);
+ assertNotNull(mappings);
+ assertTrue(mappings.size() == 0);
+ }
+
+}