Merge pull request #30 from kamax-io/rest-backend

REST backend
This commit is contained in:
Max Dor
2017-09-18 01:05:31 +02:00
committed by GitHub
26 changed files with 1380 additions and 44 deletions

View File

@@ -114,6 +114,7 @@ dependencies {
compile 'org.xerial:sqlite-jdbc:3.20.0' compile 'org.xerial:sqlite-jdbc:3.20.0'
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'com.github.tomakehurst:wiremock:2.8.0'
} }
springBoot { springBoot {

169
docs/backends/rest.md Normal file
View File

@@ -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": []
}
```

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -20,8 +20,13 @@
package io.kamax.mxisd.auth; package io.kamax.mxisd.auth;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.ThreePid; import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider; 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.invitation.InvitationManager;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -40,26 +45,45 @@ public class AuthManager {
@Autowired @Autowired
private List<AuthenticatorProvider> providers = new ArrayList<>(); private List<AuthenticatorProvider> providers = new ArrayList<>();
@Autowired
private MatrixConfig mxCfg;
@Autowired @Autowired
private InvitationManager invMgr; private InvitationManager invMgr;
public UserAuthResult authenticate(String id, String password) { public UserAuthResult authenticate(String id, String password) {
_MatrixID mxid = new MatrixID(id);
for (AuthenticatorProvider provider : providers) { for (AuthenticatorProvider provider : providers) {
if (!provider.isEnabled()) { if (!provider.isEnabled()) {
continue; continue;
} }
UserAuthResult result = provider.authenticate(id, password); BackendAuthResult result = provider.authenticate(mxid, password);
if (result.isSuccess()) { 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()); 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); log.info("Processing {} for {}", pid, id);
invMgr.publishMappingIfInvited(new ThreePidMapping(pid, result.getMxid())); invMgr.publishMappingIfInvited(new ThreePidMapping(pid, authResult.getMxid()));
} }
invMgr.lookupMappingsForInvites(); invMgr.lookupMappingsForInvites();
return result; return authResult;
} }
} }

View File

@@ -20,12 +20,12 @@
package io.kamax.mxisd.auth.provider; package io.kamax.mxisd.auth.provider;
import io.kamax.mxisd.auth.UserAuthResult; import io.kamax.matrix._MatrixID;
public interface AuthenticatorProvider { public interface AuthenticatorProvider {
boolean isEnabled(); boolean isEnabled();
UserAuthResult authenticate(String id, String password); BackendAuthResult authenticate(_MatrixID mxid, String password);
} }

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<ThreePid> threePids = new ArrayList<>();
public String getDisplayName() {
return displayName;
}
public List<ThreePid> 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;
}
}

View File

@@ -27,15 +27,17 @@ import com.google.firebase.internal.NonNull
import com.google.firebase.tasks.OnFailureListener import com.google.firebase.tasks.OnFailureListener
import com.google.firebase.tasks.OnSuccessListener import com.google.firebase.tasks.OnSuccessListener
import io.kamax.matrix.ThreePidMedium 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.AuthenticatorProvider
import io.kamax.mxisd.auth.provider.BackendAuthResult
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils
import org.slf4j.Logger import org.slf4j.Logger
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.regex.Matcher
import java.util.regex.Pattern import java.util.regex.Pattern
public class GoogleFirebaseAuthenticator implements AuthenticatorProvider { public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
@@ -49,7 +51,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private FirebaseApp fbApp; private FirebaseApp fbApp;
private FirebaseAuth fbAuth; 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 { try {
l.await(timeout, unit); l.await(timeout, unit);
} catch (InterruptedException e) { } catch (InterruptedException e) {
@@ -108,22 +110,16 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
} }
@Override @Override
public UserAuthResult authenticate(String id, String password) { public BackendAuthResult authenticate(_MatrixID mxid, String password) {
if (!isEnabled()) { if (!isEnabled()) {
throw new IllegalStateException(); throw new IllegalStateException();
} }
final UserAuthResult result = new UserAuthResult(); log.info("Trying to authenticate {}", mxid);
log.info("Trying to authenticate {}", id); BackendAuthResult result = BackendAuthResult.failure();
Matcher m = matrixIdLaxPattern.matcher(id);
if (!m.matches()) {
log.warn("Could not validate {} as a Matrix ID", id);
result.failure();
}
String localpart = m.group(1); String localpart = m.group(1);
CountDownLatch l = new CountDownLatch(1); CountDownLatch l = new CountDownLatch(1);
fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener<FirebaseToken>() { fbAuth.verifyIdToken(password).addOnSuccessListener(new OnSuccessListener<FirebaseToken>() {
@Override @Override
@@ -131,26 +127,26 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
try { try {
if (!StringUtils.equals(localpart, token.getUid())) { if (!StringUtils.equals(localpart, token.getUid())) {
log.info("Failture to authenticate {}: Matrix ID localpart '{}' does not match Firebase UID '{}'", id, 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; return;
} }
log.info("{} was successfully authenticated", id); result = BackendAuthResult.success(mxid.getId(), UserIdType.MatrixID, token.getName());
result.success(id, token.getName()); log.info("{} was successfully authenticated", mxid);
log.info("Fetching profile for {}", mxid);
log.info("Fetching profile for {}", id);
CountDownLatch userRecordLatch = new CountDownLatch(1); CountDownLatch userRecordLatch = new CountDownLatch(1);
fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener<UserRecord>() { fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener<UserRecord>() {
@Override @Override
void onSuccess(UserRecord user) { void onSuccess(UserRecord user) {
try { try {
if (StringUtils.isNotBlank(user.getEmail())) { 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())) { if (StringUtils.isNotBlank(user.getPhoneNumber())) {
result.withThreePid(ThreePidMedium.PhoneNumber, user.getPhoneNumber()); result.withThreePid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user.getPhoneNumber()));
} }
} finally { } finally {
userRecordLatch.countDown(); userRecordLatch.countDown();
} }
@@ -159,8 +155,8 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
@Override @Override
void onFailure(@NonNull Exception e) { void onFailure(@NonNull Exception e) {
try { try {
log.warn("Unable to fetch Firebase user profile for {}", id); log.warn("Unable to fetch Firebase user profile for {}", mxid);
result.failure(); result = BackendAuthResult.failure();
} finally { } finally {
userRecordLatch.countDown(); userRecordLatch.countDown();
} }
@@ -177,13 +173,13 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
void onFailure(@NonNull Exception e) { void onFailure(@NonNull Exception e) {
try { try {
if (e instanceof IllegalArgumentException) { if (e instanceof IllegalArgumentException) {
log.info("Failure to authenticate {}: invalid firebase token", id); log.info("Failure to authenticate {}: invalid firebase token", mxid);
} else { } else {
log.info("Failure to authenticate {}: {}", id, e.getMessage(), e); log.info("Failure to authenticate {}: {}", id, e.getMessage(), e);
log.info("Exception", e); log.info("Exception", e);
} }
result.failure(); result = BackendAuthResult.failure();
} finally { } finally {
l.countDown() l.countDown()
} }

View File

@@ -20,9 +20,10 @@
package io.kamax.mxisd.backend.ldap; package io.kamax.mxisd.backend.ldap;
import io.kamax.matrix.MatrixID; import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.auth.UserAuthResult; import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider; import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.BackendAuthResult;
import org.apache.commons.lang.StringUtils; import org.apache.commons.lang.StringUtils;
import org.apache.directory.api.ldap.model.cursor.CursorException; import org.apache.directory.api.ldap.model.cursor.CursorException;
import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException; import org.apache.directory.api.ldap.model.cursor.CursorLdapReferralException;
@@ -53,16 +54,15 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato
} }
@Override @Override
public UserAuthResult authenticate(String id, String password) { public BackendAuthResult authenticate(_MatrixID mxid, String password) {
log.info("Performing auth for {}", id); log.info("Performing auth for {}", mxid);
LdapConnection conn = getConn(); LdapConnection conn = getConn();
try { try {
bind(conn); bind(conn);
String uidType = getCfg().getAttribute().getUid().getType(); String uidType = getCfg().getAttribute().getUid().getType();
MatrixID mxIdExt = new MatrixID(id); String userFilterValue = StringUtils.equals(LdapThreePidProvider.UID, uidType) ? mxid.getLocalPart() : mxid.getId();
String userFilterValue = StringUtils.equals(LdapThreePidProvider.UID, uidType) ? mxIdExt.getLocalPart() : mxIdExt.getId();
String userFilter = "(" + getCfg().getAttribute().getUid().getValue() + "=" + userFilterValue + ")"; String userFilter = "(" + getCfg().getAttribute().getUid().getValue() + "=" + userFilterValue + ")";
if (!StringUtils.isBlank(getCfg().getAuth().getFilter())) { if (!StringUtils.isBlank(getCfg().getAuth().getFilter())) {
userFilter = "(&" + getCfg().getAuth().getFilter() + userFilter + ")"; userFilter = "(&" + getCfg().getAuth().getFilter() + userFilter + ")";
@@ -91,7 +91,7 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato
conn.bind(entry.getDn(), password); conn.bind(entry.getDn(), password);
} catch (LdapException e) { } catch (LdapException e) {
log.info("Unable to bind using {} because {}", entry.getDn().getName(), e.getMessage()); 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()); 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("Authentication successful for {}", entry.getDn().getName());
log.info("DN {} is a valid match", dn); 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) { } 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 { } finally {
cursor.close(); cursor.close();
} }
log.info("No match were found for {}", id); log.info("No match were found for {}", mxid);
return new UserAuthResult().failure(); return BackendAuthResult.failure();
} catch (LdapException | IOException | CursorException e) { } catch (LdapException | IOException | CursorException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} finally { } finally {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.backend.rest;
import java.util.ArrayList;
import java.util.List;
public class LookupBulkResponseJson {
private List<LookupSingleResponseJson> lookup = new ArrayList<>();
public List<LookupSingleResponseJson> getLookup() {
return lookup;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<SingleLookupReply> 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<LookupSingleResponseJson> 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<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
List<LookupSingleRequestJson> 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);
}
}
}

View File

@@ -20,8 +20,9 @@
package io.kamax.mxisd.backend.sql; 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.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.BackendAuthResult;
import io.kamax.mxisd.config.ServerConfig; import io.kamax.mxisd.config.ServerConfig;
import io.kamax.mxisd.config.sql.SqlProviderConfig; import io.kamax.mxisd.config.sql.SqlProviderConfig;
import io.kamax.mxisd.invitation.InvitationManager; import io.kamax.mxisd.invitation.InvitationManager;
@@ -50,11 +51,11 @@ public class SqlAuthProvider implements AuthenticatorProvider {
} }
@Override @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"); log.info("Performing dummy authentication try to force invite mapping refresh");
invMgr.lookupMappingsForInvites(); invMgr.lookupMappingsForInvites();
return new UserAuthResult().failure(); return BackendAuthResult.failure();
} }
} }

View File

@@ -46,7 +46,7 @@ public class MatrixConfig {
} }
@PostConstruct @PostConstruct
private void postConstruct() { public void build() {
log.info("--- Matrix config ---"); log.info("--- Matrix config ---");
if (StringUtils.isBlank(domain)) { if (StringUtils.isBlank(domain)) {

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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());
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.exception;
public class InvalidResponseJsonException extends RuntimeException {
public InvalidResponseJsonException(String s) {
super(s);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.exception;
public class JsonMemberNotFoundException extends RuntimeException {
public JsonMemberNotFoundException(String s) {
super(s);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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> T parse(HttpResponse res, Class<T> 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> T parse(InputStream stream, String memberName, Class<T> type) throws IOException {
JsonObject obj = parse(stream, memberName);
return gson.fromJson(obj, type);
}
public <T> T parse(HttpResponse res, String memberName, Class<T> type) throws IOException {
return parse(res.getEntity().getContent(), memberName, type);
}
public <T> Optional<T> parseOptional(HttpResponse res, String memberName, Class<T> type) throws IOException {
try {
return Optional.of(parse(res.getEntity().getContent(), memberName, type));
} catch (JsonMemberNotFoundException e) {
return Optional.empty();
}
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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));
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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));
}
}

View File

@@ -23,6 +23,13 @@ lookup:
enabled: false enabled: false
recursiveOnly: true 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: ldap:
enabled: false enabled: false
connection: connection:

View File

@@ -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<ThreePidMapping> 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<SingleLookupReply> 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<SingleLookupReply> 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<ThreePidMapping> 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<ThreePidMapping> mappings = p.populate(lookupBulkList);
assertNotNull(mappings);
assertTrue(mappings.size() == 0);
}
}