Add initial experimental support for #58

- Skeleton for the whole identity store
- Support Authentication
This commit is contained in:
Max Dor
2018-10-20 08:08:14 +02:00
parent cb02f62b9d
commit 99d793b5ed
18 changed files with 1219 additions and 13 deletions

View File

@@ -1,8 +1,8 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
* Copyright (C) 2017 Kamax Sarl
*
* https://max.kamax.io/
* 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
@@ -30,6 +30,10 @@ public class UserID {
// stub for (de)serialization
}
public UserID(UserIdType type, String value) {
this(type.getId(), value);
}
public UserID(String type, String value) {
this.type = type;
this.value = value;

View File

@@ -1,8 +1,8 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
* Copyright (C) 2017 Kamax Sarl
*
* https://max.kamax.io/
* 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
@@ -59,9 +59,10 @@ public class AuthManager {
continue;
}
log.info("Attempting authentication with store {}", provider.getClass().getSimpleName());
BackendAuthResult result = provider.authenticate(mxid, password);
if (result.isSuccess()) {
String mxId;
if (UserIdType.Localpart.is(result.getId().getType())) {
mxId = MatrixID.from(result.getId().getValue(), mxCfg.getDomain()).acceptable().getId();

View File

@@ -1,8 +1,8 @@
/*
* mxisd - Matrix Identity Server Daemon
* Copyright (C) 2017 Maxime Dor
* Copyright (C) 2017 Kamax Sarl
*
* https://max.kamax.io/
* 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
@@ -38,6 +38,10 @@ public class BackendAuthResult {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public Set<ThreePid> getThreePids() {
return threePids;
}
@@ -73,6 +77,10 @@ public class BackendAuthResult {
private UserID id;
private BackendAuthProfile profile = new BackendAuthProfile();
public void setSuccess(boolean success) {
this.success = success;
}
public Boolean isSuccess() {
return success;
}
@@ -81,6 +89,10 @@ public class BackendAuthResult {
return id;
}
public void setId(UserID id) {
this.id = id;
}
public BackendAuthProfile getProfile() {
return profile;
}

View File

@@ -0,0 +1,37 @@
/*
* 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.exec;
import io.kamax.mxisd.auth.provider.BackendAuthResult;
public class ExecAuthResult extends BackendAuthResult {
private int exitStatus;
public int getExitStatus() {
return exitStatus;
}
public void setExitStatus(int exitStatus) {
this.exitStatus = exitStatus;
}
}

View File

@@ -0,0 +1,141 @@
/*
* 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.exec;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix.json.GsonUtil;
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.InternalServerError;
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.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
@Component
public class ExecAuthStore extends ExecStore implements AuthenticatorProvider {
private final transient Logger log = LoggerFactory.getLogger(ExecAuthStore.class);
private ExecConfig.Auth cfg;
@Autowired
public ExecAuthStore(ExecConfig cfg) {
this.cfg = Objects.requireNonNull(cfg.getAuth());
}
@Override
public boolean isEnabled() {
return cfg.isEnabled();
}
@Override
public ExecAuthResult authenticate(_MatrixID uId, String password) {
Objects.requireNonNull(uId);
Objects.requireNonNull(password);
log.info("Performing authentication for {}", uId.getId());
ExecAuthResult result = new ExecAuthResult();
result.setId(new UserID(UserIdType.Localpart, uId.getLocalPart()));
ProcessExecutor psExec = new ProcessExecutor().readOutput(true);
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()));
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)));
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");
}
}
try {
log.info("Executing {}", cfg.getCommand());
ProcessResult psResult = psExec.execute();
result.setExitStatus(psResult.getExitValue());
String output = psResult.outputUTF8();
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);
}
}
} else if (cfg.getExit().getFailure().contains(result.getExitStatus())) {
log.debug("{} stdout:{}{}", cfg.getCommand(), "\n", output);
result.setSuccess(false);
} else {
log.error("{} stdout:{}{}", cfg.getCommand(), "\n", output);
throw new InternalServerError("Exec auth command returned with unexpected exit status");
}
return result;
} catch (IOException | InterruptedException | TimeoutException e) {
throw new InternalServerError(e);
}
}
}

View File

@@ -0,0 +1,46 @@
/*
* 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.exec;
import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchResult;
import io.kamax.mxisd.directory.IDirectoryProvider;
import io.kamax.mxisd.exception.NotImplementedException;
import org.springframework.stereotype.Component;
@Component
public class ExecDirectoryStore extends ExecStore implements IDirectoryProvider {
@Override
public boolean isEnabled() {
return false;
}
@Override
public UserDirectorySearchResult searchByDisplayName(String query) {
throw new NotImplementedException(this.getClass().getName());
}
@Override
public UserDirectorySearchResult searchBy3pid(String query) {
throw new NotImplementedException(this.getClass().getName());
}
}

View File

@@ -0,0 +1,61 @@
/*
* 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.exec;
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.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
@Component
public class ExecIdentityStore extends ExecStore implements IThreePidProvider {
@Override
public boolean isEnabled() {
return false;
}
@Override
public boolean isLocal() {
return true;
}
@Override
public int getPriority() {
return 0;
}
@Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
throw new NotImplementedException(this.getClass().getName());
}
@Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
throw new NotImplementedException(this.getClass().getName());
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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.exec;
import io.kamax.matrix._MatrixID;
import io.kamax.matrix._ThreePid;
import io.kamax.mxisd.exception.NotImplementedException;
import io.kamax.mxisd.profile.ProfileProvider;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Optional;
@Component
public class ExecProfileStore extends ExecStore implements ProfileProvider {
@Override
public boolean isEnabled() {
return false;
}
@Override
public Optional<String> getDisplayName(_MatrixID userId) {
throw new NotImplementedException(this.getClass().getName());
}
@Override
public List<_ThreePid> getThreepids(_MatrixID userId) {
throw new NotImplementedException(this.getClass().getName());
}
@Override
public List<String> getRoles(_MatrixID userId) {
throw new NotImplementedException(this.getClass().getName());
}
}

View File

@@ -0,0 +1,27 @@
/*
* 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.exec;
public abstract class ExecStore {
// no-op
}

View File

@@ -0,0 +1,343 @@
/*
* 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.config;
import org.apache.commons.lang3.StringUtils;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.util.*;
@Configuration
@ConfigurationProperties("exec")
public class ExecConfig {
public class Exit {
private List<Integer> success = Collections.singletonList(0);
private List<Integer> failure = Collections.singletonList(1);
public List<Integer> getSuccess() {
return success;
}
public void setSuccess(List<Integer> success) {
this.success = success;
}
public List<Integer> getFailure() {
return failure;
}
public void setFailure(List<Integer> failure) {
this.failure = failure;
}
}
public class TokenOverride {
private String localpart;
private String domain;
private String mxid;
private String password;
public String getLocalpart() {
return StringUtils.defaultIfEmpty(localpart, getToken().getLocalpart());
}
public void setLocalpart(String localpart) {
this.localpart = localpart;
}
public String getDomain() {
return StringUtils.defaultIfEmpty(domain, getToken().getDomain());
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getMxid() {
return StringUtils.defaultIfEmpty(mxid, getToken().getMxid());
}
public void setMxid(String mxid) {
this.mxid = mxid;
}
public String getPassword() {
return StringUtils.defaultIfEmpty(password, getToken().getPassword());
}
public void setPassword(String password) {
this.password = password;
}
}
public class Token {
private String localpart = "{localpart}";
private String domain = "{domain}";
private String mxid = "{mxid}";
private String password = "{password}";
public String getLocalpart() {
return localpart;
}
public void setLocalpart(String localpart) {
this.localpart = localpart;
}
public String getDomain() {
return domain;
}
public void setDomain(String domain) {
this.domain = domain;
}
public String getMxid() {
return mxid;
}
public void setMxid(String mxid) {
this.mxid = mxid;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
public class Process {
private TokenOverride token = new TokenOverride();
private String command;
private List<String> args = new ArrayList<>();
private Map<String, String> env = new HashMap<>();
private String input;
private Exit exit = new Exit();
private String output;
public TokenOverride getToken() {
return token;
}
public void setToken(TokenOverride token) {
this.token = token;
}
public String getCommand() {
return command;
}
public void setCommand(String command) {
this.command = command;
}
public List<String> getArgs() {
return args;
}
public void setArgs(List<String> args) {
this.args = args;
}
public Map<String, String> getEnv() {
return env;
}
public void setEnv(Map<String, String> env) {
this.env = env;
}
public void addEnv(String key, String value) {
this.env.put(key, value);
}
public String getInput() {
return input;
}
public void setInput(String input) {
this.input = input;
}
public Exit getExit() {
return exit;
}
public void setExit(Exit exit) {
this.exit = exit;
}
public String getOutput() {
return output;
}
public void setOutput(String output) {
this.output = output;
}
}
public class Auth extends Process {
private Boolean enabled;
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
public class Directory extends Process {
private Boolean enabled;
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
public class Identity extends Process {
private Boolean enabled;
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
public class Profile extends Process {
private Boolean enabled;
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
}
private boolean enabled;
private Token token = new Token();
private Auth auth = new Auth();
private Directory directory = new Directory();
private Identity identity = new Identity();
private Profile profile = new Profile();
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public Token getToken() {
return token;
}
public void setToken(Token token) {
this.token = token;
}
public Auth getAuth() {
return auth;
}
public void setAuth(Auth auth) {
this.auth = auth;
}
public Directory getDirectory() {
return directory;
}
public void setDirectory(Directory directory) {
this.directory = directory;
}
public Identity getIdentity() {
return identity;
}
public void setIdentity(Identity identity) {
this.identity = identity;
}
public Profile getProfile() {
return profile;
}
public void setProfile(Profile profile) {
this.profile = profile;
}
@PostConstruct
public void build() {
if (Objects.isNull(getAuth().isEnabled())) {
getAuth().setEnabled(isEnabled());
}
if (Objects.isNull(getDirectory().isEnabled())) {
getDirectory().setEnabled(isEnabled());
}
if (Objects.isNull(getIdentity().isEnabled())) {
getIdentity().setEnabled(isEnabled());
}
if (Objects.isNull(getProfile().isEnabled())) {
getProfile().setEnabled(isEnabled());
}
}
}