Add support for Profile feature in REST Identity store (Fix #91)

This commit is contained in:
Max Dor
2018-12-21 19:21:15 +01:00
parent ad1b91f370
commit 92cf5c6b21
7 changed files with 462 additions and 49 deletions

View File

@@ -0,0 +1,147 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 Kamax Sarl
*
* https://www.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.JsonObject;
import com.google.gson.JsonSyntaxException;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.matrix.json.InvalidJsonException;
import io.kamax.mxisd.config.rest.RestBackendConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.profile.JsonProfileRequest;
import io.kamax.mxisd.profile.JsonProfileResult;
import io.kamax.mxisd.profile.ProfileProvider;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
@Component
public class RestProfileProvider extends RestProvider implements ProfileProvider {
private final Logger log = LoggerFactory.getLogger(RestProfileProvider.class);
public RestProfileProvider(RestBackendConfig cfg) {
super(cfg);
}
@Override
public boolean isEnabled() {
return cfg.isEnabled() && cfg.getEndpoints().getProfile().isPresent();
}
private <T> Optional<T> doRequest(
_MatrixID userId,
Function<RestBackendConfig.ProfileEndpoints, Optional<String>> endpoint,
Function<JsonProfileResult, Optional<T>> value
) {
return cfg.getEndpoints().getProfile()
// We get the endpoint
.flatMap(endpoint)
// We only continue if there is a value
.filter(StringUtils::isNotBlank)
// We use the endpoint
.flatMap(url -> {
try {
URIBuilder builder = new URIBuilder(url);
HttpPost req = new HttpPost(builder.build());
req.setEntity(new StringEntity(GsonUtil.get().toJson(new JsonProfileRequest(userId)), ContentType.APPLICATION_JSON));
try (CloseableHttpResponse res = client.execute(req)) {
int sc = res.getStatusLine().getStatusCode();
if (sc == 404) {
log.info("Got 404 - No result found");
return Optional.empty();
}
if (sc != 200) {
throw new InternalServerError("Unexpected backed status code: " + sc);
}
String body = IOUtils.toString(res.getEntity().getContent(), StandardCharsets.UTF_8);
if (StringUtils.isBlank(body)) {
log.warn("Backend response body is empty/blank, expected JSON object with profile key");
return Optional.empty();
}
Optional<JsonObject> pJson = GsonUtil.findObj(GsonUtil.parseObj(body), "profile");
if (!pJson.isPresent()) {
log.warn("Backend response body is invalid, expected JSON object with profile key");
return Optional.empty();
}
JsonProfileResult profile = gson.fromJson(pJson.get(), JsonProfileResult.class);
return value.apply(profile);
}
} catch (JsonSyntaxException | InvalidJsonException e) {
log.error("Unable to parse backend response as JSON", e);
throw new InternalServerError(e);
} catch (URISyntaxException e) {
log.error("Unable to build a valid request URL", e);
throw new InternalServerError(e);
} catch (IOException e) {
log.error("I/O Error during backend request", e);
throw new InternalServerError();
}
});
}
@Override
public Optional<String> getDisplayName(_MatrixID userId) {
return doRequest(userId, p -> Optional.ofNullable(p.getDisplayName()), profile -> Optional.ofNullable(profile.getDisplayName()));
}
@Override
public List<_ThreePid> getThreepids(_MatrixID userId) {
return doRequest(userId, p -> Optional.ofNullable(p.getThreepids()), profile -> {
List<_ThreePid> t = new ArrayList<>();
if (Objects.nonNull(profile.getThreepids())) {
t.addAll(profile.getThreepids());
}
return Optional.of(t);
}).orElseGet(Collections::emptyList);
}
@Override
public List<String> getRoles(_MatrixID userId) {
return doRequest(userId, p -> Optional.ofNullable(p.getRoles()), profile -> {
List<String> t = new ArrayList<>();
if (Objects.nonNull(profile.getRoles())) {
t.addAll(profile.getRoles());
}
return Optional.of(t);
}).orElseGet(Collections::emptyList);
}
}

View File

@@ -30,6 +30,8 @@ import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Objects;
import java.util.Optional;
@Configuration
@ConfigurationProperties("rest")
@@ -58,11 +60,44 @@ public class RestBackendConfig {
}
public static class ProfileEndpoints {
private String displayName;
private String threepids;
private String roles;
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getThreepids() {
return threepids;
}
public void setThreepids(String threepids) {
this.threepids = threepids;
}
public String getRoles() {
return roles;
}
public void setRoles(String roles) {
this.roles = roles;
}
}
public static class Endpoints {
private String auth;
private String directory;
private IdentityEndpoints identity = new IdentityEndpoints();
private ProfileEndpoints profile;
public String getAuth() {
return auth;
@@ -88,6 +123,14 @@ public class RestBackendConfig {
this.identity = identity;
}
public Optional<ProfileEndpoints> getProfile() {
return Optional.ofNullable(profile);
}
public void setProfile(ProfileEndpoints profile) {
this.profile = profile;
}
}
private Logger log = LoggerFactory.getLogger(RestBackendConfig.class);
@@ -121,21 +164,21 @@ public class RestBackendConfig {
}
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 {
if (!StringUtils.startsWith(endpoint, "/")) {
return 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;
}
@PostConstruct
@@ -149,6 +192,12 @@ public class RestBackendConfig {
endpoints.identity.setSingle(buildEndpointUrl(endpoints.identity.getSingle()));
endpoints.identity.setBulk(buildEndpointUrl(endpoints.identity.getBulk()));
if (Objects.nonNull(endpoints.profile)) {
endpoints.profile.setDisplayName(buildEndpointUrl(endpoints.profile.getDisplayName()));
endpoints.profile.setThreepids(buildEndpointUrl(endpoints.profile.getThreepids()));
endpoints.profile.setRoles(buildEndpointUrl(endpoints.profile.getRoles()));
}
log.info("Host: {}", getHost());
log.info("Auth endpoint: {}", endpoints.getAuth());
log.info("Directory endpoint: {}", endpoints.getDirectory());

View File

@@ -56,6 +56,10 @@ rest:
identity:
single: '/_mxisd/backend/api/v1/identity/lookup/single'
bulk: '/_mxisd/backend/api/v1/identity/lookup/bulk'
profile:
displayName: '/_mxisd/backend/api/v1/profile/displayName'
threepids: '/_mxisd/backend/api/v1/profile/threepids'
roles: '/_mxisd/backend/api/v1/profile/roles'
ldap:
enabled: false

View File

@@ -0,0 +1,146 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2018 Kamax Sarl
*
* https://www.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.github.tomakehurst.wiremock.junit.WireMockRule;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.rest.RestBackendConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.profile.JsonProfileRequest;
import io.kamax.mxisd.profile.JsonProfileResult;
import org.apache.http.entity.ContentType;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import java.util.Optional;
import static com.github.tomakehurst.wiremock.client.WireMock.*;
import static io.kamax.mxisd.config.rest.RestBackendConfig.ProfileEndpoints;
import static org.junit.Assert.*;
public class RestProfileProviderTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule(65000);
private final String displayNameEndpoint = "/displayName";
private final _MatrixID userId = MatrixID.from("john", "matrix.localhost").valid();
private RestProfileProvider p;
@Before
public void before() {
ProfileEndpoints endpoints = new ProfileEndpoints();
endpoints.setDisplayName(displayNameEndpoint);
RestBackendConfig cfg = new RestBackendConfig();
cfg.setEnabled(true);
cfg.setHost("http://localhost:65000");
cfg.getEndpoints().setProfile(endpoints);
cfg.build();
p = new RestProfileProvider(cfg);
}
@Test
public void forNameFound() {
String value = "This is my display name";
JsonProfileResult r = new JsonProfileResult();
r.setDisplayName(value);
stubFor(post(urlEqualTo(displayNameEndpoint))
.willReturn(aResponse()
.withHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType())
.withBody(GsonUtil.get().toJson(GsonUtil.makeObj("profile", r)))
)
);
Optional<String> v = p.getDisplayName(userId);
verify(postRequestedFor(urlMatching(displayNameEndpoint))
.withHeader("Content-Type", containing(ContentType.APPLICATION_JSON.getMimeType()))
.withRequestBody(equalTo(GsonUtil.get().toJson(new JsonProfileRequest(userId))))
);
assertTrue(v.isPresent());
assertEquals(value, v.get());
}
@Test
public void forNameNotFound() {
stubFor(post(urlEqualTo(displayNameEndpoint))
.willReturn(aResponse()
.withStatus(404)
)
);
Optional<String> v = p.getDisplayName(userId);
verify(postRequestedFor(urlMatching(displayNameEndpoint))
.withHeader("Content-Type", containing(ContentType.APPLICATION_JSON.getMimeType()))
.withRequestBody(equalTo(GsonUtil.get().toJson(new JsonProfileRequest(userId))))
);
assertFalse(v.isPresent());
}
@Test
public void forNameEmptyBody() {
stubFor(post(urlEqualTo(displayNameEndpoint))
.willReturn(aResponse()
.withHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType())
)
);
Optional<String> v = p.getDisplayName(userId);
verify(postRequestedFor(urlMatching(displayNameEndpoint))
.withHeader("Content-Type", containing(ContentType.APPLICATION_JSON.getMimeType()))
.withRequestBody(equalTo(GsonUtil.get().toJson(new JsonProfileRequest(userId))))
);
assertFalse(v.isPresent());
}
@Test(expected = InternalServerError.class)
public void forNameInvalidBody() {
stubFor(post(urlEqualTo(displayNameEndpoint))
.willReturn(aResponse()
.withHeader("Content-Type", ContentType.APPLICATION_JSON.getMimeType())
.withBody("This is not a valid JSON object")
)
);
try {
p.getDisplayName(userId);
} finally {
verify(postRequestedFor(urlMatching(displayNameEndpoint))
.withHeader("Content-Type", containing(ContentType.APPLICATION_JSON.getMimeType()))
.withRequestBody(equalTo(GsonUtil.get().toJson(new JsonProfileRequest(userId))))
);
}
}
}