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); + } + +}