Add skeleton support for Directory and Identity in Exec IdStore

This commit is contained in:
Max Dor
2018-10-31 03:49:06 +01:00
parent 026a2e82d9
commit b892d19023
10 changed files with 518 additions and 254 deletions

View File

@@ -27,100 +27,28 @@ import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.UserID; import io.kamax.mxisd.UserID;
import io.kamax.mxisd.UserIdType; import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider; import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.backend.rest.RestAuthRequestJson;
import io.kamax.mxisd.config.ExecConfig; import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.util.TriFunction;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
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;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.ProcessResult;
import java.io.IOException; import java.util.Objects;
import java.nio.charset.StandardCharsets; import java.util.Optional;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Component @Component
public class ExecAuthStore extends ExecStore implements AuthenticatorProvider { public class ExecAuthStore extends ExecStore implements AuthenticatorProvider {
private final transient Logger log = LoggerFactory.getLogger(ExecAuthStore.class); private final transient Logger log = LoggerFactory.getLogger(ExecAuthStore.class);
private Map<String, Supplier<String>> inputTemplates;
private Map<String, BiConsumer<String, ExecAuthResult>> outputMapper;
private TriFunction<String, _MatrixID, String, String> inputMapper;
private ExecConfig.Auth cfg; private ExecConfig.Auth cfg;
@Autowired @Autowired
public ExecAuthStore(ExecConfig cfg) { public ExecAuthStore(ExecConfig cfg) {
this.cfg = Objects.requireNonNull(cfg.getAuth()); this.cfg = Objects.requireNonNull(cfg.getAuth());
inputTemplates = new HashMap<>();
inputTemplates.put(JsonType, () -> {
JsonObject json = new JsonObject();
json.addProperty("localpart", cfg.getToken().getLocalpart());
json.addProperty("domain", cfg.getToken().getDomain());
json.addProperty("mxid", cfg.getToken().getMxid());
json.addProperty("password", cfg.getToken().getPassword());
return GsonUtil.get().toJson(json);
});
inputTemplates.put(MultilinesType, () -> cfg.getToken().getLocalpart() + System.lineSeparator() +
cfg.getToken().getDomain() + System.lineSeparator() +
cfg.getToken().getMxid() + System.lineSeparator() +
cfg.getToken().getPassword() + System.lineSeparator()
);
inputMapper = (input, uId, password) -> input.replace(cfg.getToken().getLocalpart(), uId.getLocalPart())
.replace(cfg.getToken().getDomain(), uId.getDomain())
.replace(cfg.getToken().getMxid(), uId.getId())
.replace(cfg.getToken().getPassword(), password);
outputMapper = new HashMap<>();
outputMapper.put(JsonType, (output, result) -> {
JsonObject data = GsonUtil.getObj(GsonUtil.parseObj(output), "auth");
GsonUtil.findPrimitive(data, "success")
.map(JsonPrimitive::getAsBoolean)
.ifPresent(result::setSuccess);
GsonUtil.findObj(data, "profile")
.flatMap(p -> GsonUtil.findString(p, "display_name"))
.ifPresent(v -> result.getProfile().setDisplayName(v));
});
outputMapper.put(MultilinesType, (output, result) -> {
String[] lines = output.split("\\R");
if (lines.length > 2) {
throw new InternalServerError("Exec auth command returned more than 2 lines (" + lines.length + ")");
}
result.setSuccess(Optional.ofNullable(StringUtils.isEmpty(lines[0]) ? null : lines[0])
.map(v -> StringUtils.equalsAnyIgnoreCase(v, "true", "1"))
.orElse(result.isSuccess()));
if (lines.length == 2) {
Optional.ofNullable(StringUtils.isEmpty(lines[1]) ? null : lines[1])
.ifPresent(v -> result.getProfile().setDisplayName(v));
}
});
validateConfig();
}
private void validateConfig() {
if (StringUtils.isNotEmpty(cfg.getInput().getType()) && !inputTemplates.containsKey(cfg.getInput().getType())) {
throw new ConfigurationException("Exec Auth input type is not valid: " + cfg.getInput().getType());
}
if (StringUtils.isNotEmpty(cfg.getOutput().getType()) && !outputMapper.containsKey(cfg.getOutput().getType())) {
throw new ConfigurationException("Exec Auth output type is not valid: " + cfg.getInput().getType());
}
} }
@Override @Override
@@ -138,52 +66,64 @@ public class ExecAuthStore extends ExecStore implements AuthenticatorProvider {
ExecAuthResult result = new ExecAuthResult(); ExecAuthResult result = new ExecAuthResult();
result.setId(new UserID(UserIdType.Localpart, uId.getLocalPart())); result.setId(new UserID(UserIdType.Localpart, uId.getLocalPart()));
ProcessExecutor psExec = new ProcessExecutor().readOutput(true); Processor<ExecAuthResult> process = new Processor<>();
process.withConfig(cfg);
process.addTokenMapper(cfg.getToken().getLocalpart(), uId::getLocalPart);
process.addTokenMapper(cfg.getToken().getDomain(), uId::getDomain);
process.addTokenMapper(cfg.getToken().getMxid(), uId::getId);
process.addTokenMapper(cfg.getToken().getPassword(), () -> password);
List<String> args = new ArrayList<>(); process.addInputTemplate(JsonType, tokens -> {
args.add(cfg.getCommand()); RestAuthRequestJson json = new RestAuthRequestJson();
args.addAll(cfg.getArgs().stream().map(arg -> inputMapper.apply(arg, uId, password)).collect(Collectors.toList())); json.setLocalpart(tokens.getLocalpart());
psExec.command(args); json.setDomain(tokens.getDomain());
json.setMxid(tokens.getMxid());
json.setPassword(tokens.getPassword());
return GsonUtil.get().toJson(json);
});
process.addInputTemplate(MultilinesType, tokens -> tokens.getLocalpart() + System.lineSeparator() +
tokens.getDomain() + System.lineSeparator() +
tokens.getMxid() + System.lineSeparator() +
tokens.getPassword() + System.lineSeparator()
);
psExec.environment(new HashMap<>(cfg.getEnv()).entrySet().stream() process.withExitHandler(pr -> result.setExitStatus(pr.getExitValue()));
.peek(e -> e.setValue(inputMapper.apply(e.getValue(), uId, password)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
if (StringUtils.isNotBlank(cfg.getInput().getType())) { process.withSuccessHandler(pr -> result.setSuccess(true));
String template = cfg.getInput().getTemplate().orElseGet(inputTemplates.get(cfg.getInput().getType())); process.withSuccessDefault(o -> result);
String input = inputMapper.apply(template, uId, password); process.addSuccessMapper(JsonType, output -> {
psExec.redirectInput(IOUtils.toInputStream(input, StandardCharsets.UTF_8)); JsonObject data = GsonUtil.getObj(GsonUtil.parseObj(output), "auth");
} GsonUtil.findPrimitive(data, "success")
.map(JsonPrimitive::getAsBoolean)
.ifPresent(result::setSuccess);
GsonUtil.findObj(data, "profile")
.flatMap(p -> GsonUtil.findString(p, "display_name"))
.ifPresent(v -> result.getProfile().setDisplayName(v));
try { return result;
log.info("Executing {}", cfg.getCommand()); });
ProcessResult psResult = psExec.execute(); process.addSuccessMapper(MultilinesType, output -> {
result.setExitStatus(psResult.getExitValue()); String[] lines = output.split("\\R");
String output = psResult.outputUTF8(); if (lines.length > 2) {
log.debug("Command output:{}{}", System.lineSeparator(), output); throw new InternalServerError("Exec auth command returned more than 2 lines (" + lines.length + ")");
}
log.info("Exit status: {}", result.getExitStatus()); result.setSuccess(Optional.ofNullable(StringUtils.isEmpty(lines[0]) ? null : lines[0])
if (cfg.getExit().getSuccess().contains(result.getExitStatus())) { .map(v -> StringUtils.equalsAnyIgnoreCase(v, "true", "1"))
result.setSuccess(true); .orElse(result.isSuccess()));
if (result.isSuccess() && StringUtils.isNotEmpty(output)) {
outputMapper.get(cfg.getOutput().getType()).accept(output, result); if (lines.length == 2) {
} else { Optional.ofNullable(StringUtils.isEmpty(lines[1]) ? null : lines[1])
if (StringUtils.isNotEmpty(output)) { .ifPresent(v -> result.getProfile().setDisplayName(v));
log.info("Exec auth failed with output:{}{}", System.lineSeparator(), output);
}
}
} else if (cfg.getExit().getFailure().contains(result.getExitStatus())) {
log.debug("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
result.setSuccess(false);
} else {
log.error("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec auth command returned with unexpected exit status");
} }
return result; return result;
} catch (IOException | InterruptedException | TimeoutException e) { });
throw new InternalServerError(e);
} process.withFailureHandler(pr -> result.setSuccess(false));
process.withFailureDefault(o -> result);
return process.execute();
} }
} }

View File

@@ -20,10 +20,13 @@
package io.kamax.mxisd.backend.exec; package io.kamax.mxisd.backend.exec;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.ExecConfig; import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchRequest;
import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchResult; import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchResult;
import io.kamax.mxisd.directory.IDirectoryProvider; import io.kamax.mxisd.directory.IDirectoryProvider;
import io.kamax.mxisd.exception.NotImplementedException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -31,10 +34,12 @@ import org.springframework.stereotype.Component;
public class ExecDirectoryStore extends ExecStore implements IDirectoryProvider { public class ExecDirectoryStore extends ExecStore implements IDirectoryProvider {
private ExecConfig.Directory cfg; private ExecConfig.Directory cfg;
private MatrixConfig mxCfg;
@Autowired @Autowired
public ExecDirectoryStore(ExecConfig cfg) { public ExecDirectoryStore(ExecConfig cfg, MatrixConfig mxCfg) {
this.cfg = cfg.getDirectory(); this.cfg = cfg.getDirectory();
this.mxCfg = mxCfg;
} }
@Override @Override
@@ -42,14 +47,35 @@ public class ExecDirectoryStore extends ExecStore implements IDirectoryProvider
return cfg.isEnabled(); return cfg.isEnabled();
} }
private UserDirectorySearchResult search(ExecConfig.Process cfg, UserDirectorySearchRequest request) {
Processor<UserDirectorySearchResult> processor = new Processor<>();
processor.withConfig(cfg);
processor.addInputTemplate(JsonType, tokens -> GsonUtil.get().toJson(new UserDirectorySearchRequest(tokens.getType(), tokens.getQuery())));
processor.addInputTemplate(MultilinesType, tokens -> tokens.getType() + System.lineSeparator() + tokens.getQuery());
processor.addTokenMapper(cfg.getToken().getType(), request::getBy);
processor.addTokenMapper(cfg.getToken().getQuery(), request::getSearchTerm);
processor.addSuccessMapper(JsonType, output -> {
UserDirectorySearchResult response = GsonUtil.get().fromJson(output, UserDirectorySearchResult.class);
for (UserDirectorySearchResult.Result result : response.getResults()) {
result.setUserId(MatrixID.asAcceptable(result.getUserId(), mxCfg.getDomain()).getId());
}
return response;
});
processor.withFailureDefault(output -> new UserDirectorySearchResult());
return processor.execute();
}
@Override @Override
public UserDirectorySearchResult searchByDisplayName(String query) { public UserDirectorySearchResult searchByDisplayName(String query) {
throw new NotImplementedException(this.getClass().getName()); return search(cfg.getSearch().getByName(), new UserDirectorySearchRequest("name", query));
} }
@Override @Override
public UserDirectorySearchResult searchBy3pid(String query) { public UserDirectorySearchResult searchBy3pid(String query) {
throw new NotImplementedException(this.getClass().getName()); return search(cfg.getSearch().getByName(), new UserDirectorySearchRequest("threepid", query));
} }
} }

View File

@@ -20,35 +20,29 @@
package io.kamax.mxisd.backend.exec; package io.kamax.mxisd.backend.exec;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject; import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID; import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID; import io.kamax.matrix.ThreePid;
import io.kamax.matrix.json.GsonUtil; import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.backend.rest.LookupBulkResponseJson;
import io.kamax.mxisd.config.ExecConfig; import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.config.MatrixConfig; import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.InternalServerError; import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.NotImplementedException;
import io.kamax.mxisd.lookup.SingleLookupReply; import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest; import io.kamax.mxisd.lookup.SingleLookupRequest;
import io.kamax.mxisd.lookup.ThreePidMapping; import io.kamax.mxisd.lookup.ThreePidMapping;
import io.kamax.mxisd.lookup.provider.IThreePidProvider; import io.kamax.mxisd.lookup.provider.IThreePidProvider;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
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;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.ProcessResult;
import java.io.IOException; import java.util.Collections;
import java.nio.charset.StandardCharsets; import java.util.List;
import java.util.*; import java.util.Optional;
import java.util.concurrent.TimeoutException;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@Component @Component
@@ -59,85 +53,10 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
private final ExecConfig.Identity cfg; private final ExecConfig.Identity cfg;
private final MatrixConfig mxCfg; private final MatrixConfig mxCfg;
private BiFunction<String, SingleLookupRequest, String> singleInputMap;
private Map<String, Supplier<String>> singleInputTemplates;
private Map<String, Function<String, Optional<_MatrixID>>> singleOutputMap;
@Autowired @Autowired
public ExecIdentityStore(ExecConfig cfg, MatrixConfig mxCfg) { public ExecIdentityStore(ExecConfig cfg, MatrixConfig mxCfg) {
this.cfg = cfg.getIdentity(); this.cfg = cfg.getIdentity();
this.mxCfg = mxCfg; this.mxCfg = mxCfg;
singleInputMap = (v, request) -> v.replace(cfg.getToken().getMedium(), request.getType())
.replace(cfg.getToken().getAddress(), request.getThreePid());
singleInputTemplates = new HashMap<>();
singleInputTemplates.put(JsonType, () -> {
JsonObject json = new JsonObject();
json.addProperty("medium", cfg.getToken().getMedium());
json.addProperty("address", cfg.getToken().getAddress());
return GsonUtil.get().toJson(json);
});
singleInputTemplates.put(MultilinesType, () -> cfg.getToken().getMedium()
+ System.lineSeparator()
+ cfg.getToken().getAddress()
);
singleOutputMap = new HashMap<>();
singleOutputMap.put(JsonType, output -> {
if (StringUtils.isBlank(output)) {
return Optional.empty();
}
return GsonUtil.findObj(GsonUtil.parseObj(output), "lookup").map(lookup -> {
String type = GsonUtil.getStringOrThrow(lookup, "type");
String value = GsonUtil.getStringOrThrow(lookup, "value");
if (StringUtils.equals(type, "uid")) {
return MatrixID.asAcceptable(value, mxCfg.getDomain());
}
if (StringUtils.equals(type, "mxid")) {
return MatrixID.asAcceptable(value);
}
throw new InternalServerError("Invalid user type: " + type);
});
});
singleOutputMap.put(MultilinesType, output -> {
String[] lines = output.split("\\R");
if (lines.length > 2) {
throw new InternalServerError("Exec auth command returned more than 2 lines (" + lines.length + ")");
}
if (lines.length == 1 && StringUtils.isBlank(lines[0])) {
return Optional.empty();
}
String type = StringUtils.trimToEmpty(lines.length == 1 ? "uid" : lines[0]);
String value = StringUtils.trimToEmpty(lines.length == 2 ? lines[1] : lines[0]);
if (StringUtils.equals(type, "uid")) {
return Optional.of(MatrixID.asAcceptable(value, mxCfg.getDomain()));
}
if (StringUtils.equals(type, "mxid")) {
return Optional.of(MatrixID.asAcceptable(value));
}
throw new InternalServerError("Invalid user type: " + type);
});
validateConfig();
}
private void validateConfig() {
if (StringUtils.isNotEmpty(cfg.getInput().getType()) && !singleInputTemplates.containsKey(cfg.getInput().getType())) {
throw new ConfigurationException("Exec Identity Single Lookup: input type is not valid: " + cfg.getInput().getType());
}
if (StringUtils.isNotEmpty(cfg.getOutput().getType()) && !singleOutputMap.containsKey(cfg.getOutput().getType())) {
throw new ConfigurationException("Exec Auth output type is not valid: " + cfg.getInput().getType());
}
} }
@Override @Override
@@ -155,55 +74,125 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
return cfg.getPriority(); return cfg.getPriority();
} }
private ExecConfig.Process getSingleCfg() {
return cfg.getLookup().getSingle();
}
@Override @Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) { public Optional<SingleLookupReply> find(SingleLookupRequest request) {
ProcessExecutor psExec = new ProcessExecutor().readOutput(true); Processor<Optional<SingleLookupReply>> processor = new Processor<>();
processor.withConfig(cfg.getLookup().getSingle());
List<String> args = new ArrayList<>(); processor.addTokenMapper(getSingleCfg().getToken().getMedium(), request::getType);
args.add(cfg.getCommand()); processor.addTokenMapper(getSingleCfg().getToken().getAddress(), request::getThreePid);
args.addAll(cfg.getArgs().stream().map(arg -> singleInputMap.apply(arg, request)).collect(Collectors.toList()));
psExec.command(args);
psExec.environment(new HashMap<>(cfg.getEnv()).entrySet().stream() processor.addInputTemplate(JsonType, tokens -> {
.peek(e -> e.setValue(singleInputMap.apply(e.getValue(), request))) JsonObject json = new JsonObject();
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); json.addProperty("medium", tokens.getMedium());
json.addProperty("address", tokens.getAddress());
return GsonUtil.get().toJson(json);
});
processor.addInputTemplate(MultilinesType, tokens -> tokens.getMedium()
+ System.lineSeparator()
+ tokens.getAddress()
);
if (StringUtils.isNotBlank(cfg.getInput().getType())) { processor.addSuccessMapper(JsonType, output -> {
String template = cfg.getInput().getTemplate().orElseGet(singleInputTemplates.get(cfg.getInput().getType())); if (StringUtils.isBlank(output)) {
String input = singleInputMap.apply(template, request); return Optional.empty();
psExec.redirectInput(IOUtils.toInputStream(input, StandardCharsets.UTF_8)); }
}
try { return GsonUtil.findObj(GsonUtil.parseObj(output), "lookup").map(lookup -> {
log.info("Executing {}", cfg.getCommand()); String type = GsonUtil.getStringOrThrow(lookup, "type");
ProcessResult psResult = psExec.execute(); String value = GsonUtil.getStringOrThrow(lookup, "value");
String output = psResult.outputUTF8(); if (UserIdType.Localpart.is(type)) {
log.debug("Command output:{}{}", System.lineSeparator(), output); return MatrixID.asAcceptable(value, mxCfg.getDomain());
log.info("Exit status: {}", psResult.getExitValue());
if (cfg.getExit().getSuccess().contains(psResult.getExitValue())) {
if (StringUtils.isBlank(output)) {
return Optional.empty();
} }
return singleOutputMap.get(cfg.getOutput().getType()) if (UserIdType.MatrixID.is(type)) {
.apply(output) return MatrixID.asAcceptable(value);
.map(mxId -> new SingleLookupReply(request, mxId)); }
} else if (cfg.getExit().getFailure().contains(psResult.getExitValue())) {
log.debug("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output); throw new InternalServerError("Invalid user type: " + type);
return Optional.empty(); }).map(mxId -> new SingleLookupReply(request, mxId));
} else { });
log.error("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec auth command returned with unexpected exit status"); processor.addSuccessMapper(MultilinesType, output -> {
String[] lines = output.split("\\R");
if (lines.length > 2) {
throw new InternalServerError("Exec auth command returned more than 2 lines (" + lines.length + ")");
} }
} catch (IOException | InterruptedException | TimeoutException e) {
throw new InternalServerError(e); if (lines.length == 1 && StringUtils.isBlank(lines[0])) {
} return Optional.empty();
}
String type = StringUtils.trimToEmpty(lines.length == 1 ? "uid" : lines[0]);
String value = StringUtils.trimToEmpty(lines.length == 2 ? lines[1] : lines[0]);
if (UserIdType.Localpart.is(type)) {
return Optional.of(new SingleLookupReply(request, MatrixID.asAcceptable(value, mxCfg.getDomain())));
}
if (UserIdType.MatrixID.is(type)) {
return Optional.of(new SingleLookupReply(request, MatrixID.asAcceptable(value)));
}
throw new InternalServerError("Invalid user type: " + type);
});
processor.withFailureDefault(o -> Optional.empty());
return processor.execute();
} }
@Override @Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) { public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
throw new NotImplementedException(this.getClass().getName()); Processor<List<ThreePidMapping>> processor = new Processor<>();
processor.withConfig(cfg.getLookup().getBulk());
processor.addInput(JsonType, () -> {
JsonArray tpids = GsonUtil.asArray(mappings.stream()
.map(mapping -> GsonUtil.get().toJsonTree(new ThreePid(mapping.getMedium(), mapping.getValue())))
.collect(Collectors.toList()));
return GsonUtil.get().toJson(GsonUtil.makeObj("lookup", tpids));
});
processor.addInput(MultilinesType, () -> {
StringBuilder input = new StringBuilder();
for (ThreePidMapping mapping : mappings) {
input.append(mapping.getMedium()).append("\t").append(mapping.getValue()).append(System.lineSeparator());
}
return input.toString();
});
processor.addSuccessMapper(JsonType, output -> {
if (StringUtils.isBlank(output)) {
return Collections.emptyList();
}
LookupBulkResponseJson response = GsonUtil.get().fromJson(output, LookupBulkResponseJson.class);
return response.getLookup().stream().map(item -> {
ThreePidMapping mapping = new ThreePidMapping();
mapping.setMedium(item.getMedium());
mapping.setValue(item.getAddress());
if (UserIdType.Localpart.is(item.getId().getType())) {
mapping.setValue(MatrixID.asAcceptable(item.getId().getValue(), mxCfg.getDomain()).getId());
return mapping;
}
if (UserIdType.MatrixID.is(item.getId().getType())) {
mapping.setValue(MatrixID.asAcceptable(item.getId().getValue()).getId());
return mapping;
}
throw new InternalServerError("Invalid user type: " + item.getId().getType());
}).collect(Collectors.toList());
});
processor.withFailureDefault(output -> Collections.emptyList());
return processor.execute();
} }
} }

