Skip to content

Commit 99d793b

Browse files
author
Max Dor
committed
Add initial experimental support for #58
- Skeleton for the whole identity store - Support Authentication
1 parent cb02f62 commit 99d793b

File tree

18 files changed

+1219
-13
lines changed

18 files changed

+1219
-13
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ dependencies {
126126
// SendGrid SDK to send emails from GCE
127127
compile 'com.sendgrid:sendgrid-java:2.2.2'
128128

129+
// ZT-Exec for exec identity store
130+
compile 'org.zeroturnaround:zt-exec:1.10'
131+
129132
testCompile 'junit:junit:4.12'
130133
testCompile 'com.github.tomakehurst:wiremock:2.8.0'
131134
}

docs/stores/README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
# Identity Stores
2-
- [Synapse](synapse.md)
3-
- [LDAP-based](ldap.md)
4-
- [SQL Databases](sql.md)
5-
- [Website / Web service / Web app](rest.md)
6-
- [Google Firebase](firebase.md)
7-
- [Wordpress](wordpress.md)
2+
- [Synapse](synapse.md) - Turn your SynapseDB into a self-contained Identity store
3+
- [LDAP-based](ldap.md) - Any LDAP-based product like Active Directory, Samba, NetIQ, OpenLDAP
4+
- [SQL Databases](sql.md) - Most common databases like MariaDB, MySQL, PostgreSQL, SQLite
5+
- [Website / Web service / Web app](rest.md) - Arbitrary REST endpoints
6+
- [Executables](exec.md) - Run arbitrary executables with configurable stdin, arguments, environment and stdout
7+
- [Wordpress](wordpress.md) - Connect your Wordpress-powered website DB
8+
- [Google Firebase](firebase.md) - Use your Firebase users (with experimental SSO support!)

