Compare commits

..

7 Commits

Author SHA1 Message Date
Max Dor
99b7d9f27d Complete the documentation and polish the code 2018-11-01 05:09:47 +01:00
Max Dor
ded5e3db5e Add support for all features for Exec Identity Store 2018-11-01 02:15:56 +01:00
Max Dor
b892d19023 Add skeleton support for Directory and Identity in Exec IdStore 2018-10-31 03:49:06 +01:00
Max Dor
026a2e82d9 Further progress on Exec Identity Store 2018-10-29 07:00:07 +01:00
Max Dor
b881f73798 Add support for setting build version using env variable 2018-10-28 20:20:30 +01:00
Max Dor
99d793b5ed Add initial experimental support for #58
- Skeleton for the whole identity store
- Support Authentication
2018-10-20 08:08:14 +02:00
Max Dor
cb02f62b9d Fix #77 2018-10-19 00:21:04 +02:00
31 changed files with 3131 additions and 18 deletions

View File

@@ -41,17 +41,25 @@ def debBuildDataPath = "${debBuildBasePath}${debDataPath}"
def debBuildSystemdPath = "${debBuildBasePath}${debSystemdPath}" def debBuildSystemdPath = "${debBuildBasePath}${debSystemdPath}"
def dockerImageName = "kamax/mxisd" def dockerImageName = "kamax/mxisd"
def dockerImageTag = "${dockerImageName}:${gitVersion()}" def dockerImageTag = "${dockerImageName}:${mxisdVersion()}"
String mxisdVersion() {
def versionPattern = Pattern.compile("v(\\d+\\.)?(\\d+\\.)?(\\d+)(-.*)?")
String version = System.getenv('MXISD_BUILD_VERSION')
if (version == null || version.size() == 0) {
version = gitVersion()
}
return versionPattern.matcher(version).matches() ? version.substring(1) : version
}
String gitVersion() { String gitVersion() {
def versionPattern = Pattern.compile("v(\\d+\\.)?(\\d+\\.)?(\\d+)(-.*)?")
ByteArrayOutputStream out = new ByteArrayOutputStream() ByteArrayOutputStream out = new ByteArrayOutputStream()
exec { exec {
commandLine = ['git', 'describe', '--tags', '--always', '--dirty'] commandLine = ['git', 'describe', '--tags', '--always', '--dirty']
standardOutput = out standardOutput = out
} }
def v = out.toString().replace(System.lineSeparator(), '') return out.toString().replace(System.lineSeparator(), '');
return versionPattern.matcher(v).matches() ? v.substring(1) : v
} }
buildscript { buildscript {
@@ -126,6 +134,9 @@ dependencies {
// SendGrid SDK to send emails from GCE // SendGrid SDK to send emails from GCE
compile 'com.sendgrid:sendgrid-java:2.2.2' 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 'junit:junit:4.12'
testCompile 'com.github.tomakehurst:wiremock:2.8.0' testCompile 'com.github.tomakehurst:wiremock:2.8.0'
} }
@@ -151,7 +162,7 @@ processResources {
task buildDeb(dependsOn: build) { task buildDeb(dependsOn: build) {
doLast { doLast {
def v = gitVersion() def v = mxisdVersion()
println "Version for package: ${v}" println "Version for package: ${v}"
mkdir distDir mkdir distDir
mkdir debBuildBasePath mkdir debBuildBasePath

View File

@@ -1,4 +1,7 @@
# Debian package # Debian package
## Requirements
- Any distribution that supports Java 8
## Install ## Install
1. Download the [latest release](https://github.com/kamax-matrix/mxisd/releases/latest) 1. Download the [latest release](https://github.com/kamax-matrix/mxisd/releases/latest)
2. Run: 2. Run:

View File

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

470
docs/stores/exec.md Normal file
View File

@@ -0,0 +1,470 @@
# Exec Identity Store
- [Features](#features)
- [Overview](#overview)
- [Configuration](#configuration)
- [Global](#global)
- [Tokens](#tokens)
- [Executable](#executable)
- [Input](#input)
- [Output](#output)
- [Examples](#examples)
- [Per-Feature](#per-feature)
- [Authentication](#authentication)
- [Tokens](#tokens-1)
- [Input](#input-1)
- [Output](#output-1)
- [Directory](#directory)
- [Tokens](#tokens-2)
- [Input](#input-2)
- [Output](#output-2)
- [Identity](#identity)
- [Single Lookup](#single-lookup)
- [Tokens](#tokens-3)
- [Input](#input-3)
- [Output](#output-3)
- [Bulk Lookup](#bulk-lookup)
- [Tokens](#tokens-4)
- [Input](#input-4)
- [Output](#output-4)
- [Profile](#profile)
- [Tokens](#tokens-5)
- [Input](#input-5)
- [Output](#output-5)
---
## Features
| Name | Supported |
|-------------------------------------------------|-----------|
| [Authentication](../features/authentication.md) | Yes |
| [Directory](../features/directory.md) | Yes |
| [Identity](../features/identity.md) | Yes |
| [Profile](#profile) | Yes |
This Identity Store lets you run arbitrary commands to handle the various requests in each support feature.
It is the most versatile Identity store of mxisd, allowing you to connect any kind of logic with any executable/script.
## Overview
Each request can be mapping to a fully customizable command configuration.
The various parameters can be provided via any combination of:
- [Standard Input](https://en.wikipedia.org/wiki/Standard_streams#Standard_input_(stdin))
- [Command-line arguments](https://en.wikipedia.org/wiki/Command-line_interface#Arguments)
- [Environment variables](https://en.wikipedia.org/wiki/Environment_variable)
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 any combination of:
- [Exit status](https://en.wikipedia.org/wiki/Exit_status)
- [Standard Output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_(stdout))
Each of those supports a set of configuration item to decide how to process the value and/or in which format.
All values, inputs and outputs are UTF-8 encoded.
## Configuration
Each feature comes with a set of possible lookup/action which is mapped to a generic configuration item block.
We will use the term `Executable` for each lookup/action and `Processor` for each configuration block.
### Global
```yaml
exec.enabled: <boolean>
```
Enable/disable the Identity store at a global/default level. Each feature can still be individually enabled/disabled.
#### Tokens
The following options allow to globally set tokens for value replacement across all features and processors config.
Not all features use all tokens, and each feature might also have its own specific tokens. See each feature documentation.
They can be set within the following scope:
```yaml
exec.token.<token>: '<value>'
```
---
The following tokens and default values are available:
```yaml
localpart: '{localpart}'
```
Localpart of Matrix User IDs
```yaml
domain: '{domain}'
```
Domain of Matrix User IDs
```yaml
mxid: '{mxid}'
```
Full representation of Matrix User IDs
```yaml
medium: '{medium}'
```
Medium of 3PIDs
```yaml
address: '{address}'
```
Address of 3PIDs
```yaml
type: '{type}'
```
Type of query
```yaml
query: '{query}'
```
Query value
### Executable
*Executable*s have the following options:
```yaml
command: '/path/to/executableOrScript'
```
Set the executable (relative or absolute) path to be executed. If no command is given, the action will return a "neutral"
result if possible or be skipped altogether.
---
Command line arguments can be given via a list via both YAML formats:
```yaml
args:
- '-t'
- '{token}'
- '-v'
- 'value'
```
or
```yaml
args: ['-t', '{token}', '-v', 'value]
```
Each argument will be processed for token replacement.
---
Environment variables can be given as key/value pairs:
```yaml
env:
ENV_VAR_1: 'value'
ENV_VAR_2: '{token}'
```
Each variable value will be processed for token replacement.
#### Input
Standard input can be configured in the namespaces `input` with:
- `type`: The format to use
- `template`: The full or partial template with tokens to be used when generating the input
Not all features and *Executable*s allow for a template to be provided.
Templates for listed-based input are not supported at this time.
Default templates may be provided per *Executable*.
The following types are available:
- `json`: Use JSON format, shared with the [REST Identity Store](rest.md)
- `plain`: Use a custom multi-lines, optionally tab-separated input
#### Output
Standard output can be configured in the namespaces `output` with:
- `type`: The format to use
- `template`: The full or partial template with tokens to be used when processing the output
Not all features and *Executable*s allow for a template to be provided.
Templates for listed-based output are not supported at this time.
Default templates may be provided per *Executable*.
The following types are available:
- `json`: Use JSON format, shared with the [REST Identity Store](rest.md)
- `plain`: Use a custom multi-lines, optionally tab-separated output
### Examples
#### Basic
```yaml
exec.auth.enabled: true
exec.auth.command: '/opt/mxisd-exec/auth.sh'
exec.auth.args: ['{localpart}']
exec.auth.input.type: 'plain'
exec.auth.input.template: '{password}'
exec.auth.env:
DOMAIN: '{domain}'
```
With Authentication enabled, run `/opt/mxisd-exec/auth.sh` when validating credentials, providing:
- A single command-line argument to provide the `localoart` as username
- A plain text string with the password token for standard input, which will be replaced by the password to check
- A single environment variable `DOMAIN` containing Matrix ID domain, if given
The command will use the default values for:
- Success exit status of `0`
- Failure exit status of `1`
- Any other exit status considered as error
- The standard output processing as not processed
#### Advanced
Given the fictional `placeholder` feature:
```yaml
exec.enabled: true
exec.token.mxid: '{matrixId}'
exec.placeholder.token.localpart: '{username}'
exec.placeholder.command: '/path/to/executable'
exec.placeholder.args:
- '-u'
- '{username}'
exec.placeholder.env:
MATRIX_DOMAIN: '{domain}'
MATRIX_USER_ID: '{matrixId}'
exec.placeholder.output.type: 'json'
exec.placeholder.exit.success: [0, 128]
exec.placeholder.exit.failure: [1, 129]
```
With:
- The Identity store enabled for all features
- A global specific token `{matrixId}` for Matrix User IDs, replacing the default `{mxid}`
Running `/path/to/executable` providing:
- A custom token for localpart, `{username}`, used as a 2nd command-line argument
- An extracted Matrix User ID `localpart` provided as the second command line argument, the first one being `-u`
- A password, the extracted Matrix `domain` and the full User ID as arbitrary environment variables, respectively
`PASSWORD`, `MATRIX_DOMAIN` and `MATRIX_USER_ID`
After execution:
- Process stdout as [JSON](https://en.wikipedia.org/wiki/JSON)
- Consider exit status `0` and `128` as success and try to process the stdout for data
- Consider exit status `1` and `129` as failure and try to process the stdout for error code and message
### Per Feature
See each dedicated [Feature](#features) section.
## Authentication
The Authentication feature can be enabled/disabled using:
```yaml
exec.auth.enabled: <true/false>
```
---
This feature provides a single *Executable* under the namespace:
```yaml
exec.auth:
...
```
### Tokens
The following tokens/default values are specific to this feature:
```yaml
password: '{password}'
```
The provided password
### Input
Supported input types and default templates:
#### JSON (`json`)
Same as the [REST Identity Store](rest.md);
#### Plain (`plain`)
Default template:
```
{localpart}
{domain}
{mxid}
{password}
```
### Output
Supported output types and default templates:
#### JSON (`json`)
Same as the [REST Identity Store](rest.md);
#### Plain (`plain`)
**NOTE:** This has limited support. Use the JSON type for full support.
Default template:
```
[success status, true or 1 are interpreted as success]
[display name of the user]
```
## Directory
The Directory feature can be enabled/disabled using:
```yaml
exec.directory.enabled: <true/false>
```
---
Two search types configuration namespace are available, using the same input/output formats and templates:
By name:
```yaml
exec.directory.search.byName:
...
```
By 3PID:
```yaml
exec.directory.search.byThreepid:
...
```
#### Tokens
No specific tokens are available.
#### Input
Supported input types and default templates:
##### JSON (`json`)
Same as the [REST Identity Store](rest.md);
##### Plain (`plain`)
Default template:
```
[type of search, following the REST Identity store format]
[query string]
```
#### Output
Supported output types and default templates:
##### JSON (`json`)
Same as the [REST Identity Store](rest.md);
##### Plain (`plain`)
**Not supported at this time.** Use the JSON type.
## Identity
The Identity feature can be enabled/disabled using:
```yaml
exec.identity.enabled: <true/false>
```
### Single lookup
Configuration namespace:
```yaml
exec.identity.lookup.single:
...
```
#### Tokens
No specific tokens are available.
#### Input
Supported input types and default templates:
##### JSON (`json`)
Same as the [REST Identity Store](rest.md);
##### Plain (`plain`)
Default template:
```
{medium}
{address}
```
#### Output
Supported output types and default templates:
##### JSON (`json`)
Same as the [REST Identity Store](rest.md);
##### Plain (`plain`)
Default template:
```
[User ID type, as documented in the REST Identity Store]
[User ID value]
```
The User ID type will default to `localpart` if:
- Only one line is returned
- The first line is empty
### Bulk lookup
Configuration namespace:
```yaml
exec.identity.lookup.bulk:
...
```
#### Tokens
No specific tokens are available.
#### Input
Supported input types and default templates:
##### JSON (`json`)
**NOTE:** Custom Templates are not supported.
Same as the [REST Identity Store](rest.md).
##### Plain (`plain`)
**Not supported at this time.** Use the JSON type.
#### Output
Supported output types and default templates:
##### JSON (`json`)
**NOTE:** Custom Templates are not supported.
Same as the [REST Identity Store](rest.md).
##### Plain (`plain`)
**Not supported at this time.** Use the JSON type.
## Profile
The Profile feature can be enabled/disabled using:
```yaml
exec.profile.enabled: <true/false>
```
---
The following *Executable*s namespace are available, share the same input/output formats and templates:
Get Display name:
```yaml
exec.profile.displayName:
...
```
Get 3PIDs:
```yaml
exec.profile.threePid:
...
```
Get Roles:
```yaml
exec.profile.role:
...
```
### Tokens
No specific tokens are available.
### Input
Supported input types and default templates:
#### JSON (`json`)
Same as the [REST Identity Store](rest.md);
#### Plain (`plain`)
Default template:
```
{localpart}
{domain}
{mxid}
```
### Output
Supported output types and default templates:
#### JSON (`json`)
Same as the [REST Identity Store](rest.md);
#### Plain (`plain`)
**Not supported at this time.** Use the JSON type.

View File

@@ -1,8 +1,8 @@
/* /*
* mxisd - Matrix Identity Server Daemon * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -30,6 +30,10 @@ public class UserID {
// stub for (de)serialization // stub for (de)serialization
} }
public UserID(UserIdType type, String value) {
this(type.getId(), value);
}
public UserID(String type, String value) { public UserID(String type, String value) {
this.type = type; this.type = type;
this.value = value; this.value = value;

View File

@@ -1,8 +1,8 @@
/* /*
* mxisd - Matrix Identity Server Daemon * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -59,9 +59,10 @@ public class AuthManager {
continue; continue;
} }
log.info("Attempting authentication with store {}", provider.getClass().getSimpleName());
BackendAuthResult result = provider.authenticate(mxid, password); BackendAuthResult result = provider.authenticate(mxid, password);
if (result.isSuccess()) { if (result.isSuccess()) {
String mxId; String mxId;
if (UserIdType.Localpart.is(result.getId().getType())) { if (UserIdType.Localpart.is(result.getId().getType())) {
mxId = MatrixID.from(result.getId().getValue(), mxCfg.getDomain()).acceptable().getId(); mxId = MatrixID.from(result.getId().getValue(), mxCfg.getDomain()).acceptable().getId();

View File

@@ -1,8 +1,8 @@
/* /*
* mxisd - Matrix Identity Server Daemon * 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 * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@@ -38,6 +38,10 @@ public class BackendAuthResult {
return displayName; return displayName;
} }
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public Set<ThreePid> getThreePids() { public Set<ThreePid> getThreePids() {
return threePids; return threePids;
} }
@@ -73,6 +77,10 @@ public class BackendAuthResult {
private UserID id; private UserID id;
private BackendAuthProfile profile = new BackendAuthProfile(); private BackendAuthProfile profile = new BackendAuthProfile();
public void setSuccess(boolean success) {
this.success = success;
}
public Boolean isSuccess() { public Boolean isSuccess() {
return success; return success;
} }
@@ -81,6 +89,10 @@ public class BackendAuthResult {
return id; return id;
} }
public void setId(UserID id) {
this.id = id;
}
public BackendAuthProfile getProfile() { public BackendAuthProfile getProfile() {
return profile; 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,129 @@
/*
* 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.backend.rest.RestAuthRequestJson;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.exception.InternalServerError;
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 java.util.Objects;
import java.util.Optional;
@Component
public class ExecAuthStore extends ExecStore implements AuthenticatorProvider {
private final 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()));
Processor<ExecAuthResult> p = new Processor<>(cfg);
p.addTokenMapper(cfg.getToken().getLocalpart(), uId::getLocalPart);
p.addTokenMapper(cfg.getToken().getDomain(), uId::getDomain);
p.addTokenMapper(cfg.getToken().getMxid(), uId::getId);
p.addTokenMapper(cfg.getToken().getPassword(), () -> password);
p.addJsonInputTemplate(tokens -> {
RestAuthRequestJson json = new RestAuthRequestJson();
json.setLocalpart(tokens.getLocalpart());
json.setDomain(tokens.getDomain());
json.setMxid(tokens.getMxid());
json.setPassword(tokens.getPassword());
return json;
});
p.addInputTemplate(PlainType, tokens -> tokens.getLocalpart() + System.lineSeparator() +
tokens.getDomain() + System.lineSeparator() +
tokens.getMxid() + System.lineSeparator() +
tokens.getPassword() + System.lineSeparator()
);
p.withExitHandler(pr -> result.setExitStatus(pr.getExitValue()));
p.withSuccessHandler(pr -> result.setSuccess(true));
p.withSuccessDefault(o -> result);
p.addSuccessMapper(JsonType, output -> {
JsonObject data = GsonUtil.getObj(GsonUtil.parseObj(output), "auth");
GsonUtil.findPrimitive(data, "success")
.map(JsonPrimitive::getAsBoolean)
.ifPresent(result::setSuccess);
GsonUtil.findObj(data, "profile")
.flatMap(profile -> GsonUtil.findString(profile, "display_name"))
.ifPresent(v -> result.getProfile().setDisplayName(v));
return result;
});
p.addSuccessMapper(PlainType, output -> {
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));
}
return result;
});
p.withFailureHandler(pr -> result.setSuccess(false));
p.withFailureDefault(o -> result);
return p.execute();
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.json.GsonUtil;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchRequest;
import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchResult;
import io.kamax.mxisd.directory.IDirectoryProvider;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class ExecDirectoryStore extends ExecStore implements IDirectoryProvider {
private ExecConfig.Directory cfg;
private MatrixConfig mxCfg;
@Autowired
public ExecDirectoryStore(ExecConfig cfg, MatrixConfig mxCfg) {
this(cfg.getDirectory(), mxCfg);
}
public ExecDirectoryStore(ExecConfig.Directory cfg, MatrixConfig mxCfg) {
this.cfg = cfg;
this.mxCfg = mxCfg;
}
@Override
public boolean isEnabled() {
return cfg.isEnabled();
}
private UserDirectorySearchResult search(ExecConfig.Process cfg, UserDirectorySearchRequest request) {
if (StringUtils.isEmpty(cfg.getCommand())) {
return UserDirectorySearchResult.empty();
}
Processor<UserDirectorySearchResult> p = new Processor<>(cfg);
p.addJsonInputTemplate(tokens -> new UserDirectorySearchRequest(tokens.getType(), tokens.getQuery()));
p.addInputTemplate(PlainType, tokens -> tokens.getType() + System.lineSeparator() + tokens.getQuery());
p.addTokenMapper(cfg.getToken().getType(), request::getBy);
p.addTokenMapper(cfg.getToken().getQuery(), request::getSearchTerm);
p.addSuccessMapper(JsonType, output -> {
if (StringUtils.isBlank(output)) {
return UserDirectorySearchResult.empty();
}
UserDirectorySearchResult response = GsonUtil.get().fromJson(output, UserDirectorySearchResult.class);
for (UserDirectorySearchResult.Result result : response.getResults()) {
result.setUserId(MatrixID.asAcceptable(result.getUserId(), mxCfg.getDomain()).getId());
}
return response;
});
p.withFailureDefault(output -> new UserDirectorySearchResult());
return p.execute();
}
@Override
public UserDirectorySearchResult searchByDisplayName(String query) {
return search(cfg.getSearch().getByName(), new UserDirectorySearchRequest("name", query));
}
@Override
public UserDirectorySearchResult searchBy3pid(String query) {
return search(cfg.getSearch().getByName(), new UserDirectorySearchRequest("threepid", query));
}
}

View File

@@ -0,0 +1,209 @@
/*
* 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.JsonArray;
import com.google.gson.JsonParseException;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePid;
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.backend.rest.LookupBulkResponseJson;
import io.kamax.mxisd.backend.rest.LookupSingleResponseJson;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.exception.InternalServerError;
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.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
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;
@Autowired
public ExecIdentityStore(ExecConfig cfg, MatrixConfig mxCfg) {
this(cfg.getIdentity(), mxCfg);
}
public ExecIdentityStore(ExecConfig.Identity cfg, MatrixConfig mxCfg) {
this.cfg = cfg;
this.mxCfg = mxCfg;
}
@Override
public boolean isEnabled() {
return cfg.isEnabled();
}
@Override
public boolean isLocal() {
return true;
}
@Override
public int getPriority() {
return cfg.getPriority();
}
private ExecConfig.Process getSingleCfg() {
return cfg.getLookup().getSingle();
}
private _MatrixID getUserId(UserID id) {
if (Objects.isNull(id)) {
throw new JsonParseException("User id key is not present");
}
if (UserIdType.Localpart.is(id.getType())) {
return MatrixID.asAcceptable(id.getValue(), mxCfg.getDomain());
}
if (UserIdType.MatrixID.is(id.getType())) {
return MatrixID.asAcceptable(id.getValue());
}
throw new InternalServerError("Unknown user type: " + id.getType());
}
@Override
public Optional<SingleLookupReply> find(SingleLookupRequest request) {
Processor<Optional<SingleLookupReply>> p = new Processor<>();
p.withConfig(cfg.getLookup().getSingle());
p.addTokenMapper(getSingleCfg().getToken().getMedium(), request::getType);
p.addTokenMapper(getSingleCfg().getToken().getAddress(), request::getThreePid);
p.addJsonInputTemplate(tokens -> new ThreePid(tokens.getMedium(), tokens.getAddress()));
p.addInputTemplate(PlainType, tokens -> tokens.getMedium()
+ System.lineSeparator()
+ tokens.getAddress()
);
p.addSuccessMapper(JsonType, output -> {
if (StringUtils.isBlank(output)) {
return Optional.empty();
}
return GsonUtil.findObj(GsonUtil.parseObj(output), "lookup")
.filter(obj -> !obj.entrySet().isEmpty())
.map(json -> GsonUtil.get().fromJson(json, LookupSingleResponseJson.class))
.map(lookup -> getUserId(lookup.getId()))
.map(mxId -> new SingleLookupReply(request, mxId));
});
p.addSuccessMapper(PlainType, 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 ? UserIdType.Localpart.getId() : lines[0]);
String value = StringUtils.trimToEmpty(lines.length == 2 ? lines[1] : lines[0]);
if (UserIdType.Localpart.is(type)) {
return Optional.of(new SingleLookupReply(request, MatrixID.asAcceptable(value, mxCfg.getDomain())));
}
if (UserIdType.MatrixID.is(type)) {
return Optional.of(new SingleLookupReply(request, MatrixID.asAcceptable(value)));
}
throw new InternalServerError("Invalid user type: " + type);
});
p.withFailureDefault(o -> Optional.empty());
return p.execute();
}
@Override
public List<ThreePidMapping> populate(List<ThreePidMapping> mappings) {
Processor<List<ThreePidMapping>> p = new Processor<>();
p.withConfig(cfg.getLookup().getBulk());
p.addInput(JsonType, () -> {
JsonArray tpids = GsonUtil.asArray(mappings.stream()
.map(mapping -> GsonUtil.get().toJsonTree(new ThreePid(mapping.getMedium(), mapping.getValue())))
.collect(Collectors.toList()));
return GsonUtil.get().toJson(GsonUtil.makeObj("lookup", tpids));
});
p.addInput(PlainType, () -> {
StringBuilder input = new StringBuilder();
for (ThreePidMapping mapping : mappings) {
input.append(mapping.getMedium()).append("\t").append(mapping.getValue()).append(System.lineSeparator());
}
return input.toString();
});
p.addSuccessMapper(JsonType, output -> {
if (StringUtils.isBlank(output)) {
return Collections.emptyList();
}
LookupBulkResponseJson response = GsonUtil.get().fromJson(output, LookupBulkResponseJson.class);
return response.getLookup().stream().map(item -> {
ThreePidMapping mapping = new ThreePidMapping();
mapping.setMedium(item.getMedium());
mapping.setValue(item.getAddress());
if (UserIdType.Localpart.is(item.getId().getType())) {
mapping.setValue(MatrixID.asAcceptable(item.getId().getValue(), mxCfg.getDomain()).getId());
return mapping;
}
if (UserIdType.MatrixID.is(item.getId().getType())) {
mapping.setValue(MatrixID.asAcceptable(item.getId().getValue()).getId());
return mapping;
}
throw new InternalServerError("Invalid user type: " + item.getId().getType());
}).collect(Collectors.toList());
});
p.withFailureDefault(output -> Collections.emptyList());
return p.execute();
}
}

View File

@@ -0,0 +1,103 @@
/*
* 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.matrix.json.GsonUtil;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.profile.JsonProfileRequest;
import io.kamax.mxisd.profile.JsonProfileResult;
import io.kamax.mxisd.profile.ProfileProvider;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
@Component
public class ExecProfileStore extends ExecStore implements ProfileProvider {
private ExecConfig.Profile cfg;
@Autowired
public ExecProfileStore(ExecConfig cfg) {
this(cfg.getProfile());
}
public ExecProfileStore(ExecConfig.Profile cfg) {
this.cfg = cfg;
}
@Override
public boolean isEnabled() {
return cfg.isEnabled();
}
private Optional<JsonProfileResult> getFull(_MatrixID userId, ExecConfig.Process cfg) {
Processor<Optional<JsonProfileResult>> p = new Processor<>(cfg);
p.addJsonInputTemplate(tokens -> new JsonProfileRequest(tokens.getLocalpart(), tokens.getDomain(), tokens.getMxid()));
p.addInputTemplate(PlainType, tokens -> tokens.getLocalpart() + System.lineSeparator()
+ tokens.getDomain() + System.lineSeparator()
+ tokens.getMxid() + System.lineSeparator()
);
p.addTokenMapper(cfg.getToken().getLocalpart(), userId::getLocalPart);
p.addTokenMapper(cfg.getToken().getDomain(), userId::getDomain);
p.addTokenMapper(cfg.getToken().getMxid(), userId::getId);
p.withFailureDefault(v -> Optional.empty());
p.addSuccessMapper(JsonType, output -> {
if (StringUtils.isBlank(output)) {
return Optional.empty();
}
return GsonUtil.findObj(GsonUtil.parseObj(output), "profile")
.map(obj -> GsonUtil.get().fromJson(obj, JsonProfileResult.class));
});
return p.execute();
}
@Override
public Optional<String> getDisplayName(_MatrixID userId) {
return getFull(userId, cfg.getDisplayName()).map(JsonProfileResult::getDisplayName);
}
@Override
public List<_ThreePid> getThreepids(_MatrixID userId) {
return getFull(userId, cfg.getThreePid())
.map(p -> Collections.<_ThreePid>unmodifiableList(p.getThreepids()))
.orElseGet(Collections::emptyList);
}
@Override
public List<String> getRoles(_MatrixID userId) {
return getFull(userId, cfg.getRole())
.map(JsonProfileResult::getRoles)
.orElseGet(Collections::emptyList);
}
}

View File

@@ -0,0 +1,252 @@
/*
* 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.json.GsonUtil;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.exception.InternalServerError;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class ExecStore {
public static final String JsonType = "json";
public static final String PlainType = "plain";
protected static String toJson(Object o) {
return GsonUtil.get().toJson(o);
}
private final Logger log = LoggerFactory.getLogger(ExecStore.class);
private Supplier<ProcessExecutor> executorSupplier = () -> new ProcessExecutor().readOutput(true);
public void setExecutorSupplier(Supplier<ProcessExecutor> supplier) {
executorSupplier = supplier;
}
public class Processor<V> {
private ExecConfig.Process cfg;
private Supplier<Optional<String>> inputSupplier;
private Function<String, String> inputTypeMapper;
private Function<String, String> inputUnknownTypeMapper;
private Map<String, Supplier<String>> inputTypeSuppliers;
private Map<String, Function<ExecConfig.TokenOverride, String>> inputTypeTemplates;
private Supplier<String> inputTypeNoTemplateHandler;
private Map<String, Supplier<String>> tokenMappers;
private Function<String, String> tokenHandler;
private Consumer<ProcessResult> onExitHandler;
private Consumer<ProcessResult> successHandler;
private Map<String, Function<String, V>> successMappers;
private Function<String, V> successDefault;
private Consumer<ProcessResult> failureHandler;
private Map<String, Function<String, V>> failureMappers;
private Function<String, V> failureDefault;
private Consumer<ProcessResult> unknownHandler;
private Map<String, Function<String, V>> unknownMappers;
private Function<String, V> unknownDefault;
public Processor(ExecConfig.Process cfg) {
this();
withConfig(cfg);
}
public Processor() {
tokenMappers = new HashMap<>();
inputTypeSuppliers = new HashMap<>();
inputTypeTemplates = new HashMap<>();
withTokenHandler(tokenHandler = input -> {
for (Map.Entry<String, Supplier<String>> entry : tokenMappers.entrySet()) {
input = input.replace(entry.getKey(), entry.getValue().get());
}
return input;
});
inputTypeNoTemplateHandler = () -> cfg.getInput().getType()
.map(type -> inputTypeTemplates.get(type).apply(cfg.getToken()))
.orElse("");
inputUnknownTypeMapper = type -> tokenHandler.apply(cfg.getInput().getTemplate().orElseGet(inputTypeNoTemplateHandler));
inputTypeMapper = type -> {
if (!inputTypeSuppliers.containsKey(type)) {
return inputUnknownTypeMapper.apply(type);
}
return inputTypeSuppliers.get(type).get();
};
inputSupplier = () -> cfg.getInput().getType().map(type -> inputTypeMapper.apply(type));
withExitHandler(pr -> {
});
successHandler = pr -> {
};
successMappers = new HashMap<>();
successDefault = output -> {
log.info("{} stdout: {}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec command has no success handler configured. This is a bug. Please report.");
};
failureHandler = pr -> {
};
failureMappers = new HashMap<>();
failureDefault = output -> {
log.info("{} stdout: {}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec command has no failure handler configured. This is a bug. Please report.");
};
unknownHandler = pr -> log.warn("Unexpected exit status: {}", pr.getExitValue());
unknownMappers = new HashMap<>();
withUnknownDefault(output -> {
log.error("{} stdout:{}{}", cfg.getCommand(), System.lineSeparator(), output);
throw new InternalServerError("Exec command returned with unexpected exit status");
});
}
public void withConfig(ExecConfig.Process cfg) {
this.cfg = cfg;
}
public void addTokenMapper(String token, Supplier<String> data) {
tokenMappers.put(token, data);
}
public void withTokenHandler(Function<String, String> handler) {
tokenHandler = handler;
}
public void addInput(String type, Supplier<String> handler) {
inputTypeSuppliers.put(type, handler);
}
protected void addInputTemplate(String type, Function<ExecConfig.TokenOverride, String> template) {
inputTypeTemplates.put(type, template);
}
public void addJsonInputTemplate(Function<ExecConfig.TokenOverride, Object> template) {
inputTypeTemplates.put(JsonType, token -> GsonUtil.get().toJson(template.apply(token)));
}
public void withExitHandler(Consumer<ProcessResult> handler) {
onExitHandler = handler;
}
public void withSuccessHandler(Consumer<ProcessResult> handler) {
successHandler = handler;
}
public void addSuccessMapper(String type, Function<String, V> mapper) {
successMappers.put(type, mapper);
}
public void withSuccessDefault(Function<String, V> mapper) {
successDefault = mapper;
}
public void withFailureHandler(Consumer<ProcessResult> handler) {
failureHandler = handler;
}
public void addFailureMapper(String type, Function<String, V> mapper) {
failureMappers.put(type, mapper);
}
public void withFailureDefault(Function<String, V> mapper) {
failureDefault = mapper;
}
public void addUnknownMapper(String type, Function<String, V> mapper) {
unknownMappers.put(type, mapper);
}
public void withUnknownDefault(Function<String, V> mapper) {
unknownDefault = mapper;
}
public V execute() {
log.info("Executing {}", cfg.getCommand());
try {
ProcessExecutor psExec = executorSupplier.get();
List<String> args = new ArrayList<>();
args.add(tokenHandler.apply(cfg.getCommand()));
args.addAll(cfg.getArgs().stream().map(arg -> tokenHandler.apply(arg)).collect(Collectors.toList()));
psExec.command(args);
psExec.environment(new HashMap<>(cfg.getEnv()).entrySet().stream()
.peek(e -> e.setValue(tokenHandler.apply(e.getValue())))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)));
inputSupplier.get().ifPresent(input -> psExec.redirectInput(IOUtils.toInputStream(input, StandardCharsets.UTF_8)));
ProcessResult psResult = psExec.execute();
String output = psResult.outputUTF8();
onExitHandler.accept(psResult);
if (cfg.getExit().getSuccess().contains(psResult.getExitValue())) {
successHandler.accept(psResult);
return cfg.getOutput().getType()
.map(type -> successMappers.getOrDefault(type, successDefault).apply(output))
.orElseGet(() -> successDefault.apply(output));
} else if (cfg.getExit().getFailure().contains(psResult.getExitValue())) {
failureHandler.accept(psResult);
return cfg.getOutput().getType()
.map(type -> failureMappers.getOrDefault(type, failureDefault).apply(output))
.orElseGet(() -> failureDefault.apply(output));
} else {
unknownHandler.accept(psResult);
return cfg.getOutput().getType()
.map(type -> unknownMappers.getOrDefault(type, unknownDefault).apply(output))
.orElseGet(() -> unknownDefault.apply(output));
}
} catch (RuntimeException | IOException | InterruptedException | TimeoutException e) {
log.error("Failed to execute {}", cfg.getCommand());
log.debug("Internal exception:", e);
throw new InternalServerError(e);
}
}
}
}

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.backend.rest; package io.kamax.mxisd.backend.rest;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.mxisd.UserID; import io.kamax.mxisd.UserID;
public class LookupSingleResponseJson { public class LookupSingleResponseJson {
@@ -32,12 +33,28 @@ public class LookupSingleResponseJson {
return medium; return medium;
} }
public void setMedium(String medium) {
this.medium = medium;
}
public void setMedium(ThreePidMedium medium) {
setMedium(medium.getId());
}
public String getAddress() { public String getAddress() {
return address; return address;
} }
public void setAddress(String address) {
this.address = address;
}
public UserID getId() { public UserID getId() {
return id; return id;
} }
public void setId(UserID id) {
this.id = id;
}
} }

View File

@@ -0,0 +1,540 @@
/*
* 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 IO {
private String type;
private String template;
public Optional<String> getType() {
return Optional.ofNullable(type);
}
public void setType(String type) {
this.type = type;
}
public Optional<String> getTemplate() {
return Optional.ofNullable(template);
}
public void setTemplate(String template) {
this.template = template;
}
}
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;
private String medium;
private String address;
private String type;
private String query;
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 String getMedium() {
return StringUtils.defaultIfEmpty(medium, getToken().getMedium());
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getAddress() {
return StringUtils.defaultIfEmpty(address, getToken().getAddress());
}
public void setAddress(String address) {
this.address = address;
}
public String getType() {
return StringUtils.defaultIfEmpty(type, getToken().getType());
}
public void setType(String type) {
this.type = type;
}
public String getQuery() {
return StringUtils.defaultIfEmpty(query, getToken().getQuery());
}
public void setQuery(String query) {
this.query = query;
}
}
public class Token {
private String localpart = "{localpart}";
private String domain = "{domain}";
private String mxid = "{mxid}";
private String password = "{password}";
private String medium = "{medium}";
private String address = "{address}";
private String type = "{type}";
private String query = "{query}";
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 String getMedium() {
return medium;
}
public void setMedium(String medium) {
this.medium = medium;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getQuery() {
return query;
}
public void setQuery(String query) {
this.query = query;
}
}
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 IO input = new IO();
private Exit exit = new Exit();
private IO output = new IO();
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 IO getInput() {
return input;
}
public void setInput(IO input) {
this.input = input;
}
public Exit getExit() {
return exit;
}
public void setExit(Exit exit) {
this.exit = exit;
}
public IO getOutput() {
return output;
}
public void setOutput(IO 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 {
public class Search {
private Process byName = new Process();
private Process byThreepid = new Process();
public Process getByName() {
return byName;
}
public void setByName(Process byName) {
this.byName = byName;
}
public Process getByThreepid() {
return byThreepid;
}
public void setByThreepid(Process byThreepid) {
this.byThreepid = byThreepid;
}
}
private Boolean enabled;
private Search search = new Search();
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Search getSearch() {
return search;
}
public void setSearch(Search search) {
this.search = search;
}
}
public class Lookup {
private Process single = new Process();
private Process bulk = new Process();
public Process getSingle() {
return single;
}
public void setSingle(Process single) {
this.single = single;
}
public Process getBulk() {
return bulk;
}
public void setBulk(Process bulk) {
this.bulk = bulk;
}
}
public class Identity {
private Boolean enabled;
private int priority;
private Lookup lookup = new Lookup();
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public int getPriority() {
return priority;
}
public void setPriority(int priority) {
this.priority = priority;
}
public Lookup getLookup() {
return lookup;
}
public void setLookup(Lookup lookup) {
this.lookup = lookup;
}
}
public class Profile {
private Boolean enabled;
private Process displayName = new Process();
private Process threePid = new Process();
private Process role = new Process();
public Boolean isEnabled() {
return enabled;
}
public void setEnabled(Boolean enabled) {
this.enabled = enabled;
}
public Process getDisplayName() {
return displayName;
}
public void setDisplayName(Process displayName) {
this.displayName = displayName;
}
public Process getThreePid() {
return threePid;
}
public void setThreePid(Process threePid) {
this.threePid = threePid;
}
public Process getRole() {
return role;
}
public void setRoles(Process role) {
this.role = role;
}
}
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 ExecConfig compute() {
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());
}
return this;
}
}

View File

@@ -29,6 +29,11 @@ public class UserDirectorySearchRequest {
setSearchTerm(searchTerm); setSearchTerm(searchTerm);
} }
public UserDirectorySearchRequest(String type, String searchTerm) {
setBy(type);
setSearchTerm(searchTerm);
}
public String getBy() { public String getBy() {
return by; return by;
} }

View File

@@ -25,6 +25,10 @@ import java.util.Set;
public class UserDirectorySearchResult { public class UserDirectorySearchResult {
public static UserDirectorySearchResult empty() {
return new UserDirectorySearchResult();
}
public static class Result { public static class Result {
private String displayName; private String displayName;

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
package io.kamax.mxisd.profile;
import io.kamax.matrix._MatrixID;
public class JsonProfileRequest {
private String localpart;
private String domain;
private String mxid;
public JsonProfileRequest(_MatrixID mxId) {
this.localpart = mxId.getLocalPart();
this.domain = mxId.getDomain();
this.mxid = mxId.getId();
}
public JsonProfileRequest(String localpart, String domain, String mxId) {
this.localpart = localpart;
this.domain = domain;
this.mxid = mxId;
}
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;
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.profile;
import io.kamax.matrix.ThreePid;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class JsonProfileResult {
private String displayName;
private List<ThreePid> threepids;
private List<String> roles;
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public List<ThreePid> getThreepids() {
return threepids;
}
public void setThreepids(List<ThreePid> threepids) {
this.threepids = threepids;
}
public void addThreepid(ThreePid threepid) {
if (Objects.isNull(threepids)) threepids = new ArrayList<>();
threepids.add(threepid);
}
public List<String> getRoles() {
return roles;
}
public void setRoles(List<String> roles) {
this.roles = roles;
}
public void addRole(String role) {
if (Objects.isNull(roles)) roles = new ArrayList<>();
roles.add(role);
}
}

View File

@@ -0,0 +1,28 @@
/*
* 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.util;
@FunctionalInterface
public interface TriFunction<T, U, V, R> {
R apply(T t, U u, V v);
}

View File

@@ -0,0 +1,202 @@
/*
* 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._MatrixID;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.config.ExecConfig;
import org.apache.commons.lang3.RandomStringUtils;
import org.junit.Before;
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 setValidInput() {
// 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));
}
@Before
public void setValidConfig() {
setValidCommand();
setValidEnv();
setValidArgs();
setValidInput();
setValidExit();
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);
}
public ExecAuthStoreTest() {
cfg = new ExecConfig();
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 emptyLocalpartConfig() {
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();
}
}

View File

@@ -0,0 +1,128 @@
/*
* 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.json.GsonUtil;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.controller.directory.v1.io.UserDirectorySearchResult;
import io.kamax.mxisd.exception.InternalServerError;
import org.apache.commons.lang3.StringUtils;
import org.junit.Test;
import java.util.Collections;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class ExecDirectoryStoreTest extends ExecStoreTest {
public ExecDirectoryStoreTest() {
executables.put("byNameSuccessEmptyResult", () ->
make(0, () ->
GsonUtil.get().toJson(UserDirectorySearchResult.empty())
)
);
executables.put("byNameSuccessSingleResult", () -> make(0, () -> {
UserDirectorySearchResult.Result resultIo = new UserDirectorySearchResult.Result();
resultIo.setUserId(user1Localpart);
resultIo.setDisplayName(user1Name);
UserDirectorySearchResult io = new UserDirectorySearchResult();
io.setLimited(false);
io.setResults(Collections.singleton(resultIo));
return GsonUtil.get().toJson(io);
}));
}
private ExecConfig.Directory getCfg() {
ExecConfig.Directory cfg = new ExecConfig().compute().getDirectory();
assertFalse(cfg.isEnabled());
cfg.setEnabled(true);
assertTrue(cfg.isEnabled());
cfg.getSearch().getByName().getOutput().setType(ExecStore.JsonType);
return cfg;
}
private ExecDirectoryStore getStore(ExecConfig.Directory cfg) {
ExecDirectoryStore store = new ExecDirectoryStore(cfg, getMatrixCfg());
store.setExecutorSupplier(this::build);
assertTrue(store.isEnabled());
return store;
}
private ExecDirectoryStore getStore(String command) {
ExecConfig.Directory cfg = getCfg();
cfg.getSearch().getByName().setCommand(command);
cfg.getSearch().getByThreepid().setCommand(command);
return getStore(cfg);
}
@Test
public void byNameNoCommandDefined() {
ExecConfig.Directory cfg = getCfg();
assertTrue(StringUtils.isEmpty(cfg.getSearch().getByName().getCommand()));
ExecDirectoryStore store = getStore(cfg);
UserDirectorySearchResult result = store.searchByDisplayName("user");
assertFalse(result.isLimited());
assertTrue(result.getResults().isEmpty());
}
@Test
public void byNameSuccessNoOutput() {
UserDirectorySearchResult result = getStore(sno).searchByDisplayName("user");
assertFalse(result.isLimited());
assertTrue(result.getResults().isEmpty());
}
@Test
public void byNameSuccessEmptyResult() {
UserDirectorySearchResult output = getStore("byNameSuccessEmptyResult").searchByDisplayName("user");
assertFalse(output.isLimited());
assertTrue(output.getResults().isEmpty());
}
@Test
public void byNameSuccessSingleResult() {
UserDirectorySearchResult output = getStore("byNameSuccessSingleResult").searchByDisplayName("user");
assertFalse(output.isLimited());
assertEquals(1, output.getResults().size());
UserDirectorySearchResult.Result result = output.getResults().iterator().next();
assertEquals(MatrixID.from(user1Localpart, domain).acceptable().getId(), result.getUserId());
assertEquals(user1Name, result.getDisplayName());
}
@Test
public void byNameFailureNoOutput() {
UserDirectorySearchResult result = getStore(fno).searchByDisplayName("user");
assertFalse(result.isLimited());
assertTrue(result.getResults().isEmpty());
}
@Test(expected = InternalServerError.class)
public void byNameUnknownNoOutput() {
getStore(uno).searchByDisplayName("user");
}
}

View File

@@ -0,0 +1,129 @@
/*
* 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 io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.UserID;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.backend.rest.LookupSingleResponseJson;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.exception.InternalServerError;
import io.kamax.mxisd.lookup.SingleLookupReply;
import io.kamax.mxisd.lookup.SingleLookupRequest;
import org.junit.Test;
import java.util.Optional;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class ExecIdentityStoreTest extends ExecStoreTest {
public ExecIdentityStoreTest() {
executables.put("singleSuccessEmpty", () -> make(0, () ->
GsonUtil.get().toJson(GsonUtil.makeObj("lookup", new JsonObject()))));
executables.put("singleSuccessData", () -> makeJson(0, () -> {
LookupSingleResponseJson json = new LookupSingleResponseJson();
json.setMedium(ThreePidMedium.Email);
json.setAddress(user1Email);
json.setId(new UserID(UserIdType.Localpart, user1Localpart));
return GsonUtil.makeObj("lookup", GsonUtil.get().toJsonTree(json));
}));
executables.put("singleSuccessEmptyFromInvalidOutput", () -> makeJson(0, () -> {
JsonObject lookup = new JsonObject();
lookup.addProperty("medium", "");
return GsonUtil.makeObj("lookup", lookup);
}));
}
private ExecConfig.Identity getCfg() {
ExecConfig.Identity cfg = new ExecConfig().compute().getIdentity();
assertFalse(cfg.isEnabled());
cfg.setEnabled(true);
assertTrue(cfg.isEnabled());
cfg.getLookup().getSingle().getOutput().setType(ExecStore.JsonType);
cfg.getLookup().getBulk().getOutput().setType(ExecStore.JsonType);
return cfg;
}
private ExecIdentityStore getStore(ExecConfig.Identity cfg) {
ExecIdentityStore store = new ExecIdentityStore(cfg, getMatrixCfg());
store.setExecutorSupplier(this::build);
assertTrue(store.isEnabled());
return store;
}
private ExecIdentityStore getStore(String command) {
ExecConfig.Identity cfg = getCfg();
cfg.getLookup().getSingle().setCommand(command);
cfg.getLookup().getBulk().setCommand(command);
return getStore(cfg);
}
@Test
public void singleSuccessNoOutput() {
ExecIdentityStore store = getStore(sno);
SingleLookupRequest req = new SingleLookupRequest();
req.setType(ThreePidMedium.Email.getId());
req.setThreePid(user1Email);
Optional<SingleLookupReply> lookup = store.find(req);
assertFalse(lookup.isPresent());
}
@Test
public void singleSuccessEmpty() {
ExecIdentityStore store = getStore("singleSuccessEmpty");
SingleLookupRequest req = new SingleLookupRequest();
req.setType(ThreePidMedium.Email.getId());
req.setThreePid(user1Email);
Optional<SingleLookupReply> lookup = store.find(req);
assertFalse(lookup.isPresent());
}
@Test
public void singleSuccessData() {
SingleLookupRequest req = new SingleLookupRequest();
req.setType(ThreePidMedium.Email.getId());
req.setThreePid(user1Email);
Optional<SingleLookupReply> lookup = getStore("singleSuccessData").find(req);
assertTrue(lookup.isPresent());
SingleLookupReply reply = lookup.get();
assertEquals(MatrixID.asAcceptable(user1Localpart, domain), reply.getMxid());
}
@Test(expected = InternalServerError.class)
public void singleSuccessEmptyFromInvalidOutput() {
SingleLookupRequest req = new SingleLookupRequest();
req.setType(ThreePidMedium.Email.getId());
req.setThreePid(user1Email);
getStore("singleSuccessEmptyFromInvalidOutput").find(req);
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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 io.kamax.matrix.MatrixID;
import io.kamax.matrix.ThreePid;
import io.kamax.matrix.ThreePidMedium;
import io.kamax.matrix._ThreePid;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.UserIdType;
import io.kamax.mxisd.config.ExecConfig;
import io.kamax.mxisd.profile.JsonProfileResult;
import org.junit.Test;
import java.util.List;
import java.util.Optional;
import static junit.framework.TestCase.assertTrue;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
public class ExecProfileStoreTest extends ExecStoreTest {
private final String seo = "successEmptyOutput";
private final String sn = "successName";
private final String sst = "successSingleThreepid";
private final String smt = "successMultiThreepid";
private final String user1Msisdn = Long.toString(System.currentTimeMillis());
public ExecProfileStoreTest() {
executables.put(seo, () -> make(0, () -> "{}"));
executables.put(sn, () -> makeJson(0, () -> {
JsonObject profile = new JsonObject();
profile.addProperty("display_name", user1Name);
return GsonUtil.makeObj("profile", profile);
}));
executables.put(sst, () -> makeJson(0, () -> {
JsonProfileResult profile = new JsonProfileResult();
profile.addThreepid(new ThreePid(ThreePidMedium.Email.getId(), user1Email));
return GsonUtil.makeObj("profile", profile);
}));
executables.put(smt, () -> makeJson(0, () -> {
JsonProfileResult profile = new JsonProfileResult();
profile.addThreepid(new ThreePid(ThreePidMedium.Email.getId(), user1Email));
profile.addThreepid(new ThreePid(ThreePidMedium.PhoneNumber.getId(), user1Msisdn));
return GsonUtil.makeObj("profile", profile);
}));
}
private ExecConfig.Profile getCfg() {
ExecConfig.Profile cfg = new ExecConfig().compute().getProfile();
assertFalse(cfg.isEnabled());
cfg.setEnabled(true);
assertTrue(cfg.isEnabled());
cfg.getDisplayName().getOutput().setType(ExecStore.JsonType);
cfg.getThreePid().getOutput().setType(ExecStore.JsonType);
cfg.getRole().getOutput().setType(ExecStore.JsonType);
return cfg;
}
private ExecProfileStore getStore(ExecConfig.Profile cfg) {
ExecProfileStore store = new ExecProfileStore(cfg);
store.setExecutorSupplier(this::build);
assertTrue(store.isEnabled());
return store;
}
private ExecProfileStore getStore(String command) {
ExecConfig.Profile cfg = getCfg();
cfg.getDisplayName().setCommand(command);
cfg.getThreePid().setCommand(command);
cfg.getRole().setCommand(command);
return getStore(cfg);
}
@Test
public void getNameSuccessNoOutput() {
Optional<String> name = getStore(sno).getDisplayName(MatrixID.asAcceptable(user1Localpart, domain));
assertFalse(name.isPresent());
}
@Test
public void getNameSuccessEmptyOutput() {
Optional<String> name = getStore(seo).getDisplayName(MatrixID.asAcceptable(user1Localpart, domain));
assertFalse(name.isPresent());
}
@Test
public void getNameSuccess() {
Optional<String> name = getStore(sn).getDisplayName(user1Id);
assertTrue(name.isPresent());
assertEquals(user1Name, name.get());
}
@Test
public void getSingleThreePidSuccess() {
List<_ThreePid> tpids = getStore(sst).getThreepids(user1Id);
assertEquals(1, tpids.size());
_ThreePid tpid = tpids.get(0);
assertEquals(UserIdType.Email.getId(), tpid.getMedium());
assertEquals(user1Email, tpid.getAddress());
}
@Test
public void getMultiThreePidSuccess() {
List<_ThreePid> tpids = getStore(smt).getThreepids(user1Id);
assertEquals(2, tpids.size());
_ThreePid firstTpid = tpids.get(0);
assertEquals(ThreePidMedium.Email.getId(), firstTpid.getMedium());
assertEquals(user1Email, firstTpid.getAddress());
_ThreePid secondTpid = tpids.get(1);
assertEquals(ThreePidMedium.PhoneNumber.getId(), secondTpid.getMedium());
assertEquals(user1Msisdn, secondTpid.getAddress());
}
}

View File

@@ -0,0 +1,98 @@
/*
* 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._MatrixID;
import io.kamax.matrix.json.GsonUtil;
import io.kamax.mxisd.config.MatrixConfig;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.ProcessResult;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
public class ExecStoreTest {
protected final String sno = "successNoOutput";
protected final String fno = "failureNoOutput";
protected final String uno = "unknownNoOutput";
protected final String domain = "domain.tld";
protected final String userLocalpart = "user";
protected final String user1Localpart = userLocalpart + "1";
protected final String user1Name = "User 1";
protected final String user2Localpart = userLocalpart + "2";
protected final String user2Name = "User 2";
protected final _MatrixID user1Id = MatrixID.asAcceptable(user1Localpart, domain);
protected final String user1Email = user1Localpart + "@" + domain;
protected Map<String, Supplier<ProcessResult>> executables = new HashMap<>();
public ExecStoreTest() {
executables.put(sno, () -> make(0, () -> ""));
executables.put(fno, () -> make(1, () -> ""));
executables.put(uno, () -> make(Integer.MAX_VALUE, () -> ""));
}
protected MatrixConfig getMatrixCfg() {
MatrixConfig mxCfg = new MatrixConfig();
mxCfg.setDomain(domain);
return mxCfg;
}
protected ProcessResult make(int exitCode, Supplier<String> supplier) {
return new ProcessResult(exitCode, null) {
@Override
public String outputUTF8() {
return supplier.get();
}
};
}
protected ProcessResult makeJson(int exitCode, Supplier<Object> supplier) {
return make(exitCode, () -> GsonUtil.get().toJson(supplier.get()));
}
protected ProcessExecutor build() {
return new ProcessExecutor() {
private Function<String, RuntimeException> notFound = command ->
new IllegalArgumentException("Command not found: " + command);
@Override
public ProcessResult execute() {
if (getCommand().size() == 0) throw new IllegalStateException();
String command = getCommand().get(0);
return executables.getOrDefault(command, () -> {
throw notFound.apply(command);
}).get();
}
};
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.auth.input;
import io.kamax.mxisd.backend.exec.ExecAuthStoreTest;
import java.util.Arrays;
public class ExecAuthArgsTest extends ExecAuthStoreTest {
@Override
protected void setValidCommand() {
cfg.getAuth().setCommand("src/test/resources/store/exec/input/argsTest.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));
}
}

View File

@@ -0,0 +1,77 @@
/*
* 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.auth.input;
import io.kamax.mxisd.backend.exec.ExecAuthStoreTest;
import java.util.HashMap;
public class ExecAuthEnvTest 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/input/envTest.sh");
}
@Override
protected void setValidEnv() {
cfg.getAuth().setEnv(new HashMap<>());
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);
}
}

View File

@@ -0,0 +1,93 @@
/*
* 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.auth.input;
import io.kamax.mxisd.backend.exec.ExecAuthStoreTest;
import io.kamax.mxisd.backend.exec.ExecStore;
public class ExecAuthInputMultilinesTest extends ExecAuthStoreTest {
@Override
protected void setValidCommand() {
cfg.getAuth().setCommand("src/test/resources/store/exec/input/multilinesTest.sh");
}
@Override
protected void setValidInput() {
cfg.getAuth().getInput().setType(ExecStore.PlainType);
cfg.getAuth().getInput().setTemplate(null);
}
@Override
protected void setEmptyLocalpartConfig() {
cfg.getAuth().getInput().setTemplate("" + System.lineSeparator()
+ DomainToken + System.lineSeparator()
+ MxidToken + System.lineSeparator()
+ PassToken + System.lineSeparator()
);
}
@Override
public void setWrongLocalpartConfig() {
cfg.getAuth().getInput().setTemplate(LocalpartInvalid + System.lineSeparator()
+ DomainToken + System.lineSeparator()
+ MxidToken + System.lineSeparator()
+ PassToken + System.lineSeparator()
);
}
@Override
protected void setEmptyDomainConfig() {
cfg.getAuth().getInput().setTemplate(LocalpartToken + System.lineSeparator()
+ "" + System.lineSeparator()
+ MxidToken + System.lineSeparator()
+ PassToken + System.lineSeparator()
);
}
@Override
public void setWrongDomainConfig() {
cfg.getAuth().getInput().setTemplate(LocalpartToken + System.lineSeparator()
+ DomainInvalid + System.lineSeparator()
+ MxidToken + System.lineSeparator()
+ PassToken + System.lineSeparator()
);
}
@Override
protected void setEmptyMxidConfig() {
cfg.getAuth().getInput().setTemplate(LocalpartToken + System.lineSeparator()
+ DomainToken + System.lineSeparator()
+ "" + System.lineSeparator()
+ PassToken + System.lineSeparator()
);
}
@Override
public void setWrongMxidConfig() {
cfg.getAuth().getInput().setTemplate(LocalpartToken + System.lineSeparator()
+ DomainToken + System.lineSeparator()
+ MxidInvalid + System.lineSeparator()
+ PassToken + System.lineSeparator()
);
}
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
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

View File

@@ -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 <http://www.gnu.org/licenses/>.
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

View File

@@ -0,0 +1,42 @@
#!/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 <http://www.gnu.org/licenses/>.
if [ -n "$WITH_LOCALPART" ]; then
read LOCALPART
[ -n "$LOCALPART" ] || exit 10
[ "$LOCALPART" = "$REQ_LOCALPART" ] || exit 20
fi
if [ -n "$WITH_DOMAIN" ]; then
read DOMAIN
[ -n "$DOMAIN" ] || exit 11
[ "$DOMAIN" = "$REQ_DOMAIN" ] || exit 21
fi
if [ -n "$WITH_MXID" ]; then
read MXID
[ -n "$MXID" ] || exit 12
[ "$MXID" = "$REQ_MXID" ] || exit 22
fi
read PASS
[ "$PASS" = "$REQ_PASS" ] || exit 1
exit 0