Further progress on Exec Identity Store
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user