Refactor logic, preparing to generalize post-login publish of mappings

This commit is contained in:
Maxime Dor
2017-09-12 19:47:01 +02:00
parent 55b759a31c
commit 09b789dfc2
10 changed files with 151 additions and 91 deletions

View File

@@ -39,4 +39,9 @@ public class ThreePid {
return address; return address;
} }
@Override
public String toString() {
return getMedium() + ":" + getAddress();
}
} }

View File

@@ -20,7 +20,10 @@
package io.kamax.mxisd.auth; package io.kamax.mxisd.auth;
import io.kamax.mxisd.ThreePid;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider; import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.ThreePidMapping;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +40,9 @@ public class AuthManager {
@Autowired @Autowired
private List<AuthenticatorProvider> providers = new ArrayList<>(); private List<AuthenticatorProvider> providers = new ArrayList<>();
@Autowired
private InvitationManager invMgr;
public UserAuthResult authenticate(String id, String password) { public UserAuthResult authenticate(String id, String password) {
for (AuthenticatorProvider provider : providers) { for (AuthenticatorProvider provider : providers) {
if (!provider.isEnabled()) { if (!provider.isEnabled()) {
@@ -45,6 +51,12 @@ public class AuthManager {
UserAuthResult result = provider.authenticate(id, password); UserAuthResult result = provider.authenticate(id, password);
if (result.isSuccess()) { if (result.isSuccess()) {
log.info("{} was authenticated by {}, publishing 3PID mappings, if any", id, provider.getClass().getSimpleName());
for (ThreePid pid : result.getThreePids()) {
log.info("Processing {} for {}", pid, id);
invMgr.publishMappingIfInvited(new ThreePidMapping(pid, result.getMxid()));
}
return result; return result;
} }
} }

View File

@@ -20,11 +20,19 @@
package io.kamax.mxisd.auth; package io.kamax.mxisd.auth;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.ThreePid;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class UserAuthResult { public class UserAuthResult {
private boolean success; private boolean success;
private String mxid; private String mxid;
private String displayName; private String displayName;
private List<ThreePid> threePids = new ArrayList<>();
public UserAuthResult failure() { public UserAuthResult failure() {
success = false; success = false;
@@ -66,4 +74,18 @@ public class UserAuthResult {
this.displayName = displayName; this.displayName = displayName;
} }
public UserAuthResult withThreePid(ThreePidMedium medium, String address) {
return withThreePid(medium.getId(), address);
}
public UserAuthResult withThreePid(String medium, String address) {
threePids.add(new ThreePid(medium, address));
return this;
}
public List<ThreePid> getThreePids() {
return Collections.unmodifiableList(threePids);
}
} }

View File

@@ -22,17 +22,12 @@ package io.kamax.mxisd.auth.provider
import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions import com.google.firebase.FirebaseOptions
import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.*
import com.google.firebase.auth.FirebaseCredential
import com.google.firebase.auth.FirebaseCredentials
import com.google.firebase.auth.FirebaseToken
import com.google.firebase.internal.NonNull 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.mxisd.auth.UserAuthResult
import io.kamax.mxisd.invitation.InvitationManager
import io.kamax.mxisd.lookup.ThreePidMapping
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
@@ -46,25 +41,31 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class); private Logger log = LoggerFactory.getLogger(GoogleFirebaseAuthenticator.class);
private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); private static final Pattern matrixIdLaxPattern = Pattern.compile("@(.*):(.+)"); // FIXME use matrix-java-sdk
private boolean isEnabled; private boolean isEnabled;
private String domain; private String domain;
private FirebaseApp fbApp; private FirebaseApp fbApp;
private FirebaseAuth fbAuth; private FirebaseAuth fbAuth;
private InvitationManager invMgr; private void waitOnLatch(UserAuthResult result, CountDownLatch l, long timeout, TimeUnit unit, String purpose) {
try {
public GoogleFirebaseAuthenticator(InvitationManager invMgr, boolean isEnabled) { l.await(timeout, unit);
this.isEnabled = isEnabled; } catch (InterruptedException e) {
this.invMgr = invMgr; log.warn("Interrupted while waiting for " + purpose);
result.failure();
}
} }
public GoogleFirebaseAuthenticator(InvitationManager invMgr, String credsPath, String db, String domain) { public GoogleFirebaseAuthenticator(boolean isEnabled) {
this(invMgr, true); this.isEnabled = isEnabled;
}
public GoogleFirebaseAuthenticator(String credsPath, String db, String domain) {
this(true);
this.domain = domain; this.domain = domain;
try { try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db)); fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "AuthenticationProvider");
fbAuth = FirebaseAuth.getInstance(fbApp); fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready"); log.info("Google Firebase Authentication is ready");
@@ -130,14 +131,42 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
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.failure();
return;
} }
log.info("{} was successfully authenticated", id); log.info("{} was successfully authenticated", id);
result.success(id, token.getName()); result.success(id, token.getName());
if (StringUtils.isNotBlank(token.getEmail())) { log.info("Fetching profile for {}", id);
invMgr.publishMappingIfInvited(new ThreePidMapping(ThreePidMedium.Email.getId(), token.getEmail(), id)) CountDownLatch userRecordLatch = new CountDownLatch(1);
} fbAuth.getUser(token.getUid()).addOnSuccessListener(new OnSuccessListener<UserRecord>() {
@Override
void onSuccess(UserRecord user) {
try {
if (StringUtils.isNotBlank(user.getEmail())) {
result.withThreePid(ThreePidMedium.Email, user.getEmail());
}
if (StringUtils.isNotBlank(user.getPhoneNumber())) {
result.withThreePid(ThreePidMedium.PhoneNumber, user.getPhoneNumber());
}
} finally {
userRecordLatch.countDown();
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
void onFailure(@NonNull Exception e) {
try {
log.warn("Unable to fetch Firebase user profile for {}", id);
result.failure();
} finally {
userRecordLatch.countDown();
}
}
});
waitOnLatch(result, userRecordLatch, 30, TimeUnit.SECONDS, "Firebase user profile");
} finally { } finally {
l.countDown() l.countDown()
} }
@@ -160,13 +189,7 @@ public class GoogleFirebaseAuthenticator implements AuthenticatorProvider {
} }
}); });
try { waitOnLatch(result, l, 30, TimeUnit.SECONDS, "Firebase auth check");
l.await(30, TimeUnit.SECONDS);
} catch (InterruptedException e) {
log.warn("Interrupted while waiting for Firebase auth check");
result.failure();
}
return result; return result;
} }

