Compare commits
	
		
			5 Commits
		
	
	
		
			v1.2.0-bet
			...
			v1.2.0-rc.
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 99b7d9f27d | ||
|  | ded5e3db5e | ||
|  | b892d19023 | ||
|  | 026a2e82d9 | ||
|  | b881f73798 | 
							
								
								
									
										18
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										18
									
								
								build.gradle
									
									
									
									
									
								
							| @@ -41,17 +41,25 @@ def debBuildDataPath = "${debBuildBasePath}${debDataPath}" | ||||
| def debBuildSystemdPath = "${debBuildBasePath}${debSystemdPath}" | ||||
|  | ||||
| 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() { | ||||
|     def versionPattern = Pattern.compile("v(\\d+\\.)?(\\d+\\.)?(\\d+)(-.*)?") | ||||
|     ByteArrayOutputStream out = new ByteArrayOutputStream() | ||||
|     exec { | ||||
|         commandLine = ['git', 'describe', '--tags', '--always', '--dirty'] | ||||
|         standardOutput = out | ||||
|     } | ||||
|     def v = out.toString().replace(System.lineSeparator(), '') | ||||
|     return versionPattern.matcher(v).matches() ? v.substring(1) : v | ||||
|     return out.toString().replace(System.lineSeparator(), ''); | ||||
| } | ||||
|  | ||||
| buildscript { | ||||
| @@ -154,7 +162,7 @@ processResources { | ||||
|  | ||||
| task buildDeb(dependsOn: build) { | ||||
|     doLast { | ||||
|         def v = gitVersion() | ||||
|         def v = mxisdVersion() | ||||
|         println "Version for package: ${v}" | ||||
|         mkdir distDir | ||||
|         mkdir debBuildBasePath | ||||
|   | ||||
| @@ -1,60 +1,470 @@ | ||||
| # Exec Identity Store | ||||
| This Identity Store lets you run arbitrary commands to handle the various requests in each support feature. | ||||
|  | ||||
| This is the most versatile Identity store of mxisd, allowing you to connect any kind of logic in any language/scripting. | ||||
| - [Features](#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 | Yes           | | ||||
| | Directory      | *In Progress* | | ||||
| | Identity       | *In Progress* | | ||||
| | Profile        | *In Progress* | | ||||
| |                       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 | ||||
| - Command line arguments | ||||
| - Environment variables | ||||
| - [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 [Exit status](https://en.wikipedia.org/wiki/Exit_status) and Standard Output, both | ||||
| supporting a set of options.  | ||||
| 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 enabled/disabled specifically. | ||||
| Enable/disable the Identity store at a global/default level. Each feature can still be individually enabled/disabled. | ||||
|  | ||||
| *TBC* | ||||
| #### 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: | ||||
|  | ||||
| ## Use-case examples | ||||
| ```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.auth.command: '/path/to/auth/executable' | ||||
| exec.auth.args: ['-u', '{localpart}'] | ||||
| exec.auth.env: | ||||
|   PASSWORD: '{password}' | ||||
| 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: '{mxid}' | ||||
|   MATRIX_USER_ID: '{matrixId}' | ||||
|    | ||||
| exec.placeholder.output.type: 'json' | ||||
| exec.placeholder.exit.success: [0, 128] | ||||
| exec.placeholder.exit.failure: [1, 129] | ||||
| ``` | ||||
| This will run `/path/to/auth/executable` with: | ||||
| - The extracted Matrix User ID `localpart` provided as the second command line argument, the first one being `-u`  | ||||
| - The password, the extract Matrix `domain` and the full User ID as arbitrary environment variables, respectively `PASSWORD`, `MATRIX_DOMAIN` and `MATRIX_USER_ID` | ||||
| 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 | ||||
| ## Few more available config items | ||||
| # | ||||
| # exec.token.domain: '{matrixDomain}' # This sets the default replacement token for the Matrix Domain of the User ID, across all features. | ||||
| # exec.auth.token.domain: '{matrixDomainForAuth}' # We can also set another token specific to a feature. | ||||
| # exec.auth.input: 'json' # This is not supported yet. | ||||
| # exec.auth.exit.success: [0] # Exit status that will consider the request successful. This is already the default. | ||||
| # exec.auth.exit.failure: [1,2,3] # Exist status that will consider the request failed. Anything else than success or failure statuses will throw an exception. | ||||
| # exec.auth.output: 'json' # Required if stdout should be read on success. This uses the same output as the REST Identity store for Auth. | ||||
| exec.auth.enabled: <true/false> | ||||
| ``` | ||||
| *TBC* | ||||
|  | ||||
| --- | ||||
|  | ||||
| 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. | ||||
|   | ||||
| @@ -27,27 +27,22 @@ 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.io.IOUtils; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.beans.factory.annotation.Autowired; | ||||
| import org.springframework.stereotype.Component; | ||||
| import org.zeroturnaround.exec.ProcessExecutor; | ||||
| import org.zeroturnaround.exec.ProcessResult; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.*; | ||||
| import java.util.concurrent.TimeoutException; | ||||
| import java.util.stream.Collectors; | ||||
| import java.util.Objects; | ||||
| import java.util.Optional; | ||||
|  | ||||
| @Component | ||||
| public class ExecAuthStore extends ExecStore implements AuthenticatorProvider { | ||||
|  | ||||
|     private final transient Logger log = LoggerFactory.getLogger(ExecAuthStore.class); | ||||
|     private final Logger log = LoggerFactory.getLogger(ExecAuthStore.class); | ||||
|  | ||||
|     private ExecConfig.Auth cfg; | ||||
|  | ||||
| @@ -71,71 +66,64 @@ public class ExecAuthStore extends ExecStore implements AuthenticatorProvider { | ||||
|         ExecAuthResult result = new ExecAuthResult(); | ||||
|         result.setId(new UserID(UserIdType.Localpart, uId.getLocalPart())); | ||||
|  | ||||
|         ProcessExecutor psExec = new ProcessExecutor().readOutput(true); | ||||
|         Processor<ExecAuthResult> p = new Processor<>(cfg); | ||||
|  | ||||
|         List<String> args = new ArrayList<>(); | ||||
|         args.add(cfg.getCommand()); | ||||
|         args.addAll(cfg.getArgs().stream().map(arg -> arg | ||||
|                 .replace(cfg.getToken().getLocalpart(), uId.getLocalPart()) | ||||
|                 .replace(cfg.getToken().getDomain(), uId.getDomain()) | ||||
|                 .replace(cfg.getToken().getMxid(), uId.getId()) | ||||
|                 .replace(cfg.getToken().getPassword(), password) | ||||
|         ).collect(Collectors.toList())); | ||||
|         psExec.command(args); | ||||
|         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); | ||||
|  | ||||
|         psExec.environment(new HashMap<>(cfg.getEnv()).entrySet().stream().peek(e -> { | ||||
|             e.setValue(e.getValue().replace(cfg.getToken().getLocalpart(), uId.getLocalPart())); | ||||
|             e.setValue(e.getValue().replace(cfg.getToken().getDomain(), uId.getDomain())); | ||||
|             e.setValue(e.getValue().replace(cfg.getToken().getMxid(), uId.getId())); | ||||
|             e.setValue(e.getValue().replace(cfg.getToken().getPassword(), password)); | ||||
|         }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); | ||||
|         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() | ||||
|         ); | ||||
|  | ||||
|         if (StringUtils.isNotBlank(cfg.getInput())) { | ||||
|             if (StringUtils.equals("json", cfg.getInput())) { | ||||
|                 JsonObject input = new JsonObject(); | ||||
|                 input.addProperty("localpart", uId.getLocalPart()); | ||||
|                 input.addProperty("mxid", uId.getId()); | ||||
|                 input.addProperty("password", password); | ||||
|                 psExec.redirectInput(IOUtils.toInputStream(GsonUtil.get().toJson(input), StandardCharsets.UTF_8)); | ||||
|             } else { | ||||
|                 throw new InternalServerError(cfg.getInput() + " is not a valid executable input format"); | ||||
|         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 + ")"); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         try { | ||||
|             log.info("Executing {}", cfg.getCommand()); | ||||
|             ProcessResult psResult = psExec.execute(); | ||||
|             result.setExitStatus(psResult.getExitValue()); | ||||
|             String output = psResult.outputUTF8(); | ||||
|             result.setSuccess(Optional.ofNullable(StringUtils.isEmpty(lines[0]) ? null : lines[0]) | ||||
|                     .map(v -> StringUtils.equalsAnyIgnoreCase(v, "true", "1")) | ||||
|                     .orElse(result.isSuccess())); | ||||
|  | ||||
|             log.info("Exit status: {}", result.getExitStatus()); | ||||
|             if (cfg.getExit().getSuccess().contains(result.getExitStatus())) { | ||||
|                 result.setSuccess(true); | ||||
|                 if (result.isSuccess()) { | ||||
|                     if (StringUtils.equals("json", cfg.getOutput())) { | ||||
|                         JsonObject data = GsonUtil.parseObj(output); | ||||
|                         GsonUtil.findPrimitive(data, "success") | ||||
|                                 .map(JsonPrimitive::getAsBoolean) | ||||
|                                 .ifPresent(result::setSuccess); | ||||
|                         GsonUtil.findObj(data, "profile") | ||||
|                                 .flatMap(p -> GsonUtil.findString(p, "display_name")) | ||||
|                                 .ifPresent(v -> result.getProfile().setDisplayName(v)); | ||||
|                     } else { | ||||
|                         log.debug("Command output:{}{}", "\n", output); | ||||
|                     } | ||||
|                 } | ||||
|             } else if (cfg.getExit().getFailure().contains(result.getExitStatus())) { | ||||
|                 log.debug("{} stdout:{}{}", cfg.getCommand(), "\n", output); | ||||
|                 result.setSuccess(false); | ||||
|             } else { | ||||
|                 log.error("{} stdout:{}{}", cfg.getCommand(), "\n", output); | ||||
|                 throw new InternalServerError("Exec auth command returned with unexpected exit status"); | ||||
|             if (lines.length == 2) { | ||||
|                 Optional.ofNullable(StringUtils.isEmpty(lines[1]) ? null : lines[1]) | ||||
|                         .ifPresent(v -> result.getProfile().setDisplayName(v)); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         } catch (IOException | InterruptedException | TimeoutException e) { | ||||
|             throw new InternalServerError(e); | ||||
|         } | ||||
|         }); | ||||
|  | ||||
|         p.withFailureHandler(pr -> result.setSuccess(false)); | ||||
|         p.withFailureDefault(o -> result); | ||||
|  | ||||
|         return p.execute(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,27 +20,75 @@ | ||||
|  | ||||
| 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 io.kamax.mxisd.exception.NotImplementedException; | ||||
| 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 false; | ||||
|         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) { | ||||
|         throw new NotImplementedException(this.getClass().getName()); | ||||
|         return search(cfg.getSearch().getByName(), new UserDirectorySearchRequest("name", query)); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public UserDirectorySearchResult searchBy3pid(String query) { | ||||
|         throw new NotImplementedException(this.getClass().getName()); | ||||
|         return search(cfg.getSearch().getByName(), new UserDirectorySearchRequest("threepid", query)); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,22 +20,56 @@ | ||||
|  | ||||
| package io.kamax.mxisd.backend.exec; | ||||
|  | ||||
| import io.kamax.mxisd.exception.NotImplementedException; | ||||
| 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 false; | ||||
|         return cfg.isEnabled(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
| @@ -45,17 +79,131 @@ public class ExecIdentityStore extends ExecStore implements IThreePidProvider { | ||||
|  | ||||
|     @Override | ||||
|     public int getPriority() { | ||||
|         return 0; | ||||
|         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) { | ||||
|         throw new NotImplementedException(this.getClass().getName()); | ||||
|         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) { | ||||
|         throw new NotImplementedException(this.getClass().getName()); | ||||
|         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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -22,34 +22,82 @@ package io.kamax.mxisd.backend.exec; | ||||
|  | ||||
| import io.kamax.matrix._MatrixID; | ||||
| import io.kamax.matrix._ThreePid; | ||||
| import io.kamax.mxisd.exception.NotImplementedException; | ||||
| import io.kamax.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 false; | ||||
|         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) { | ||||
|         throw new NotImplementedException(this.getClass().getName()); | ||||
|         return getFull(userId, cfg.getDisplayName()).map(JsonProfileResult::getDisplayName); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<_ThreePid> getThreepids(_MatrixID userId) { | ||||
|         throw new NotImplementedException(this.getClass().getName()); | ||||
|         return getFull(userId, cfg.getThreePid()) | ||||
|                 .map(p -> Collections.<_ThreePid>unmodifiableList(p.getThreepids())) | ||||
|                 .orElseGet(Collections::emptyList); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public List<String> getRoles(_MatrixID userId) { | ||||
|         throw new NotImplementedException(this.getClass().getName()); | ||||
|         return getFull(userId, cfg.getRole()) | ||||
|                 .map(JsonProfileResult::getRoles) | ||||
|                 .orElseGet(Collections::emptyList); | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,8 +20,233 @@ | ||||
|  | ||||
| package io.kamax.mxisd.backend.exec; | ||||
|  | ||||
| public abstract class ExecStore { | ||||
| 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; | ||||
|  | ||||
|     // no-op | ||||
| 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); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -20,6 +20,7 @@ | ||||
|  | ||||
| package io.kamax.mxisd.backend.rest; | ||||
|  | ||||
| import io.kamax.matrix.ThreePidMedium; | ||||
| import io.kamax.mxisd.UserID; | ||||
|  | ||||
| public class LookupSingleResponseJson { | ||||
| @@ -32,12 +33,28 @@ public class LookupSingleResponseJson { | ||||
|         return medium; | ||||
|     } | ||||
|  | ||||
|     public void setMedium(String medium) { | ||||
|         this.medium = medium; | ||||
|     } | ||||
|  | ||||
|     public void setMedium(ThreePidMedium medium) { | ||||
|         setMedium(medium.getId()); | ||||
|     } | ||||
|  | ||||
|     public String getAddress() { | ||||
|         return address; | ||||
|     } | ||||
|  | ||||
|     public void setAddress(String address) { | ||||
|         this.address = address; | ||||
|     } | ||||
|  | ||||
|     public UserID getId() { | ||||
|         return id; | ||||
|     } | ||||
|  | ||||
|     public void setId(UserID id) { | ||||
|         this.id = id; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -31,6 +31,29 @@ import java.util.*; | ||||
| @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); | ||||
| @@ -60,6 +83,10 @@ public class ExecConfig { | ||||
|         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()); | ||||
| @@ -93,6 +120,38 @@ public class ExecConfig { | ||||
|             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 { | ||||
| @@ -101,6 +160,10 @@ public class ExecConfig { | ||||
|         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; | ||||
| @@ -134,6 +197,38 @@ public class ExecConfig { | ||||
|             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 { | ||||
| @@ -143,10 +238,10 @@ public class ExecConfig { | ||||
|  | ||||
|         private List<String> args = new ArrayList<>(); | ||||
|         private Map<String, String> env = new HashMap<>(); | ||||
|         private String input; | ||||
|         private IO input = new IO(); | ||||
|  | ||||
|         private Exit exit = new Exit(); | ||||
|         private String output; | ||||
|         private IO output = new IO(); | ||||
|  | ||||
|         public TokenOverride getToken() { | ||||
|             return token; | ||||
| @@ -184,11 +279,11 @@ public class ExecConfig { | ||||
|             this.env.put(key, value); | ||||
|         } | ||||
|  | ||||
|         public String getInput() { | ||||
|         public IO getInput() { | ||||
|             return input; | ||||
|         } | ||||
|  | ||||
|         public void setInput(String input) { | ||||
|         public void setInput(IO input) { | ||||
|             this.input = input; | ||||
|         } | ||||
|  | ||||
| @@ -200,11 +295,11 @@ public class ExecConfig { | ||||
|             this.exit = exit; | ||||
|         } | ||||
|  | ||||
|         public String getOutput() { | ||||
|         public IO getOutput() { | ||||
|             return output; | ||||
|         } | ||||
|  | ||||
|         public void setOutput(String output) { | ||||
|         public void setOutput(IO output) { | ||||
|             this.output = output; | ||||
|         } | ||||
|  | ||||
| @@ -224,9 +319,33 @@ public class ExecConfig { | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Directory extends Process { | ||||
|     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; | ||||
| @@ -236,25 +355,44 @@ public class ExecConfig { | ||||
|             this.enabled = enabled; | ||||
|         } | ||||
|  | ||||
|         public Search getSearch() { | ||||
|             return search; | ||||
|         } | ||||
|  | ||||
|         public void setSearch(Search search) { | ||||
|             this.search = search; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Identity extends Process { | ||||
|     public class Lookup { | ||||
|  | ||||
|         private Boolean enabled; | ||||
|         private Process single = new Process(); | ||||
|         private Process bulk = new Process(); | ||||
|  | ||||
|         public Boolean isEnabled() { | ||||
|             return enabled; | ||||
|         public Process getSingle() { | ||||
|             return single; | ||||
|         } | ||||
|  | ||||
|         public void setEnabled(Boolean enabled) { | ||||
|             this.enabled = enabled; | ||||
|         public void setSingle(Process single) { | ||||
|             this.single = single; | ||||
|         } | ||||
|  | ||||
|         public Process getBulk() { | ||||
|             return bulk; | ||||
|         } | ||||
|  | ||||
|         public void setBulk(Process bulk) { | ||||
|             this.bulk = bulk; | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
|     public class Profile extends Process { | ||||
|     public class Identity { | ||||
|  | ||||
|         private Boolean enabled; | ||||
|         private int priority; | ||||
|         private Lookup lookup = new Lookup(); | ||||
|  | ||||
|         public Boolean isEnabled() { | ||||
|             return enabled; | ||||
| @@ -264,6 +402,63 @@ public class ExecConfig { | ||||
|             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; | ||||
| @@ -322,7 +517,7 @@ public class ExecConfig { | ||||
|     } | ||||
|  | ||||
|     @PostConstruct | ||||
|     public void build() { | ||||
|     public ExecConfig compute() { | ||||
|         if (Objects.isNull(getAuth().isEnabled())) { | ||||
|             getAuth().setEnabled(isEnabled()); | ||||
|         } | ||||
| @@ -338,6 +533,8 @@ public class ExecConfig { | ||||
|         if (Objects.isNull(getProfile().isEnabled())) { | ||||
|             getProfile().setEnabled(isEnabled()); | ||||
|         } | ||||
|  | ||||
|         return this; | ||||
|     } | ||||
|  | ||||
| } | ||||
|   | ||||
| @@ -29,6 +29,11 @@ public class UserDirectorySearchRequest { | ||||
|         setSearchTerm(searchTerm); | ||||
|     } | ||||
|  | ||||
|     public UserDirectorySearchRequest(String type, String searchTerm) { | ||||
|         setBy(type); | ||||
|         setSearchTerm(searchTerm); | ||||
|     } | ||||
|  | ||||
|     public String getBy() { | ||||
|         return by; | ||||
|     } | ||||
|   | ||||
| @@ -25,6 +25,10 @@ import java.util.Set; | ||||
|  | ||||
| public class UserDirectorySearchResult { | ||||
|  | ||||
|     public static UserDirectorySearchResult empty() { | ||||
|         return new UserDirectorySearchResult(); | ||||
|     } | ||||
|  | ||||
|     public static class Result { | ||||
|  | ||||
|         private String displayName; | ||||
|   | ||||
							
								
								
									
										67
									
								
								src/main/java/io/kamax/mxisd/profile/JsonProfileRequest.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/main/java/io/kamax/mxisd/profile/JsonProfileRequest.java
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										69
									
								
								src/main/java/io/kamax/mxisd/profile/JsonProfileResult.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/main/java/io/kamax/mxisd/profile/JsonProfileResult.java
									
									
									
									
									
										Normal 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); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										28
									
								
								src/main/java/io/kamax/mxisd/util/TriFunction.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/main/java/io/kamax/mxisd/util/TriFunction.java
									
									
									
									
									
										Normal 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); | ||||
|  | ||||
| } | ||||
| @@ -25,6 +25,7 @@ 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; | ||||
| @@ -61,20 +62,23 @@ public abstract class ExecAuthStoreTest { | ||||
|         // 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)); | ||||
|     } | ||||
|  | ||||
|     protected void setValidConfig() { | ||||
|     @Before | ||||
|     public void setValidConfig() { | ||||
|         setValidCommand(); | ||||
|         setValidEnv(); | ||||
|         setValidArgs(); | ||||
|         setValidInput(); | ||||
|         setValidExit(); | ||||
|     } | ||||
|  | ||||
|     public ExecAuthStoreTest() { | ||||
|         cfg = new ExecConfig(); | ||||
|         cfg.getAuth().addEnv("WITH_LOCALPART", "1"); | ||||
|         cfg.getAuth().addEnv("REQ_LOCALPART", uId.getLocalPart()); | ||||
|         cfg.getAuth().addEnv("WITH_DOMAIN", "1"); | ||||
| @@ -82,9 +86,10 @@ public abstract class ExecAuthStoreTest { | ||||
|         cfg.getAuth().addEnv("WITH_MXID", "1"); | ||||
|         cfg.getAuth().addEnv("REQ_MXID", uId.getId()); | ||||
|         cfg.getAuth().addEnv("REQ_PASS", requiredPass); | ||||
|     } | ||||
|  | ||||
|         setValidConfig(); | ||||
|  | ||||
|     public ExecAuthStoreTest() { | ||||
|         cfg = new ExecConfig(); | ||||
|         p = new ExecAuthStore(cfg); | ||||
|     } | ||||
|  | ||||
| @@ -119,7 +124,7 @@ public abstract class ExecAuthStoreTest { | ||||
|     protected abstract void setEmptyLocalpartConfig(); | ||||
|  | ||||
|     @Test | ||||
|     public void doEmptyLocalpartConfig() { | ||||
|     public void emptyLocalpartConfig() { | ||||
|         setEmptyLocalpartConfig(); | ||||
|  | ||||
|         ExecAuthResult res = p.authenticate(uId, requiredPass); | ||||
|   | ||||
| @@ -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"); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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()); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										98
									
								
								src/test/java/io/kamax/mxisd/backend/exec/ExecStoreTest.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								src/test/java/io/kamax/mxisd/backend/exec/ExecStoreTest.java
									
									
									
									
									
										Normal 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(); | ||||
|             } | ||||
|  | ||||
|         }; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -18,15 +18,17 @@ | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| package io.kamax.mxisd.backend.exec; | ||||
| package io.kamax.mxisd.backend.exec.auth.input; | ||||
| 
 | ||||
| import io.kamax.mxisd.backend.exec.ExecAuthStoreTest; | ||||
| 
 | ||||
| import java.util.Arrays; | ||||
| 
 | ||||
| public class ExecAuthStoreArgsTest extends ExecAuthStoreTest { | ||||
| public class ExecAuthArgsTest extends ExecAuthStoreTest { | ||||
| 
 | ||||
|     @Override | ||||
|     protected void setValidCommand() { | ||||
|         cfg.getAuth().setCommand("src/test/resources/store/exec/authArgsTest.sh"); | ||||
|         cfg.getAuth().setCommand("src/test/resources/store/exec/input/argsTest.sh"); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @@ -18,9 +18,13 @@ | ||||
|  * along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  */ | ||||
| 
 | ||||
| package io.kamax.mxisd.backend.exec; | ||||
| package io.kamax.mxisd.backend.exec.auth.input; | ||||
| 
 | ||||
| public class ExecAuthStoreEnvTest extends ExecAuthStoreTest { | ||||
| 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"; | ||||
| @@ -28,11 +32,12 @@ public class ExecAuthStoreEnvTest extends ExecAuthStoreTest { | ||||
| 
 | ||||
|     @Override | ||||
|     protected void setValidCommand() { | ||||
|         cfg.getAuth().setCommand("src/test/resources/store/exec/authEnvTest.sh"); | ||||
|         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); | ||||
| @@ -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() | ||||
|         ); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										42
									
								
								src/test/resources/store/exec/input/multilinesTest.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										42
									
								
								src/test/resources/store/exec/input/multilinesTest.sh
									
									
									
									
									
										Executable 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 | ||||
		Reference in New Issue
	
	Block a user