docs/stores/exec.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Exec Identity Store
2+
This Identity Store lets you run arbitrary commands to handle the various requests in each support feature.
3+
4+
This is the most versatile Identity store of mxisd, allowing you to connect any kind of logic in any language/scripting.
5+
6+
## Features
7+
| Name | Supported? |
8+
|----------------|---------------|
9+
| Authentication | Yes |
10+
| Directory | *In Progress* |
11+
| Identity | *In Progress* |
12+
| Profile | *In Progress* |
13+
14+
## Overview
15+
Each request can be mapping to a fully customizable command configuration.
16+
The various parameters can be provided via any combination of:
17+
- Standard Input
18+
- Command line arguments
19+
- Environment variables
20+
21+
Each of those supports a set of customizable token which will be replaced prior to running the command, allowing to
22+
provide the input values in any number of ways.
23+
24+
Success and data will be provided via [Exit status](https://en.wikipedia.org/wiki/Exit_status) and Standard Output, both
25+
supporting a set of options.
26+
27+
## Configuration
28+
```yaml
29+
exec.enabled: <boolean>
30+
```
31+
Enable/disable the Identity store at a global/default level. Each feature can still be enabled/disabled specifically.
32+
33+
*TBC*
34+
35+
## Use-case examples
36+
```yaml
37+
exec.enabled: true
38+
39+
exec.auth.command: '/path/to/auth/executable'
40+
exec.auth.args: ['-u', '{localpart}']
41+
exec.auth.env:
42+
PASSWORD: '{password}'
43+
MATRIX_DOMAIN: '{domain}'
44+
MATRIX_USER_ID: '{mxid}'
45+
```
46+
This will run `/path/to/auth/executable` with:
47+
- The extracted Matrix User ID `localpart` provided as the second command line argument, the first one being `-u`
48+
- The password, the extract Matrix `domain` and the full User ID as arbitrary environment variables, respectively `PASSWORD`, `MATRIX_DOMAIN` and `MATRIX_USER_ID`
49+
50+
```yaml
51+
## Few more available config items
52+
#
53+
# exec.token.domain: '{matrixDomain}' # This sets the default replacement token for the Matrix Domain of the User ID, across all features.
54+
# exec.auth.token.domain: '{matrixDomainForAuth}' # We can also set another token specific to a feature.
55+
# exec.auth.input: 'json' # This is not supported yet.
56+
# exec.auth.exit.success: [0] # Exit status that will consider the request successful. This is already the default.
57+
# 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.
58+
# exec.auth.output: 'json' # Required if stdout should be read on success. This uses the same output as the REST Identity store for Auth.
59+
```
60+
*TBC*

src/main/java/io/kamax/mxisd/UserID.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*
22
* mxisd - Matrix Identity Server Daemon
3-
* Copyright (C) 2017 Maxime Dor
3+
* Copyright (C) 2017 Kamax Sarl
44
*
5-
* https://max.kamax.io/
5+
* https://www.kamax.io/
66
*
77
* This program is free software: you can redistribute it and/or modify
88
* it under the terms of the GNU Affero General Public License as
@@ -30,6 +30,10 @@ protected UserID() {
3030
// stub for (de)serialization
3131
}
3232

33+
public UserID(UserIdType type, String value) {
34+
this(type.getId(), value);
35+
}
36+
3337
public UserID(String type, String value) {
3438
this.type = type;
3539
this.value = value;

src/main/java/io/kamax/mxisd/auth/AuthManager.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*
22
* mxisd - Matrix Identity Server Daemon
3-
* Copyright (C) 2017 Maxime Dor
3+
* Copyright (C) 2017 Kamax Sarl
44
*
5-
* https://max.kamax.io/
5+
* https://www.kamax.io/
66
*
77
* This program is free software: you can redistribute it and/or modify
88
* it under the terms of the GNU Affero General Public License as
@@ -59,9 +59,10 @@ public UserAuthResult authenticate(String id, String password) {
5959
continue;
6060
}
6161

62+
log.info("Attempting authentication with store {}", provider.getClass().getSimpleName());
63+
6264
BackendAuthResult result = provider.authenticate(mxid, password);
6365
if (result.isSuccess()) {
64-
6566
String mxId;
6667
if (UserIdType.Localpart.is(result.getId().getType())) {
6768
mxId = MatrixID.from(result.getId().getValue(), mxCfg.getDomain()).acceptable().getId();

src/main/java/io/kamax/mxisd/auth/provider/BackendAuthResult.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/*
22
* mxisd - Matrix Identity Server Daemon
3-
* Copyright (C) 2017 Maxime Dor
3+
* Copyright (C) 2017 Kamax Sarl
44
*
5-
* https://max.kamax.io/
5+
* https://www.kamax.io/
66
*
77
* This program is free software: you can redistribute it and/or modify
88
* it under the terms of the GNU Affero General Public License as
@@ -38,6 +38,10 @@ public String getDisplayName() {
3838
return displayName;
3939
}
4040

41+
public void setDisplayName(String displayName) {
42+
this.displayName = displayName;
43+
}
44+
4145
public Set<ThreePid> getThreePids() {
4246
return threePids;
4347
}
@@ -73,6 +77,10 @@ public void succeed(String id, String type, String displayName) {
7377
private UserID id;
7478
private BackendAuthProfile profile = new BackendAuthProfile();
7579

80+
public void setSuccess(boolean success) {
81+
this.success = success;
82+
}
83+
7684
public Boolean isSuccess() {
7785
return success;
7886
}
@@ -81,6 +89,10 @@ public UserID getId() {
8189
return id;
8290
}
8391

92+
public void setId(UserID id) {
93+
this.id = id;
94+
}
95+
8496
public BackendAuthProfile getProfile() {
8597
return profile;
8698
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* mxisd - Matrix Identity Server Daemon
3+
* Copyright (C) 2018 Kamax Sarl
4+
*
5+
* https://www.kamax.io/
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as
9+
* published by the Free Software Foundation, either version 3 of the
10+
* License, or (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package io.kamax.mxisd.backend.exec;
22+
23+
import io.kamax.mxisd.auth.provider.BackendAuthResult;
24+
25+
public class ExecAuthResult extends BackendAuthResult {
26+
27+
private int exitStatus;
28+
29+
public int getExitStatus() {
30+
return exitStatus;
31+
}
32+
33+
public void setExitStatus(int exitStatus) {
34+
this.exitStatus = exitStatus;
35+
}
36+
37+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* mxisd - Matrix Identity Server Daemon
3+
* Copyright (C) 2018 Kamax Sarl
4+
*
5+
* https://www.kamax.io/
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU Affero General Public License as
9+
* published by the Free Software Foundation, either version 3 of the
10+
* License, or (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU Affero General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Affero General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package io.kamax.mxisd.backend.exec;
22+
23+
import com.google.gson.JsonObject;
24+
import com.google.gson.JsonPrimitive;
25+
import io.kamax.matrix._MatrixID;
26+
import io.kamax.matrix.json.GsonUtil;
27+
import io.kamax.mxisd.UserID;
28+
import io.kamax.mxisd.UserIdType;
29+
import io.kamax.mxisd.auth.provider.AuthenticatorProvider;
30+
import io.kamax.mxisd.config.ExecConfig;
31+
import io.kamax.mxisd.exception.InternalServerError;
32+
import org.apache.commons.io.IOUtils;
33+
import org.apache.commons.lang3.StringUtils;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
36+
import org.springframework.beans.factory.annotation.Autowired;
37+
import org.springframework.stereotype.Component;
38+
import org.zeroturnaround.exec.ProcessExecutor;
39+
import org.zeroturnaround.exec.ProcessResult;
40+
41+
import java.io.IOException;
42+
import java.nio.charset.StandardCharsets;
43+
import java.util.*;
44+
import java.util.concurrent.TimeoutException;
45+
import java.util.stream.Collectors;
46+
47+
@Component
48+
public class ExecAuthStore extends ExecStore implements AuthenticatorProvider {
49+
50+
private final transient Logger log = LoggerFactory.getLogger(ExecAuthStore.class);
51+
52+
private ExecConfig.Auth cfg;
53+
54+
@Autowired
55+
public ExecAuthStore(ExecConfig cfg) {
56+
this.cfg = Objects.requireNonNull(cfg.getAuth());
57+
}
58+
59+
@Override
60+
public boolean isEnabled() {
61+
return cfg.isEnabled();
62+
}
63+
64+
@Override
65+
public ExecAuthResult authenticate(_MatrixID uId, String password) {
66+
Objects.requireNonNull(uId);
67+
Objects.requireNonNull(password);
68+
69+
log.info("Performing authentication for {}", uId.getId());
70+
71+
ExecAuthResult result = new ExecAuthResult();
72+
result.setId(new UserID(UserIdType.Localpart, uId.getLocalPart()));
73+
74+
ProcessExecutor psExec = new ProcessExecutor().readOutput(true);
75+
76+
List<String> args = new ArrayList<>();
77+
args.add(cfg.getCommand());
78+
args.addAll(cfg.getArgs().stream().map(arg -> arg
79+
.replace(cfg.getToken().getLocalpart(), uId.getLocalPart())
80+
.replace(cfg.getToken().getDomain(), uId.getDomain())
81+
.replace(cfg.getToken().getMxid(), uId.getId())
82+
.replace(cfg.getToken().getPassword(), password)
83+
).collect(Collectors.toList()));
84+
psExec.command(args);
85+
86+
psExec.environment(new HashMap<>(cfg.getEnv()).entrySet().stream().peek(e -> {
87+
e.setValue(e.getValue().replace(cfg.getToken().getLocalpart(), uId.getLocalPart()));
88+
e.setValue(e.getValue().replace(cfg.getToken().getDomain(), uId.getDomain()));
89+
e.setValue(e.getValue().replace(cfg.getToken().getMxid(), uId.getId()));
90+
e.setValue(e.getValue().replace(cfg.getToken().getPassword(), password));
91+
}).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
92+
93+
if (StringUtils.isNotBlank(cfg.getInput())) {
94+
if (StringUtils.equals("json", cfg.getInput())) {
95+
JsonObject input = new JsonObject();
96+
input.addProperty("localpart", uId.getLocalPart());
97+
input.addProperty("mxid", uId.getId());
98+
input.addProperty("password", password);
99+
psExec.redirectInput(IOUtils.toInputStream(GsonUtil.get().toJson(input), StandardCharsets.UTF_8));
100+
} else {
101+
throw new InternalServerError(cfg.getInput() + " is not a valid executable input format");
102+
}
103+
}
104+
105+
try {
106+
log.info("Executing {}", cfg.getCommand());
107+
ProcessResult psResult = psExec.execute();
108+
result.setExitStatus(psResult.getExitValue());
109+
String output = psResult.outputUTF8();
110+
111+
log.info("Exit status: {}", result.getExitStatus());
112+
if (cfg.getExit().getSuccess().contains(result.getExitStatus())) {
113+
result.setSuccess(true);
114+
if (result.isSuccess()) {
115+
if (StringUtils.equals("json", cfg.getOutput())) {
116+
JsonObject data = GsonUtil.parseObj(output);
117+
GsonUtil.findPrimitive(data, "success")
118+
.map(JsonPrimitive::getAsBoolean)
119+
.ifPresent(result::setSuccess);
120+
GsonUtil.findObj(data, "profile")
121+
.flatMap(p -> GsonUtil.findString(p, "display_name"))
122+
.ifPresent(v -> result.getProfile().setDisplayName(v));
123+
} else {
124+
log.debug("Command output:{}{}", "\n", output);
125+
}
126+
}
127+
} else if (cfg.getExit().getFailure().contains(result.getExitStatus())) {
128+
log.debug("{} stdout:{}{}", cfg.getCommand(), "\n", output);
129+
result.setSuccess(false);
130+
} else {
131+
log.error("{} stdout:{}{}", cfg.getCommand(), "\n", output);
132+
throw new InternalServerError("Exec auth command returned with unexpected exit status");
133+
}
134+
135+
return result;
136+
} catch (IOException | InterruptedException | TimeoutException e) {
137+
throw new InternalServerError(e);
138+
}
139+
}
140+
141+
}

0 commit comments

Comments
 (0)