From 99d793b5eddf74be1a287c67fbd87faa708ac628 Mon Sep 17 00:00:00 2001 From: Max Dor Date: Sat, 20 Oct 2018 08:08:14 +0200 Subject: [PATCH] Add initial experimental support for #58 - Skeleton for the whole identity store - Support Authentication --- build.gradle | 3 + docs/stores/README.md | 13 +- docs/stores/exec.md | 60 +++ src/main/java/io/kamax/mxisd/UserID.java | 8 +- .../java/io/kamax/mxisd/auth/AuthManager.java | 7 +- .../auth/provider/BackendAuthResult.java | 16 +- .../mxisd/backend/exec/ExecAuthResult.java | 37 ++ .../mxisd/backend/exec/ExecAuthStore.java | 141 +++++++ .../backend/exec/ExecDirectoryStore.java | 46 +++ .../mxisd/backend/exec/ExecIdentityStore.java | 61 ++++ .../mxisd/backend/exec/ExecProfileStore.java | 55 +++ .../kamax/mxisd/backend/exec/ExecStore.java | 27 ++ .../io/kamax/mxisd/config/ExecConfig.java | 343 ++++++++++++++++++ .../backend/exec/ExecAuthStoreArgsTest.java | 67 ++++ .../backend/exec/ExecAuthStoreEnvTest.java | 72 ++++ .../mxisd/backend/exec/ExecAuthStoreTest.java | 197 ++++++++++ src/test/resources/store/exec/authArgsTest.sh | 41 +++ src/test/resources/store/exec/authEnvTest.sh | 38 ++ 18 files changed, 1219 insertions(+), 13 deletions(-) create mode 100644 docs/stores/exec.md create mode 100644 src/main/java/io/kamax/mxisd/backend/exec/ExecAuthResult.java create mode 100644 src/main/java/io/kamax/mxisd/backend/exec/ExecAuthStore.java create mode 100644 src/main/java/io/kamax/mxisd/backend/exec/ExecDirectoryStore.java create mode 100644 src/main/java/io/kamax/mxisd/backend/exec/ExecIdentityStore.java create mode 100644 src/main/java/io/kamax/mxisd/backend/exec/ExecProfileStore.java create mode 100644 src/main/java/io/kamax/mxisd/backend/exec/ExecStore.java create mode 100644 src/main/java/io/kamax/mxisd/config/ExecConfig.java create mode 100644 src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreArgsTest.java create mode 100644 src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreEnvTest.java create mode 100644 src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreTest.java create mode 100755 src/test/resources/store/exec/authArgsTest.sh create mode 100755 src/test/resources/store/exec/authEnvTest.sh diff --git a/build.gradle b/build.gradle index baa14f51..e61b758d 100644 --- a/build.gradle +++ b/build.gradle @@ -126,6 +126,9 @@ dependencies { // SendGrid SDK to send emails from GCE compile 'com.sendgrid:sendgrid-java:2.2.2' + // ZT-Exec for exec identity store + compile 'org.zeroturnaround:zt-exec:1.10' + testCompile 'junit:junit:4.12' testCompile 'com.github.tomakehurst:wiremock:2.8.0' } diff --git a/docs/stores/README.md b/docs/stores/README.md index 3283b8e4..897d75b1 100644 --- a/docs/stores/README.md +++ b/docs/stores/README.md @@ -1,7 +1,8 @@ # Identity Stores -- [Synapse](synapse.md) -- [LDAP-based](ldap.md) -- [SQL Databases](sql.md) -- [Website / Web service / Web app](rest.md) -- [Google Firebase](firebase.md) -- [Wordpress](wordpress.md) +- [Synapse](synapse.md) - Turn your SynapseDB into a self-contained Identity store +- [LDAP-based](ldap.md) - Any LDAP-based product like Active Directory, Samba, NetIQ, OpenLDAP +- [SQL Databases](sql.md) - Most common databases like MariaDB, MySQL, PostgreSQL, SQLite +- [Website / Web service / Web app](rest.md) - Arbitrary REST endpoints +- [Executables](exec.md) - Run arbitrary executables with configurable stdin, arguments, environment and stdout +- [Wordpress](wordpress.md) - Connect your Wordpress-powered website DB +- [Google Firebase](firebase.md) - Use your Firebase users (with experimental SSO support!) diff --git a/docs/stores/exec.md b/docs/stores/exec.md new file mode 100644 index 00000000..7533d1a7 --- /dev/null +++ b/docs/stores/exec.md @@ -0,0 +1,60 @@ +# Exec Identity Store +This Identity Store lets you run arbitrary commands to handle the various requests in each support feature. + +This is the most versatile Identity store of mxisd, allowing you to connect any kind of logic in any language/scripting. + +## Features +| Name | Supported? | +|----------------|---------------| +| Authentication | Yes | +| Directory | *In Progress* | +| Identity | *In Progress* | +| Profile | *In Progress* | + +## Overview +Each request can be mapping to a fully customizable command configuration. +The various parameters can be provided via any combination of: +- Standard Input +- Command line arguments +- Environment variables + +Each of those supports a set of customizable token which will be replaced prior to running the command, allowing to +provide the input values in any number of ways. + +Success and data will be provided via [Exit status](https://en.wikipedia.org/wiki/Exit_status) and Standard Output, both +supporting a set of options. + +## Configuration +```yaml +exec.enabled: +``` +Enable/disable the Identity store at a global/default level. Each feature can still be enabled/disabled specifically. + +*TBC* + +## Use-case examples +```yaml +exec.enabled: true + +exec.auth.command: '/path/to/auth/executable' +exec.auth.args: ['-u', '{localpart}'] +exec.auth.env: + PASSWORD: '{password}' + MATRIX_DOMAIN: '{domain}' + MATRIX_USER_ID: '{mxid}' +``` +This will run `/path/to/auth/executable` with: +- The extracted Matrix User ID `localpart` provided as the second command line argument, the first one being `-u` +- The password, the extract Matrix `domain` and the full User ID as arbitrary environment variables, respectively `PASSWORD`, `MATRIX_DOMAIN` and `MATRIX_USER_ID` + +```yaml +## Few more available config items +# +# exec.token.domain: '{matrixDomain}' # This sets the default replacement token for the Matrix Domain of the User ID, across all features. +# exec.auth.token.domain: '{matrixDomainForAuth}' # We can also set another token specific to a feature. +# exec.auth.input: 'json' # This is not supported yet. +# exec.auth.exit.success: [0] # Exit status that will consider the request successful. This is already the default. +# exec.auth.exit.failure: [1,2,3] # Exist status that will consider the request failed. Anything else than success or failure statuses will throw an exception. +# exec.auth.output: 'json' # Required if stdout should be read on success. This uses the same output as the REST Identity store for Auth. +``` +*TBC* diff --git a/src/main/java/io/kamax/mxisd/UserID.java b/src/main/java/io/kamax/mxisd/UserID.java index 5cf683ae..78593f1d 100644 --- a/src/main/java/io/kamax/mxisd/UserID.java +++ b/src/main/java/io/kamax/mxisd/UserID.java @@ -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 @@ protected 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; diff --git a/src/main/java/io/kamax/mxisd/auth/AuthManager.java b/src/main/java/io/kamax/mxisd/auth/AuthManager.java index 7a62759a..78d09349 100644 --- a/src/main/java/io/kamax/mxisd/auth/AuthManager.java +++ b/src/main/java/io/kamax/mxisd/auth/AuthManager.java @@ -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 UserAuthResult authenticate(String id, String password) { 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(); diff --git a/src/main/java/io/kamax/mxisd/auth/provider/BackendAuthResult.java b/src/main/java/io/kamax/mxisd/auth/provider/BackendAuthResult.java index a89566d5..df7467f9 100644 --- a/src/main/java/io/kamax/mxisd/auth/provider/BackendAuthResult.java +++ b/src/main/java/io/kamax/mxisd/auth/provider/BackendAuthResult.java @@ -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 String getDisplayName() { return displayName; } + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + public Set getThreePids() { return threePids; } @@ -73,6 +77,10 @@ public void succeed(String id, String type, String displayName) { 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 UserID getId() { return id; } + public void setId(UserID id) { + this.id = id; + } + public BackendAuthProfile getProfile() { return profile; } diff --git a/src/main/java/io/kamax/mxisd/backend/exec/ExecAuthResult.java b/src/main/java/io/kamax/mxisd/backend/exec/ExecAuthResult.java new file mode 100644 index 00000000..c26503e9 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/backend/exec/ExecAuthResult.java @@ -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 . + */ + +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; + } + +} diff --git a/src/main/java/io/kamax/mxisd/backend/exec/ExecAuthStore.java b/src/main/java/io/kamax/mxisd/backend/exec/ExecAuthStore.java new file mode 100644 index 00000000..fb461bfc --- /dev/null +++ b/src/main/java/io/kamax/mxisd/backend/exec/ExecAuthStore.java @@ -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 . + */ + +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 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); + } + } + +} diff --git a/src/main/java/io/kamax/mxisd/backend/exec/ExecDirectoryStore.java b/src/main/java/io/kamax/mxisd/backend/exec/ExecDirectoryStore.java new file mode 100644 index 00000000..397d7dc5 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/backend/exec/ExecDirectoryStore.java @@ -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 . + */ + +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()); + } + +} diff --git a/src/main/java/io/kamax/mxisd/backend/exec/ExecIdentityStore.java b/src/main/java/io/kamax/mxisd/backend/exec/ExecIdentityStore.java new file mode 100644 index 00000000..d9e8436a --- /dev/null +++ b/src/main/java/io/kamax/mxisd/backend/exec/ExecIdentityStore.java @@ -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 . + */ + +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 find(SingleLookupRequest request) { + throw new NotImplementedException(this.getClass().getName()); + } + + @Override + public List populate(List mappings) { + throw new NotImplementedException(this.getClass().getName()); + } + +} diff --git a/src/main/java/io/kamax/mxisd/backend/exec/ExecProfileStore.java b/src/main/java/io/kamax/mxisd/backend/exec/ExecProfileStore.java new file mode 100644 index 00000000..aa89a444 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/backend/exec/ExecProfileStore.java @@ -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 . + */ + +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 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 getRoles(_MatrixID userId) { + throw new NotImplementedException(this.getClass().getName()); + } + +} diff --git a/src/main/java/io/kamax/mxisd/backend/exec/ExecStore.java b/src/main/java/io/kamax/mxisd/backend/exec/ExecStore.java new file mode 100644 index 00000000..69a06b16 --- /dev/null +++ b/src/main/java/io/kamax/mxisd/backend/exec/ExecStore.java @@ -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 . + */ + +package io.kamax.mxisd.backend.exec; + +public abstract class ExecStore { + + // no-op + +} diff --git a/src/main/java/io/kamax/mxisd/config/ExecConfig.java b/src/main/java/io/kamax/mxisd/config/ExecConfig.java new file mode 100644 index 00000000..5010166f --- /dev/null +++ b/src/main/java/io/kamax/mxisd/config/ExecConfig.java @@ -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 . + */ + +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 success = Collections.singletonList(0); + private List failure = Collections.singletonList(1); + + public List getSuccess() { + return success; + } + + public void setSuccess(List success) { + this.success = success; + } + + public List getFailure() { + return failure; + } + + public void setFailure(List 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 args = new ArrayList<>(); + private Map 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 getArgs() { + return args; + } + + public void setArgs(List args) { + this.args = args; + } + + public Map getEnv() { + return env; + } + + public void setEnv(Map 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()); + } + } + +} diff --git a/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreArgsTest.java b/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreArgsTest.java new file mode 100644 index 00000000..a6a2bfac --- /dev/null +++ b/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreArgsTest.java @@ -0,0 +1,67 @@ +/* + * 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 . + */ + +package io.kamax.mxisd.backend.exec; + +import java.util.Arrays; + +public class ExecAuthStoreArgsTest extends ExecAuthStoreTest { + + @Override + protected void setValidCommand() { + cfg.getAuth().setCommand("src/test/resources/store/exec/authArgsTest.sh"); + } + + @Override + protected void setValidArgs() { + cfg.getAuth().setArgs(Arrays.asList(LocalpartToken, DomainToken, MxidToken, PassToken)); + } + + @Override + protected void setEmptyLocalpartConfig() { + cfg.getAuth().setArgs(Arrays.asList("", DomainToken, MxidToken, PassToken)); + } + + @Override + public void setWrongLocalpartConfig() { + cfg.getAuth().setArgs(Arrays.asList(LocalpartInvalid, DomainToken, MxidToken, PassToken)); + } + + @Override + protected void setEmptyDomainConfig() { + cfg.getAuth().setArgs(Arrays.asList(LocalpartToken, "", MxidToken, PassToken)); + } + + @Override + public void setWrongDomainConfig() { + cfg.getAuth().setArgs(Arrays.asList(LocalpartToken, DomainInvalid, MxidToken, PassToken)); + } + + @Override + protected void setEmptyMxidConfig() { + cfg.getAuth().setArgs(Arrays.asList(LocalpartToken, DomainToken, "", PassToken)); + } + + @Override + public void setWrongMxidConfig() { + cfg.getAuth().setArgs(Arrays.asList(LocalpartToken, DomainToken, MxidInvalid, PassToken)); + } + +} diff --git a/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreEnvTest.java b/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreEnvTest.java new file mode 100644 index 00000000..e3522ad7 --- /dev/null +++ b/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreEnvTest.java @@ -0,0 +1,72 @@ +/* + * 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 . + */ + +package io.kamax.mxisd.backend.exec; + +public class ExecAuthStoreEnvTest extends ExecAuthStoreTest { + + private final String LocalpartEnv = "LOCALPART"; + private final String DomainEnv = "DOMAIN"; + private final String MxidEnv = "MXID"; + + @Override + protected void setValidCommand() { + cfg.getAuth().setCommand("src/test/resources/store/exec/authEnvTest.sh"); + } + + @Override + protected void setValidEnv() { + cfg.getAuth().addEnv(LocalpartEnv, LocalpartToken); + cfg.getAuth().addEnv(DomainEnv, DomainToken); + cfg.getAuth().addEnv(MxidEnv, MxidToken); + cfg.getAuth().addEnv("PASS", PassToken); + } + + @Override + protected void setEmptyLocalpartConfig() { + cfg.getAuth().addEnv(LocalpartEnv, ""); + } + + @Override + public void setWrongLocalpartConfig() { + cfg.getAuth().addEnv(LocalpartEnv, LocalpartInvalid); + } + + @Override + protected void setEmptyDomainConfig() { + cfg.getAuth().addEnv(DomainEnv, ""); + } + + @Override + public void setWrongDomainConfig() { + cfg.getAuth().addEnv(DomainEnv, DomainInvalid); + } + + @Override + protected void setEmptyMxidConfig() { + cfg.getAuth().addEnv(MxidEnv, ""); + } + + @Override + public void setWrongMxidConfig() { + cfg.getAuth().addEnv(MxidEnv, MxidInvalid); + } + +} diff --git a/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreTest.java b/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreTest.java new file mode 100644 index 00000000..f1b31c30 --- /dev/null +++ b/src/test/java/io/kamax/mxisd/backend/exec/ExecAuthStoreTest.java @@ -0,0 +1,197 @@ +/* + * 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 . + */ + +package io.kamax.mxisd.backend.exec; + +import io.kamax.matrix.MatrixID; +import io.kamax.matrix._MatrixID; +import io.kamax.mxisd.UserIdType; +import io.kamax.mxisd.config.ExecConfig; +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.Test; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.Assert.assertEquals; + +public abstract class ExecAuthStoreTest { + + protected final ExecConfig cfg; + protected final ExecAuthStore p; + protected final String requiredPass = Long.toString(System.currentTimeMillis()); + protected final String localpart = "user"; + protected final String domain = "domain.tld"; + protected final _MatrixID uId = MatrixID.from(localpart, domain).valid(); + + protected final String LocalpartToken = "{localpart}"; + protected final String DomainToken = "{domain}"; + protected final String MxidToken = "{mxid}"; + protected final String PassToken = "{password}"; + + protected final String LocalpartInvalid = "@:"; + protected final String DomainInvalid = "[.]:"; + protected final String MxidInvalid = LocalpartInvalid + DomainInvalid; + protected final String PassInvalid = RandomStringUtils.randomAscii(20); + + protected abstract void setValidCommand(); + + protected void setValidEnv() { + // no-op + } + + protected void setValidArgs() { + // no-op + } + + protected void setValidExit() { + cfg.getAuth().getExit().setSuccess(Collections.singletonList(0)); + cfg.getAuth().getExit().setFailure(Arrays.asList(1, 10, 11, 12, 20, 21, 22)); + } + + protected void setValidConfig() { + setValidCommand(); + setValidEnv(); + setValidArgs(); + setValidExit(); + } + + public ExecAuthStoreTest() { + cfg = new ExecConfig(); + cfg.getAuth().addEnv("WITH_LOCALPART", "1"); + cfg.getAuth().addEnv("REQ_LOCALPART", uId.getLocalPart()); + cfg.getAuth().addEnv("WITH_DOMAIN", "1"); + cfg.getAuth().addEnv("REQ_DOMAIN", uId.getDomain()); + cfg.getAuth().addEnv("WITH_MXID", "1"); + cfg.getAuth().addEnv("REQ_MXID", uId.getId()); + cfg.getAuth().addEnv("REQ_PASS", requiredPass); + + setValidConfig(); + + p = new ExecAuthStore(cfg); + } + + @Test + public void validPassword() { + ExecAuthResult res = p.authenticate(uId, requiredPass); + assertEquals(true, res.isSuccess()); + assertEquals(0, res.getExitStatus()); + assertEquals(UserIdType.Localpart.getId(), res.getId().getType()); + assertEquals(uId.getLocalPart(), res.getId().getValue()); + } + + @Test + public void invalidPassword() { + ExecAuthResult res = p.authenticate(uId, PassInvalid); + assertEquals(false, res.isSuccess()); + assertEquals(1, res.getExitStatus()); + } + + @Test + public void emptyPassword() { + ExecAuthResult res = p.authenticate(uId, ""); + assertEquals(false, res.isSuccess()); + assertEquals(1, res.getExitStatus()); + } + + @Test(expected = NullPointerException.class) + public void nullPassword() { + p.authenticate(uId, null); + } + + protected abstract void setEmptyLocalpartConfig(); + + @Test + public void doEmptyLocalpartConfig() { + setEmptyLocalpartConfig(); + + ExecAuthResult res = p.authenticate(uId, requiredPass); + assertEquals(false, res.isSuccess()); + assertEquals(10, res.getExitStatus()); + + setValidConfig(); + } + + public abstract void setWrongLocalpartConfig(); + + @Test + public void wrongLocalpartConfig() { + setWrongLocalpartConfig(); + + ExecAuthResult res = p.authenticate(uId, requiredPass); + assertEquals(false, res.isSuccess()); + assertEquals(20, res.getExitStatus()); + + setValidConfig(); + } + + protected abstract void setEmptyDomainConfig(); + + @Test + public void emptyDomainConfig() { + setEmptyDomainConfig(); + + ExecAuthResult res = p.authenticate(uId, requiredPass); + assertEquals(false, res.isSuccess()); + assertEquals(11, res.getExitStatus()); + + setValidConfig(); + } + + public abstract void setWrongDomainConfig(); + + @Test + public void wrongDomainConfig() { + setWrongDomainConfig(); + + ExecAuthResult res = p.authenticate(uId, requiredPass); + assertEquals(false, res.isSuccess()); + assertEquals(21, res.getExitStatus()); + + setValidConfig(); + } + + protected abstract void setEmptyMxidConfig(); + + @Test + public void emptyMxidConfig() { + setEmptyMxidConfig(); + + ExecAuthResult res = p.authenticate(uId, requiredPass); + assertEquals(false, res.isSuccess()); + assertEquals(12, res.getExitStatus()); + + setValidConfig(); + } + + public abstract void setWrongMxidConfig(); + + @Test + public void wrongMxidConfig() { + setWrongMxidConfig(); + + ExecAuthResult res = p.authenticate(uId, requiredPass); + assertEquals(false, res.isSuccess()); + assertEquals(22, res.getExitStatus()); + + setValidConfig(); + } + +} diff --git a/src/test/resources/store/exec/authArgsTest.sh b/src/test/resources/store/exec/authArgsTest.sh new file mode 100755 index 00000000..7bd14722 --- /dev/null +++ b/src/test/resources/store/exec/authArgsTest.sh @@ -0,0 +1,41 @@ +#!/bin/bash +# +# 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 . + +if [ -n "$WITH_LOCALPART" ]; then + [ -n "$1" ] || exit 10 + [ "$1" = "$REQ_LOCALPART" ] || exit 20 + shift +fi + +if [ -n "$WITH_DOMAIN" ]; then + [ -n "$1" ] || exit 11 + [ "$1" = "$REQ_DOMAIN" ] || exit 21 + shift +fi + +if [ -n "$WITH_MXID" ]; then + [ -n "$1" ] || exit 12 + [ "$1" = "$REQ_MXID" ] || exit 22 + shift +fi + +[ "$1" = "$REQ_PASS" ] || exit 1 + +exit 0 diff --git a/src/test/resources/store/exec/authEnvTest.sh b/src/test/resources/store/exec/authEnvTest.sh new file mode 100755 index 00000000..b959f48f --- /dev/null +++ b/src/test/resources/store/exec/authEnvTest.sh @@ -0,0 +1,38 @@ +#!/bin/bash +# +# 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 . + +if [ -n "$WITH_LOCALPART" ]; then + [ -n "$LOCALPART" ] || exit 10 + [ "$LOCALPART" = "$REQ_LOCALPART" ] || exit 20 +fi + +if [ -n "$WITH_DOMAIN" ]; then + [ -n "$DOMAIN" ] || exit 11 + [ "$DOMAIN" = "$REQ_DOMAIN" ] || exit 21 +fi + +if [ -n "$WITH_MXID" ]; then + [ -n "$MXID" ] || exit 12 + [ "$MXID" = "$REQ_MXID" ] || exit 22 +fi + +[ "$PASS" = "$REQ_PASS" ] || exit 1 + +exit 0