View File

@@ -22,7 +22,6 @@ package io.kamax.mxisd.config;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider; import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.auth.provider.GoogleFirebaseAuthenticator; import io.kamax.mxisd.auth.provider.GoogleFirebaseAuthenticator;
import io.kamax.mxisd.invitation.InvitationManager;
import io.kamax.mxisd.lookup.provider.GoogleFirebaseProvider; import io.kamax.mxisd.lookup.provider.GoogleFirebaseProvider;
import io.kamax.mxisd.lookup.provider.IThreePidProvider; import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -43,9 +42,6 @@ public class FirebaseConfig {
@Autowired @Autowired
private ServerConfig srvCfg; private ServerConfig srvCfg;
@Autowired
private InvitationManager invMgr;
private boolean enabled; private boolean enabled;
private String credentials; private String credentials;
private String database; private String database;
@@ -87,9 +83,9 @@ public class FirebaseConfig {
@Bean @Bean
public AuthenticatorProvider getAuthProvider() { public AuthenticatorProvider getAuthProvider() {
if (!enabled) { if (!enabled) {
return new GoogleFirebaseAuthenticator(invMgr, false); return new GoogleFirebaseAuthenticator(false);
} else { } else {
return new GoogleFirebaseAuthenticator(invMgr, credentials, database, srvCfg.getName()); return new GoogleFirebaseAuthenticator(credentials, database, srvCfg.getName());
} }
} }

View File

@@ -25,7 +25,6 @@ import io.kamax.matrix.ThreePid;
import io.kamax.mxisd.exception.BadRequestException; import io.kamax.mxisd.exception.BadRequestException;
import io.kamax.mxisd.exception.MappingAlreadyExistsException; import io.kamax.mxisd.exception.MappingAlreadyExistsException;
import io.kamax.mxisd.invitation.sender.IInviteSender; import io.kamax.mxisd.invitation.sender.IInviteSender;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.strategy.LookupStrategy; import io.kamax.mxisd.lookup.strategy.LookupStrategy;
import io.kamax.mxisd.signature.SignatureManager; import io.kamax.mxisd.signature.SignatureManager;
@@ -99,7 +98,7 @@ public class InvitationManager {
// TODO use caching mechanism // TODO use caching mechanism
// TODO export in matrix-java-sdk // TODO export in matrix-java-sdk
Optional<String> findHomeserverForDomain(String domain) { String findHomeserverForDomain(String domain) {
log.debug("Performing SRV lookup for {}", domain); log.debug("Performing SRV lookup for {}", domain);
String lookupDns = getSrvRecordName(domain); String lookupDns = getSrvRecordName(domain);
log.info("Lookup name: {}", lookupDns); log.info("Lookup name: {}", lookupDns);
@@ -110,7 +109,7 @@ public class InvitationManager {
Arrays.sort(records, Comparator.comparingInt(SRVRecord::getPriority)); Arrays.sort(records, Comparator.comparingInt(SRVRecord::getPriority));
for (SRVRecord record : records) { for (SRVRecord record : records) {
log.info("Found SRV record: {}", record.toString()); log.info("Found SRV record: {}", record.toString());
return Optional.of("https://" + record.getTarget().toString(true) + ":" + record.getPort()); return "https://" + record.getTarget().toString(true) + ":" + record.getPort();
} }
} else { } else {
log.info("No SRV record for {}", lookupDns); log.info("No SRV record for {}", lookupDns);
@@ -120,7 +119,7 @@ public class InvitationManager {
} }
log.info("Performing basic lookup using domain name {}", domain); log.info("Performing basic lookup using domain name {}", domain);
return Optional.of("https://" + domain + ":8448"); return "https://" + domain + ":8448";
} }
@Autowired @Autowired
@@ -143,13 +142,7 @@ public class InvitationManager {
return invitations.get(pid); return invitations.get(pid);
} }
SingleLookupRequest request = new SingleLookupRequest(); Optional<?> result = lookupMgr.find(invitation.getMedium(), invitation.getAddress(), true);
request.setType(invitation.getMedium());
request.setThreePid(invitation.getAddress());
request.setRecursive(true);
request.setRequester("Internal");
Optional<?> result = lookupMgr.findRecursive(request);
if (result.isPresent()) { if (result.isPresent()) {
log.info("Mapping for {}:{} already exists, refusing to store invite", pid.getMedium(), pid.getAddress()); log.info("Mapping for {}:{} already exists, refusing to store invite", pid.getMedium(), pid.getAddress());
throw new MappingAlreadyExistsException(); throw new MappingAlreadyExistsException();
@@ -180,60 +173,52 @@ public class InvitationManager {
log.info("{}:{} has an invite pending, publishing mapping", threePid.getMedium(), threePid.getValue()); log.info("{}:{} has an invite pending, publishing mapping", threePid.getMedium(), threePid.getValue());
String domain = reply.getInvite().getSender().getDomain(); String domain = reply.getInvite().getSender().getDomain();
log.info("Discovering HS for domain {}", domain); log.info("Discovering HS for domain {}", domain);
Optional<String> hsUrlOpt = findHomeserverForDomain(domain); String hsUrlOpt = findHomeserverForDomain(domain);
if (!hsUrlOpt.isPresent()) {
log.warn("No HS found for domain {} - ignoring publishing", domain);
} else {
// TODO this is needed as this will block if called during authentication cycle due to synapse implementation
new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool // TODO this is needed as this will block if called during authentication cycle due to synapse implementation
HttpPost req = new HttpPost(hsUrlOpt.get() + "/_matrix/federation/v1/3pid/onbind"); new Thread(() -> { // FIXME need to make this retry-able and within a general background working pool
// Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr HttpPost req = new HttpPost(hsUrlOpt + "/_matrix/federation/v1/3pid/onbind");
JSONObject obj = new JSONObject(); // TODO use Gson instead // Expected body: https://matrix.to/#/!HUeDbmFUsWAhxHHvFG:matrix.org/$150469846739DCLWc:matrix.trancendances.fr
obj.put("mxid", threePid.getMxid()); JSONObject obj = new JSONObject(); // TODO use Gson instead
obj.put("token", reply.getToken()); obj.put("mxid", threePid.getMxid());
obj.put("signatures", signMgr.signMessageJson(obj.toString())); obj.put("token", reply.getToken());
obj.put("signatures", signMgr.signMessageJson(obj.toString()));
JSONObject objUp = new JSONObject(); JSONObject objUp = new JSONObject();
objUp.put("mxid", threePid.getMxid()); objUp.put("mxid", threePid.getMxid());
objUp.put("medium", threePid.getMedium()); objUp.put("medium", threePid.getMedium());
objUp.put("address", threePid.getValue()); objUp.put("address", threePid.getValue());
objUp.put("sender", reply.getInvite().getSender().getId()); objUp.put("sender", reply.getInvite().getSender().getId());
objUp.put("room_id", reply.getInvite().getRoomId()); objUp.put("room_id", reply.getInvite().getRoomId());
objUp.put("signed", obj); objUp.put("signed", obj);
String mapping = gson.toJson(objUp); // FIXME we shouldn't need to be doing this JSONObject content = new JSONObject(); // TODO use Gson instead
JSONArray invites = new JSONArray();
invites.put(objUp);
content.put("invites", invites);
content.put("medium", threePid.getMedium());
content.put("address", threePid.getValue());
content.put("mxid", threePid.getMxid());
JSONObject content = new JSONObject(); // TODO use Gson instead content.put("signatures", signMgr.signMessageJson(content.toString()));
JSONArray invites = new JSONArray();
invites.put(objUp);
content.put("invites", invites);
content.put("medium", threePid.getMedium());
content.put("address", threePid.getValue());
content.put("mxid", threePid.getMxid());
content.put("signatures", signMgr.signMessageJson(content.toString())); StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8);
entity.setContentType("application/json");
StringEntity entity = new StringEntity(content.toString(), StandardCharsets.UTF_8); req.setEntity(entity);
entity.setContentType("application/json"); try {
req.setEntity(entity); log.info("Posting onBind event to {}", req.getURI());
try { CloseableHttpResponse response = client.execute(req);
log.info("Posting onBind event to {}", req.getURI()); int statusCode = response.getStatusLine().getStatusCode();
CloseableHttpResponse response = client.execute(req); log.info("Answer code: {}", statusCode);
int statusCode = response.getStatusLine().getStatusCode(); if (statusCode >= 400) {
log.info("Answer code: {}", statusCode); log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
if (statusCode >= 400) {
log.warn("Answer body: {}", IOUtils.toString(response.getEntity().getContent(), StandardCharsets.UTF_8));
}
response.close();
} catch (IOException e) {
log.warn("Unable to tell HS {} about invite being mapped", domain, e);
} }
invitations.remove(key); response.close();
}).start(); } catch (IOException e) {
} log.warn("Unable to tell HS {} about invite being mapped", domain, e);
}
invitations.remove(key);
}).start();
} }
} }

