Skip to content

Commit

Permalink
Add initial experimental support for #58
Browse files Browse the repository at this point in the history
- Skeleton for the whole identity store
- Support Authentication
  • Loading branch information
Max Dor committed Oct 20, 2018
1 parent cb02f62 commit 99d793b
Show file tree
Hide file tree
Showing 18 changed files with 1,219 additions and 13 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
Expand Down
13 changes: 7 additions & 6 deletions docs/stores/README.md
Original file line number Diff line number Diff line change
@@ -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!)
60 changes: 60 additions & 0 deletions docs/stores/exec.md
Original file line number Diff line number Diff line change
@@ -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: <boolean>
```
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*
8 changes: 6 additions & 2 deletions src/main/java/io/kamax/mxisd/UserID.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/main/java/io/kamax/mxisd/auth/AuthManager.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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();
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/io/kamax/mxisd/auth/provider/BackendAuthResult.java
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -38,6 +38,10 @@ public String getDisplayName() {
return displayName;
}

public void setDisplayName(String displayName) {
this.displayName = displayName;
}

public Set<ThreePid> getThreePids() {
return threePids;
}
Expand Down Expand Up @@ -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;
}
Expand All @@ -81,6 +89,10 @@ public UserID getId() {
return id;
}

public void setId(UserID id) {
this.id = id;
}

public BackendAuthProfile getProfile() {
return profile;
}
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/io/kamax/mxisd/backend/exec/ExecAuthResult.java
Original file line number Diff line number Diff line change
@@ -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;
}

}
141 changes: 141 additions & 0 deletions src/main/java/io/kamax/mxisd/backend/exec/ExecAuthStore.java
Original file line number Diff line number Diff line change
@@ -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);
}
}

}
Loading

0 comments on commit 99d793b

Please sign in to comment.