View File

@@ -20,9 +20,227 @@
package io.kamax.mxisd.backend.exec; package io.kamax.mxisd.backend.exec;
public abstract class ExecStore { import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.exception.InternalServerError;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.ProcessResult;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class ExecStore {
public static final String JsonType = "json"; public static final String JsonType = "json";
public static final String MultilinesType = "multilines"; public static final String MultilinesType = "multilines";
private final Logger log = LoggerFactory.getLogger(ExecStore.class);
public class Processor<V> {
private ExecConfig.Process cfg;
private Supplier<Optional<String>> inputSupplier;
private Function<String, String> inputTypeMapper;
private Function<String, String> inputUnknownTypeMapper;
private Map<String, Supplier<String>> inputTypeSuppliers;
private Map<String, Function<ExecConfig.TokenOverride, String>> inputTypeTemplates;
private Supplier<String> inputTypeNoTemplateHandler;
private Map<String, Function<ExecConfig.TokenOverride, String>> inputTypeTokenizers;
private Map<String, Supplier<String>> tokenMappers;
private Function<String, String> tokenHandler;
private Consumer<ProcessResult> onExitHandler;
private Consumer<ProcessResult> successHandler;
private Map<String, Function<String, V>> successMappers;
private Function<String, V> successDefault;
private Consumer<ProcessResult> failureHandler;
private Map<String, Function<String, V>> failureMappers;
private Function<String, V> failureDefault;
private Consumer<ProcessResult> unknownHandler;
private Map<String, Function<String, V>> unknownMappers;
private Function<String, V> unknownDefault;
public Processor() {
tokenMappers = new HashMap<>();
inputTypeSuppliers = new HashMap<>();
inputTypeTemplates = new HashMap<>();
tokenHandler = input -> {
for (Map.Entry<String, Supplier<String>> entry : tokenMappers.entrySet()) {
input = input.replace(entry.getKey(), entry.getValue().get());
}
return input;
};
inputTypeNoTemplateHandler = () -> cfg.getInput().getType()
.map(type -> inputTypeTemplates.get(type).apply(cfg.getToken()))
.orElse("");
inputUnknownTypeMapper = type -> tokenHandler.apply(cfg.getInput().getTemplate().orElseGet(inputTypeNoTemplateHandler));
inputTypeMapper = type -> {
if (!inputTypeSuppliers.containsKey(type)) {
return inputUnknownTypeMapper.apply(type);
}
return inputTypeSuppliers.get(type).get();
};
inputSupplier = () -> cfg.getInput().getType().map(type -> inputTypeMapper.apply(type));
onExitHandler = pr -> {
};
successMappers = new HashMap<>();
successHandler = pr -> {
};
successDefault = output -> {
log.info("{} stdout: {}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec command has no success handler configured. This is a bug. Please report.");
};
failureHandler = pr -> {
};
failureMappers = new HashMap<>();
failureDefault = output -> {
log.info("{} stdout: {}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec command has no failure handler configured. This is a bug. Please report.");
};
unknownHandler = pr -> log.warn("Unexpected exit status: {}", pr.getExitValue());
unknownMappers = new HashMap<>();
unknownDefault = output -> {
log.error("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec command returned with unexpected exit status");
};
}
public Processor<V> withConfig(ExecConfig.Process cfg) {
this.cfg = cfg;
return this;
}
public Processor<V> addTokenMapper(String token, Supplier<String> data) {
tokenMappers.put(token, data);
return this;
}
public Processor<V> withTokenHandler(Function<String, String> tokenHandler) {
this.tokenHandler = tokenHandler;
return this;
}
public Processor<V> addInput(String type, Supplier<String> handler) {
inputTypeSuppliers.put(type, handler);
return this;
}
public Processor<V> addInputTemplate(String type, Function<ExecConfig.TokenOverride, String> template) {
inputTypeTemplates.put(type, template);
return this;
}
public Processor<V> withExitHandler(Consumer<ProcessResult> handler) {
onExitHandler = handler;
return this;
}
public Processor<V> withSuccessHandler(Consumer<ProcessResult> handler) {
successHandler = handler;
return this;
}
public Processor<V> addSuccessMapper(String type, Function<String, V> mapper) {
successMappers.put(type, mapper);
return this;
}
public Processor<V> withSuccessDefault(Function<String, V> mapper) {
successDefault = mapper;
return this;
}
public Processor<V> withFailureHandler(Consumer<ProcessResult> handler) {
failureHandler = handler;
return this;
}
public Processor<V> addFailureMapper(String type, Function<String, V> mapper) {
failureMappers.put(type, mapper);
return this;
}
public Processor<V> withFailureDefault(Function<String, V> mapper) {
failureDefault = mapper;
return this;
}
public Processor<V> addUnknownMapper(String type, Function<String, V> mapper) {
unknownMappers.put(type, mapper);
return this;
}
public Processor<V> withUnknownDefault(Function<String, V> mapper) {
unknownDefault = mapper;
return this;
}
V execute() {
log.info("Executing {}", cfg.getCommand());
try {
ProcessExecutor psExec = new ProcessExecutor().readOutput(true);
List<String> args = new ArrayList<>();
args.add(cfg.getCommand());
args.addAll(cfg.getArgs().stream().map(arg -> tokenHandler.apply(arg)).collect(Collectors.toList()));
psExec.command(args);
psExec.environment(new HashMap<>(cfg.getEnv()).entrySet().stream()
.peek(e -> e.setValue(tokenHandler.apply(e.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
inputSupplier.get().ifPresent(input -> psExec.redirectInput(IOUtils.toInputStream(input, StandardCharsets.UTF_8)));
ProcessResult psResult = psExec.execute();
String output = psResult.outputUTF8();
onExitHandler.accept(psResult);
if (cfg.getExit().getSuccess().contains(psResult.getExitValue())) {
successHandler.accept(psResult);
return cfg.getOutput().getType()
.map(type -> successMappers.getOrDefault(type, successDefault).apply(output))
.orElseGet(() -> successDefault.apply(output));
} else if (cfg.getExit().getFailure().contains(psResult.getExitValue())) {
failureHandler.accept(psResult);
return cfg.getOutput().getType()
.map(type -> failureMappers.getOrDefault(type, failureDefault).apply(output))
.orElseGet(() -> failureDefault.apply(output));
} else {
unknownHandler.accept(psResult);
return cfg.getOutput().getType()
.map(type -> unknownMappers.getOrDefault(type, unknownDefault).apply(output))
.orElseGet(() -> unknownDefault.apply(output));
}
} catch (IOException | InterruptedException | TimeoutException e) {
log.error("Failed to execute {}", cfg.getCommand());
throw new InternalServerError(e);
}
}
}
} }

View File

@@ -36,8 +36,8 @@ public class ExecConfig {
private String type; private String type;
private String template; private String template;
public String getType() { public Optional<String> getType() {
return type; return Optional.ofNullable(type);
} }
public void setType(String type) { public void setType(String type) {
@@ -83,6 +83,10 @@ public class ExecConfig {
private String domain; private String domain;
private String mxid; private String mxid;
private String password; private String password;
private String medium;
private String address;
private String type;
private String query;
public String getLocalpart() { public String getLocalpart() {
return StringUtils.defaultIfEmpty(localpart, getToken().getLocalpart()); return StringUtils.defaultIfEmpty(localpart, getToken().getLocalpart());
@@ -116,6 +120,38 @@ public class ExecConfig {
this.password = password; this.password = password;
} }
public String getMedium() {
return StringUtils.defaultIfEmpty(medium, getToken().getMedium());
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getAddress() {
return StringUtils.defaultIfEmpty(address, getToken().getAddress());
}
public void setAddress(String address) {
this.address = address;
}
public String getType() {
return StringUtils.defaultIfEmpty(type, getToken().getType());
}
public void setType(String type) {
this.type = type;
}
public String getQuery() {
return StringUtils.defaultIfEmpty(query, getToken().getQuery());
}
public void setQuery(String query) {
this.query = query;
}
} }
public class Token { public class Token {
@@ -126,6 +162,8 @@ public class ExecConfig {
private String password = "{password}"; private String password = "{password}";
private String medium = "{medium}"; private String medium = "{medium}";
private String address = "{address}"; private String address = "{address}";
private String type = "{type}";
private String query = "{query}";
public String getLocalpart() { public String getLocalpart() {
return localpart; return localpart;
@@ -175,6 +213,22 @@ public class ExecConfig {
this.address = address; this.address = address;
} }
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
} }
public class Process { public class Process {
@@ -311,10 +365,34 @@ public class ExecConfig {
} }
public class Identity extends Process { public class Lookup {
private Process single = new Process();
private Process bulk = new Process();
public Process getSingle() {
return single;
}
public void setSingle(Process single) {
this.single = single;
}
public Process getBulk() {
return bulk;
}
public void setBulk(Process bulk) {
this.bulk = bulk;
}
}
public class Identity {
private Boolean enabled; private Boolean enabled;
private int priority; private int priority;
private Lookup lookup = new Lookup();
public Boolean isEnabled() { public Boolean isEnabled() {
return enabled; return enabled;
@@ -332,6 +410,14 @@ public class ExecConfig {
this.priority = priority; this.priority = priority;
} }
public Lookup getLookup() {
return lookup;
}
public void setLookup(Lookup lookup) {
this.lookup = lookup;
}
} }
public class Profile extends Process { public class Profile extends Process {

View File

@@ -29,6 +29,11 @@ public class UserDirectorySearchRequest {
setSearchTerm(searchTerm); setSearchTerm(searchTerm);
} }
public UserDirectorySearchRequest(String type, String searchTerm) {
setBy(type);
setSearchTerm(searchTerm);
}
public String getBy() { public String getBy() {
return by; return by;
} }

View File

@@ -18,11 +18,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.backend.exec.input; package io.kamax.mxisd.backend.exec.auth.input;
import java.util.Arrays; import java.util.Arrays;
public class ArgsTest extends InputTest { public class ExecAuthArgsTest extends ExecAuthTest {
@Override @Override
protected void setValidCommand() { protected void setValidCommand() {

View File

@@ -18,11 +18,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.backend.exec.input; package io.kamax.mxisd.backend.exec.auth.input;
import java.util.HashMap; import java.util.HashMap;
public class EnvTest extends InputTest { public class ExecAuthEnvTest extends ExecAuthTest {
private final String LocalpartEnv = "LOCALPART"; private final String LocalpartEnv = "LOCALPART";
private final String DomainEnv = "DOMAIN"; private final String DomainEnv = "DOMAIN";

View File

@@ -18,11 +18,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.backend.exec.input; package io.kamax.mxisd.backend.exec.auth.input;
import io.kamax.mxisd.backend.exec.ExecStore; import io.kamax.mxisd.backend.exec.ExecStore;
public class MultilinesTest extends InputTest { public class ExecAuthInputMultilinesTest extends ExecAuthTest {
@Override @Override
protected void setValidCommand() { protected void setValidCommand() {

View File

@@ -18,7 +18,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package io.kamax.mxisd.backend.exec.input; package io.kamax.mxisd.backend.exec.auth.input;
import io.kamax.matrix.MatrixID; import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID; import io.kamax.matrix._MatrixID;
@@ -35,7 +35,7 @@ import java.util.Collections;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
public abstract class InputTest { public abstract class ExecAuthTest {
protected final ExecConfig cfg; protected final ExecConfig cfg;
protected final ExecAuthStore p; protected final ExecAuthStore p;
@@ -90,7 +90,7 @@ public abstract class InputTest {
cfg.getAuth().addEnv("REQ_PASS", requiredPass); cfg.getAuth().addEnv("REQ_PASS", requiredPass);
} }
public InputTest() { public ExecAuthTest() {
cfg = new ExecConfig(); cfg = new ExecConfig();
p = new ExecAuthStore(cfg); p = new ExecAuthStore(cfg);
} }