View File

@@ -21,6 +21,7 @@
package io.kamax.mxisd.lookup; package io.kamax.mxisd.lookup;
import groovy.json.JsonOutput; import groovy.json.JsonOutput;
import io.kamax.mxisd.ThreePid;
public class ThreePidMapping { public class ThreePidMapping {
@@ -32,6 +33,10 @@ public class ThreePidMapping {
// stub // stub
} }
public ThreePidMapping(ThreePid threePid, String mxid) {
this(threePid.getMedium(), threePid.getAddress(), mxid);
}
public ThreePidMapping(String medium, String value, String mxid) { public ThreePidMapping(String medium, String value, String mxid) {
setMedium(medium); setMedium(medium);
setValue(value); setValue(value);

View File

@@ -60,7 +60,7 @@ public class GoogleFirebaseProvider implements IThreePidProvider {
this(true); this(true);
this.domain = domain; this.domain = domain;
try { try {
fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db)); fbApp = FirebaseApp.initializeApp(getOpts(credsPath, db), "ThreePidProvider");
fbAuth = FirebaseAuth.getInstance(fbApp); fbAuth = FirebaseAuth.getInstance(fbApp);
log.info("Google Firebase Authentication is ready"); log.info("Google Firebase Authentication is ready");

View File

@@ -29,6 +29,8 @@ interface LookupStrategy {
List<IThreePidProvider> getLocalProviders() List<IThreePidProvider> getLocalProviders()
Optional<?> find(String medium, String address, boolean recursive)
Optional<?> find(SingleLookupRequest request) Optional<?> find(SingleLookupRequest request)
Optional<?> findRecursive(SingleLookupRequest request) Optional<?> findRecursive(SingleLookupRequest request)

View File

@@ -121,6 +121,16 @@ class RecursivePriorityLookupStrategy implements LookupStrategy, InitializingBea
}).collect(Collectors.toList()) }).collect(Collectors.toList())
} }
@Override
Optional<?> find(String medium, String address, boolean recursive) {
SingleLookupRequest req = new SingleLookupRequest();
req.setType(medium)
req.setThreePid(address)
req.setRequester("Internal")
return find(req, recursive)
}
@Override
Optional<?> find(SingleLookupRequest request, boolean forceRecursive) { Optional<?> find(SingleLookupRequest request, boolean forceRecursive) {
for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) { for (IThreePidProvider provider : listUsableProviders(request, forceRecursive)) {
Optional<?> lookupDataOpt = provider.find(request) Optional<?> lookupDataOpt = provider.find(request)