Further progress on Exec Identity Store

This commit is contained in:
Max Dor
2018-10-29 07:00:07 +01:00
parent b881f73798
commit 026a2e82d9
14 changed files with 534 additions and 65 deletions

View File

@@ -28,7 +28,9 @@ import io.kamax.mxisd.UserID;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.exception.ConfigurationException;
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.slf4j.Logger;
@@ -42,6 +44,8 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Component
@@ -49,11 +53,74 @@ public class ExecAuthStore extends ExecStore implements AuthenticatorProvider {
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;
@Autowired
public ExecAuthStore(ExecConfig cfg) {
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
@@ -75,31 +142,17 @@ public class ExecAuthStore extends ExecStore implements AuthenticatorProvider {
List<String> args = new ArrayList<>();
args.add(cfg.getCommand());
args.addAll(cfg.getArgs().stream().map(arg -> arg
.replace(cfg.getToken().getLocalpart(), uId.getLocalPart())
.replace(cfg.getToken().getDomain(), uId.getDomain())
.replace(cfg.getToken().getMxid(), uId.getId())
.replace(cfg.getToken().getPassword(), password)
).collect(Collectors.toList()));
args.addAll(cfg.getArgs().stream().map(arg -> inputMapper.apply(arg, uId, password)).collect(Collectors.toList()));
psExec.command(args);
psExec.environment(new HashMap<>(cfg.getEnv()).entrySet().stream().peek(e -> {
e.setValue(e.getValue().replace(cfg.getToken().getLocalpart(), uId.getLocalPart()));
e.setValue(e.getValue().replace(cfg.getToken().getDomain(), uId.getDomain()));
e.setValue(e.getValue().replace(cfg.getToken().getMxid(), uId.getId()));
e.setValue(e.getValue().replace(cfg.getToken().getPassword(), password));
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
psExec.environment(new HashMap<>(cfg.getEnv()).entrySet().stream()
.peek(e -> e.setValue(inputMapper.apply(e.getValue(), uId, password)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
if (StringUtils.isNotBlank(cfg.getInput())) {
if (StringUtils.equals("json", cfg.getInput())) {
JsonObject input = new JsonObject();
input.addProperty("localpart", uId.getLocalPart());
input.addProperty("mxid", uId.getId());
input.addProperty("password", password);
psExec.redirectInput(IOUtils.toInputStream(GsonUtil.get().toJson(input), StandardCharsets.UTF_8));
} else {
throw new InternalServerError(cfg.getInput() + " is not a valid executable input format");
}
if (StringUtils.isNotBlank(cfg.getInput().getType())) {
String template = cfg.getInput().getTemplate().orElseGet(inputTemplates.get(cfg.getInput().getType()));
String input = inputMapper.apply(template, uId, password);
psExec.redirectInput(IOUtils.toInputStream(input, StandardCharsets.UTF_8));
}
try {
@@ -107,28 +160,23 @@ public class ExecAuthStore extends ExecStore implements AuthenticatorProvider {
ProcessResult psResult = psExec.execute();
result.setExitStatus(psResult.getExitValue());
String output = psResult.outputUTF8();
log.debug("Command output:{}{}", System.lineSeparator(), output);
log.info("Exit status: {}", result.getExitStatus());
if (cfg.getExit().getSuccess().contains(result.getExitStatus())) {
result.setSuccess(true);
if (result.isSuccess()) {
if (StringUtils.equals("json", cfg.getOutput())) {
JsonObject data = GsonUtil.parseObj(output);
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));
} else {
log.debug("Command output:{}{}", "\n", output);
if (result.isSuccess() && StringUtils.isNotEmpty(output)) {
outputMapper.get(cfg.getOutput().getType()).accept(output, result);
} else {
if (StringUtils.isNotEmpty(output)) {
log.info("Exec auth failed with output:{}{}", System.lineSeparator(), output);
}
}
} else if (cfg.getExit().getFailure().contains(result.getExitStatus())) {
log.debug("{} stdout:{}{}", cfg.getCommand(), "\n", output);
log.debug("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
result.setSuccess(false);
} else {
log.error("{} stdout:{}{}", cfg.getCommand(), "\n", output);
log.error("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec auth command returned with unexpected exit status");
}

View File

@@ -20,17 +20,26 @@
package io.kamax.mxisd.backend.exec;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchResult;
import io.kamax.mxisd.directory.IDirectoryProvider;
import io.kamax.mxisd.exception.NotImplementedException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ExecDirectoryStore extends ExecStore implements IDirectoryProvider {
private ExecConfig.Directory cfg;
@Autowired
public ExecDirectoryStore(ExecConfig cfg) {
this.cfg = cfg.getDirectory();
}
@Override
public boolean isEnabled() {
return false;
return cfg.isEnabled();
}
@Override

View File

@@ -20,22 +20,129 @@
package io.kamax.mxisd.backend.exec;
import com.google.gson.JsonObject;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.exception.ConfigurationException;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.exception.NotImplementedException;
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.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.ProcessResult;
import java.util.List;
import java.util.Optional;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
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;
@Component
public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
private final Logger log = LoggerFactory.getLogger(ExecIdentityStore.class);
private final ExecConfig.Identity cfg;
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
public ExecIdentityStore(ExecConfig cfg, MatrixConfig mxCfg) {
this.cfg = cfg.getIdentity();
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
public boolean isEnabled() {
return false;
return cfg.isEnabled();
}
@Override
@@ -45,12 +152,53 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
@Override
public int getPriority() {
return 0;
return cfg.getPriority();
}
@Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
throw new NotImplementedException(this.getClass().getName());
ProcessExecutor psExec = new ProcessExecutor().readOutput(true);
List<String> args = new ArrayList<>();
args.add(cfg.getCommand());
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()
.peek(e -> e.setValue(singleInputMap.apply(e.getValue(), request)))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
if (StringUtils.isNotBlank(cfg.getInput().getType())) {
String template = cfg.getInput().getTemplate().orElseGet(singleInputTemplates.get(cfg.getInput().getType()));
String input = singleInputMap.apply(template, request);
psExec.redirectInput(IOUtils.toInputStream(input, StandardCharsets.UTF_8));
}
try {
log.info("Executing {}", cfg.getCommand());
ProcessResult psResult = psExec.execute();
String output = psResult.outputUTF8();
log.debug("Command output:{}{}", System.lineSeparator(), output);
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())
.apply(output)
.map(mxId -> new SingleLookupReply(request, mxId));
} else if (cfg.getExit().getFailure().contains(psResult.getExitValue())) {
log.debug("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
return Optional.empty();
} else {
log.error("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec auth command returned with unexpected exit status");
}
} catch (IOException | InterruptedException | TimeoutException e) {
throw new InternalServerError(e);
}
}
@Override

View File

@@ -22,8 +22,10 @@ package io.kamax.mxisd.backend.exec;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.exception.NotImplementedException;
import io.kamax.mxisd.profile.ProfileProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
@@ -32,9 +34,16 @@ import java.util.Optional;
@Component
public class ExecProfileStore extends ExecStore implements ProfileProvider {
private ExecConfig.Profile cfg;
@Autowired
public ExecProfileStore(ExecConfig cfg) {
this.cfg = cfg.getProfile();
}
@Override
public boolean isEnabled() {
return false;
return cfg.isEnabled();
}
@Override

View File

@@ -22,6 +22,7 @@ package io.kamax.mxisd.backend.exec;
public abstract class ExecStore {
// no-op
public static final String JsonType = "json";
public static final String MultilinesType = "multilines";
}