Identity lookup implementation for REST backend

This commit is contained in:
Maxime Dor
2017-09-17 22:34:48 +02:00
parent efc54e73f2
commit 317fc367f8
15 changed files with 327 additions and 56 deletions

View File

@@ -93,8 +93,8 @@ Encoding: JSON UTF-8
```
#### Response Body
`lookup.uid` contains the Matrix ID localpart of the user
If a match was found:
- `lookup.id.type` supported values: `localpart`, `mxid`
```
{
"lookup": {
@@ -108,6 +108,11 @@ Encoding: JSON UTF-8
}
```
If no match was found:
```
{}
```
#### Bulk
Configured with `rest.endpoints.identity.bulk`
@@ -131,6 +136,8 @@ Encoding: JSON UTF-8
```
#### Response Body
For all entries where a match was found:
- `lookup[].id.type` supported values: `localpart`, `mxid`
```
{
"lookup": [
@@ -153,3 +160,10 @@ Encoding: JSON UTF-8
]
}
```
If no match was found:
```
{
"lookup": []
}
```

View File

@@ -21,6 +21,7 @@
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;
@@ -51,12 +52,13 @@ public class AuthManager {
private InvitationManager invMgr;
public UserAuthResult authenticate(String id, String password) {
_MatrixID mxid = new MatrixID(id);
for (AuthenticatorProvider provider : providers) {
if (!provider.isEnabled()) {
continue;
}
BackendAuthResult result = provider.authenticate(id, password);
BackendAuthResult result = provider.authenticate(mxid, password);
if (result.isSuccess()) {
String mxId;

View File

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

View File

@@ -27,6 +27,7 @@ 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.matrix._MatrixID
import io.kamax.mxisd.ThreePid
import io.kamax.mxisd.UserIdType
import io.kamax.mxisd.auth.provider.AuthenticatorProvider
@@ -37,7 +38,6 @@ 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 {
@@ -110,17 +110,12 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
}
@Override
public BackendAuthResult authenticate(String id, String password) {
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
if (!isEnabled()) {
throw new IllegalStateException();
}
log.info("Trying to authenticate {}", id);
Matcher m = matrixIdLaxPattern.matcher(id);
if (!m.matches()) {
log.warn("Could not validate {} as a Matrix ID", id);
BackendAuthResult.failure();
}
log.info("Trying to authenticate {}", mxid);
BackendAuthResult result = BackendAuthResult.failure();
@@ -136,9 +131,9 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
return;
}
result = BackendAuthResult.success(id, UserIdType.MatrixID, token.getName());
log.info("{} was successfully authenticated", id);
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<UserRecord>() {
@Override
@@ -160,7 +155,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
@Override
void onFailure(@NonNull Exception e) {
try {
log.warn("Unable to fetch Firebase user profile for {}", id);
log.warn("Unable to fetch Firebase user profile for {}", mxid);
result = BackendAuthResult.failure();
} finally {
userRecordLatch.countDown();
@@ -178,7 +173,7 @@ 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);

View File

@@ -20,7 +20,7 @@
package io.kamax.mxisd.backend.ldap;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.BackendAuthResult;
@@ -54,16 +54,15 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato
}
@Override
public BackendAuthResult 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 + ")";
EntryCursor cursor = conn.search(getCfg().getConn().getBaseDn(), userFilter, SearchScope.SUBTREE, getUidAttribute(), getCfg().getAttribute().getName());
try {
@@ -99,15 +98,15 @@ public class LdapAuthProvider extends LdapGenericBackend implements Authenticato
log.info("DN {} is a valid match", dn);
// TODO should we canonicalize the MXID?
return BackendAuthResult.success(mxIdExt.getId(), UserIdType.MatrixID, name);
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);
log.info("No match were found for {}", mxid);
return BackendAuthResult.failure();
} catch (LdapException | IOException | CursorException e) {
throw new RuntimeException(e);

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

@@ -20,40 +20,24 @@
package io.kamax.mxisd.backend.rest;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.kamax.matrix.MatrixID;
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.GsonParser;
import io.kamax.mxisd.util.RestClientUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RestAuthProvider implements AuthenticatorProvider {
private RestBackendConfig cfg;
private Gson gson;
private GsonParser parser;
private CloseableHttpClient client;
public class RestAuthProvider extends RestProvider implements AuthenticatorProvider {
@Autowired
public RestAuthProvider(RestBackendConfig cfg) {
this.cfg = cfg;
client = HttpClients.createDefault();
gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES).create();
parser = new GsonParser(gson);
super(cfg);
}
@Override
@@ -62,10 +46,9 @@ public class RestAuthProvider implements AuthenticatorProvider {
}
@Override
public BackendAuthResult authenticate(String id, String password) {
_MatrixID mxid = new MatrixID(id);
public BackendAuthResult authenticate(_MatrixID mxid, String password) {
RestAuthRequestJson auth = new RestAuthRequestJson();
auth.setMxid(id);
auth.setMxid(mxid.getId());
auth.setLocalpart(mxid.getLocalPart());
auth.setDomain(mxid.getDomain());
auth.setPassword(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

@@ -20,23 +20,47 @@
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 org.apache.commons.lang.NotImplementedException;
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;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
public class RestThreePidProvider implements IThreePidProvider {
public class RestThreePidProvider extends RestProvider implements IThreePidProvider {
private MatrixConfig mxCfg; // FIXME should be done in the lookup manager
@Autowired
private RestBackendConfig cfg;
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());
} else {
return new MatrixID(id.getValue(), mxCfg.getDomain());
}
}
@Override
public boolean isEnabled() {
@@ -53,14 +77,51 @@ public class RestThreePidProvider implements IThreePidProvider {
return 20;
}
// TODO refactor common code
@Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
throw new NotImplementedException();
HttpUriRequest req = RestClientUtils.post(
cfg.getEndpoints().getAuth(),
gson,
"lookup",
new LookupSingleRequestJson(request.getType(), request.getThreePid()));
try (CloseableHttpResponse res = client.execute(req)) {
int status = res.getStatusLine().getStatusCode();
if (status < 200 || status >= 300) {
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) {
throw new NotImplementedException();
List<LookupSingleRequestJson> ioListRequest = mappings.stream()
.map(mapping -> new LookupSingleRequestJson(mapping.getMedium(), mapping.getValue()))
.collect(Collectors.toList());
HttpUriRequest req = RestClientUtils.post(cfg.getEndpoints().getAuth(), 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,6 +20,7 @@
package io.kamax.mxisd.backend.sql;
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;
@@ -50,7 +51,7 @@ public class SqlAuthProvider implements AuthenticatorProvider {
}
@Override
public BackendAuthResult 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();

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

@@ -22,12 +22,14 @@ 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 {
@@ -42,18 +44,26 @@ public class GsonParser {
this.gson = gson;
}
public JsonObject parse(InputStream stream, String property) throws IOException {
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");
}
JsonObject obj = el.getAsJsonObject();
if (!obj.has(property)) {
throw new IOException("Member " + property + " does not exist");
return el.getAsJsonObject();
}
el = obj.get(property);
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");
}
@@ -70,4 +80,12 @@ public class GsonParser {
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

@@ -41,4 +41,8 @@ public class RestClientUtils {
return post(url, JsonUtils.getObjAsString(gson, member, o));
}
public static HttpPost post(String url, Gson gson, Object o) {
return post(url, gson.toJson(o));
}
}