Compare commits

...

24 Commits
2.4.0 ... 2.5.0

Author SHA1 Message Date
Anatoliy Sablin
ae5864cd91 Bump dependencies. 2021-04-16 21:48:19 +03:00
Anatoliy Sablin
e456724caf Bump gradle to 7.0. Replace jcenter with mavenCentral and gradle plugin portal. 2021-04-16 20:58:32 +03:00
Anatoliy Sablin
ed9dcc4061 Respond with application/json for the register submitToken. 2021-02-04 21:10:25 +03:00
Anatoliy Sablin
ea8e386939 Add internal API to manually invoke invitation manager. 2021-01-25 22:45:18 +03:00
Anatoliy Sablin
e0ec887118 Add config print full display name of the invited person. 2021-01-17 20:06:09 +03:00
Anatoliy Sablin
a71d32ba77 Add config option to specify period dimension of the invitation scheduler. 2021-01-13 22:09:30 +03:00
Anatoliy Sablin
a0f6fe9b0d Add forgotten M_TERMS_NOT_SIGNED error message. 2021-01-13 21:41:21 +03:00
ma1uta
c25647156a Merge pull request #77 from mrjohnson22/master
#76 Set a message for error responses
2020-12-20 11:14:20 +00:00
Xavier Johnson
e7c4c12a98 #76 Set a message for error responses
Without one, clients might treat errors as generic failures instead of
handling them in a manner appropriate for their error code
2020-12-18 23:00:06 -05:00
Anatoliy Sablin
90b2b5301c #68 Mark thusted_third_party_id_servers synapse parameter as deprecated. 2020-12-07 20:39:47 +03:00
Anatoliy Sablin
0d93a26e6d #65 Encode query parameters in the validation link. 2020-12-07 20:32:59 +03:00
ma1uta
c29fc0f0eb Merge pull request #71 from q-wertz/master
Force MatrixID to be lowercase
2020-11-29 08:22:45 +00:00
Clemens Sonnleitner
e421c851c9 Force MatrixID to be lowercase 2020-11-27 13:08:45 +01:00
ma1uta
5b2b45233a Merge pull request #67 from Higgs1/master
Add support for unix sockets.
2020-11-05 05:56:26 +00:00
Lexxy Fox
888f7a4209 Added support for unix sockets. 2020-11-04 08:21:35 -08:00
Anatoliy Sablin
0c301a49c7 Change column type to text for postgresql. 2020-10-26 23:26:15 +03:00
ma1uta
1fda2dd3b7 Create codeql-analysis.yml 2020-10-01 14:51:31 +03:00
ma1uta
c4a20efe5e Merge pull request #58 from nE0sIghT/wip/multidomain
Support for Active Directory multidomain forest
2020-09-19 13:14:19 +00:00
ma1uta
fc45f1b090 Merge pull request #55 from mattcen/docker-build-cleanup
Docker build cleanup
2020-09-19 13:13:16 +00:00
Matt Cengia
1480507d76 Only build .jar on current build platform
Because the .jar is platform-independent, we don't need to build it for every architecture.
2020-09-18 12:45:33 +10:00
Matt Cengia
a1ab1e8e0a Tidy Gradle dockerBuildX target
Build linux/amd64, linux/arm64, and linux/arm/v7 (arm 32-bit) targets
all at once.
This still requires Gradle and JDK on the local machine (i.e. outside of
the Docker build container), because it seems pointless to build the
.jar 3 times, once for each architecture, but tags all the images with
the same tag, rather than using a tag for each architecture. This allows
clients to all use the same image tag, but pull down the architecture
that's right for them.

I'd like to have it build the .jar file in a container (so the host
doesn't need JDK and Gradle), but couldn't think how to do that
efficiently (i.e. only once), with this approach.
2020-09-17 10:54:29 +10:00
Matt Cengia
1e5033b461 Don't require Gradle to build Docker image
Update Dockerfile to use a multi-stage build and run Gradle *within* the
builder container, rather than on the host, so the host needn't have
Gradle or JDK installed, then copy the build .jar from the builder
container into the final container.

Update build.gradle's dockerBuild target to not build the jar, but
instead do it within the Docker build process. This results in some
double-handling because it requires Gradle and JDK both on the host
system *and* within the container, and runs two instances of Gradle
during the build, but is added for backwards compatibility. The better
approach (rather than running `./gradlew dockerBuild`) is to manually
run `docker build -t ma1ua/ma1sd .`.

I've tested this process on both a Debian amd64 system and a Raspberry
Pi 3 running Raspbian, and it seems to work (though the RPi is pretty
slow).
2020-09-17 10:54:29 +10:00
Yuri Konotopov
7323851c6e Support for Active Directory multidomain forest
In AD forest samAccountName (or uid) may not be unique in the
entire forest and userPrincipalName contains "@" symbol
disallowed in Matrix User Identifiers.

This commit reflects changes in ldap_auth_provider that adds
mxid generation logic for Active Directory.

Signed-off-by: Yuri Konotopov <ykonotopov@gnome.org>
2020-08-28 15:33:10 +04:00
Anatoliy Sablin
08db73e55b Escape special characters in the LDAP query string. 2020-08-02 16:05:54 +03:00
24 changed files with 481 additions and 94 deletions

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 3 * * 6'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['java']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,3 +1,11 @@
FROM --platform=$BUILDPLATFORM openjdk:8-jre-alpine AS builder
RUN apk update && apk add gradle git && rm -rf /var/lib/apk/* /var/cache/apk/*
WORKDIR /ma1sd
COPY . .
RUN ./gradlew shadowJar
FROM openjdk:8-jre-alpine
RUN apk update && apk add bash && rm -rf /var/lib/apk/* /var/cache/apk/*
@@ -15,4 +23,4 @@ CMD [ "/start.sh" ]
ADD src/docker/start.sh /start.sh
ADD src/script/ma1sd /app/ma1sd
ADD build/libs/ma1sd.jar /app/ma1sd.jar
COPY --from=builder /ma1sd/build/libs/ma1sd.jar /app/ma1sd.jar

View File

@@ -20,7 +20,7 @@
import java.util.regex.Pattern
apply plugin: 'java'
apply plugin: 'java-library'
apply plugin: 'application'
apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'idea'
@@ -73,90 +73,94 @@ String gitVersion() {
buildscript {
repositories {
jcenter()
gradlePluginPortal()
mavenCentral()
}
dependencies {
classpath 'com.github.jengelman.gradle.plugins:shadow:5.1.0'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
classpath 'com.github.jengelman.gradle.plugins:shadow:6.1.0'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.38.0'
}
}
repositories {
jcenter()
mavenCentral()
}
dependencies {
// Logging
compile 'org.slf4j:slf4j-simple:1.7.25'
api 'org.slf4j:slf4j-simple:1.7.25'
// Easy file management
compile 'commons-io:commons-io:2.6'
api 'commons-io:commons-io:2.8.0'
// Config management
compile 'org.yaml:snakeyaml:1.25'
api 'org.yaml:snakeyaml:1.28'
// Dependencies from old Matrix-java-sdk
compile 'org.apache.commons:commons-lang3:3.9'
compile 'com.squareup.okhttp3:okhttp:4.2.2'
compile 'commons-codec:commons-codec:1.13'
api 'org.apache.commons:commons-lang3:3.12.0'
api 'com.squareup.okhttp3:okhttp:4.2.2'
api 'commons-codec:commons-codec:1.15'
// ORMLite
compile 'com.j256.ormlite:ormlite-jdbc:5.1'
api 'com.j256.ormlite:ormlite-jdbc:5.3'
// ed25519 handling
compile 'net.i2p.crypto:eddsa:0.3.0'
api 'net.i2p.crypto:eddsa:0.3.0'
// LDAP connector
compile 'org.apache.directory.api:api-all:1.0.3'
api 'org.apache.directory.api:api-all:1.0.3'
// DNS lookups
compile 'dnsjava:dnsjava:2.1.9'
api 'dnsjava:dnsjava:2.1.9'
// HTTP connections
compile 'org.apache.httpcomponents:httpclient:4.5.10'
api 'org.apache.httpcomponents:httpclient:4.5.13'
// Phone numbers validation
compile 'com.googlecode.libphonenumber:libphonenumber:8.10.22'
api 'com.googlecode.libphonenumber:libphonenumber:8.12.21'
// E-mail sending
compile 'javax.mail:javax.mail-api:1.6.2'
compile 'com.sun.mail:javax.mail:1.6.2'
api 'javax.mail:javax.mail-api:1.6.2'
api 'com.sun.mail:javax.mail:1.6.2'
// Google Firebase Authentication backend
compile 'com.google.firebase:firebase-admin:5.3.0'
api 'com.google.firebase:firebase-admin:5.3.0'
// Connection Pool
compile 'com.mchange:c3p0:0.9.5.4'
api 'com.mchange:c3p0:0.9.5.5'
// SQLite
compile 'org.xerial:sqlite-jdbc:3.28.0'
api 'org.xerial:sqlite-jdbc:3.34.0'
// PostgreSQL
compile 'org.postgresql:postgresql:42.2.8'
api 'org.postgresql:postgresql:42.2.19'
// MariaDB/MySQL
compile 'org.mariadb.jdbc:mariadb-java-client:2.5.1'
api 'org.mariadb.jdbc:mariadb-java-client:2.7.2'
// UNIX sockets
api 'com.kohlschutter.junixsocket:junixsocket-core:2.3.3'
// Twilio SDK for SMS
compile 'com.twilio.sdk:twilio:7.45.0'
api 'com.twilio.sdk:twilio:7.45.0'
// SendGrid SDK to send emails from GCE
compile 'com.sendgrid:sendgrid-java:2.2.2'
api 'com.sendgrid:sendgrid-java:2.2.2'
// ZT-Exec for exec identity store
compile 'org.zeroturnaround:zt-exec:1.11'
api 'org.zeroturnaround:zt-exec:1.12'
// HTTP server
compile 'io.undertow:undertow-core:2.0.27.Final'
api 'io.undertow:undertow-core:2.2.7.Final'
// Command parser for AS interface
implementation 'commons-cli:commons-cli:1.4'
api 'commons-cli:commons-cli:1.4'
testCompile 'junit:junit:4.13-rc-1'
testCompile 'com.github.tomakehurst:wiremock:2.25.1'
testCompile 'com.unboundid:unboundid-ldapsdk:4.0.12'
testCompile 'com.icegreen:greenmail:1.5.11'
testImplementation 'junit:junit:4.13.2'
testImplementation 'com.github.tomakehurst:wiremock:2.27.2'
testImplementation 'com.unboundid:unboundid-ldapsdk:4.0.12'
testImplementation 'com.icegreen:greenmail:1.5.11'
}
jar {
@@ -239,7 +243,7 @@ task debBuild(dependsOn: shadowJar) {
ant.chmod(
file: "${debBuildDebianPath}/postinst",
perm: 'a+x'
perm: '0755'
)
ant.chmod(
@@ -264,7 +268,7 @@ task debBuild(dependsOn: shadowJar) {
}
}
task dockerBuild(type: Exec, dependsOn: shadowJar) {
task dockerBuild(type: Exec) {
commandLine 'docker', 'build', '-t', dockerImageTag, project.rootDir
doLast {
@@ -275,22 +279,10 @@ task dockerBuild(type: Exec, dependsOn: shadowJar) {
}
task dockerBuildX(type: Exec, dependsOn: shadowJar) {
commandLine 'docker', 'buildx', 'build', '--load', '--platform', 'linux/arm64', '-t', dockerImageTag + '-arm64', project.rootDir
commandLine 'docker', 'buildx', 'build', '--push', '--platform', 'linux/arm64,linux/amd64,linux/arm/v7', '-t', dockerImageTag , project.rootDir
doLast {
exec {
commandLine 'docker', 'buildx', 'build', '--load', '--platform', 'linux/amd64', '-t', dockerImageTag + '-amd64', project.rootDir
}
exec {
commandLine 'docker', 'tag', dockerImageTag + '-arm64', "${dockerImageName}:latest-arm64-dev"
}
exec {
commandLine 'docker', 'tag', dockerImageTag + '-amd64', "${dockerImageName}:latest-amd64-dev"
}
exec {
commandLine 'docker', 'tag', dockerImageTag + '-amd64', "${dockerImageName}:latest-dev"
commandLine 'docker', 'buildx', 'build', '--push', '--platform', 'linux/arm64,linux/amd64,linux/arm/v7', '-t', "${dockerImageName}:latest-dev", project.rootDir
}
}
}

View File

@@ -121,15 +121,13 @@ server {
}
```
### Synapse
### Synapse (Deprecated with synapse v1.4.0)
Add your ma1sd domain into the `homeserver.yaml` at `trusted_third_party_id_servers` and restart synapse.
In a typical configuration, you would end up with something similar to:
```yaml
trusted_third_party_id_servers:
- matrix.example.org
```
It is **highly recommended** to remove `matrix.org` and `vector.im` (or any other default entry) from your configuration
so only your own Identity server is authoritative for your HS.
## Validate (Under reconstruction)
**NOTE:** In case your homeserver has no working federation, step 5 will not happen. If step 4 took place, consider

View File

@@ -1,5 +1,5 @@
#Thu Dec 05 22:39:36 MSK 2019
distributionUrl=https\://services.gradle.org/distributions/gradle-6.0-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStorePath=wrapper/dists

View File

@@ -165,6 +165,8 @@ threepid:
# ldap:
# enabled: true
# lookup: true # hash lookup
# activeDirectory: false
# defaultDomain: ''
# connection:
# host: 'ldap.domain.tld'
# port: 389
@@ -202,3 +204,16 @@ threepid:
# root: error # default level for all loggers (apps and thirdparty libraries)
# app: info # log level only for the ma1sd
# requests: false # or true to dump full requests and responses
# Config invitation manager
#invite:
# fullDisplayName: true # print full name of the invited user (default false)
# resolution:
# timer: 10
# period: seconds # search invites every 10 seconds (by default 5 minutes)
# Internal API
#internal:
# enabled: true # default to false

View File

@@ -58,6 +58,7 @@ import io.kamax.mxisd.http.undertow.handler.identity.v1.BulkLookupHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v1.SingleLookupHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v2.HashDetailsHandler;
import io.kamax.mxisd.http.undertow.handler.identity.v2.HashLookupHandler;
import io.kamax.mxisd.http.undertow.handler.internal.InternalInviteManagerHandler;
import io.kamax.mxisd.http.undertow.handler.invite.v1.RoomInviteHandler;
import io.kamax.mxisd.http.undertow.handler.profile.v1.InternalProfileHandler;
import io.kamax.mxisd.http.undertow.handler.profile.v1.ProfileHandler;
@@ -147,6 +148,11 @@ public class HttpMxisd {
termsEndpoints(handler);
hashEndpoints(handler);
accountEndpoints(handler);
if (m.getConfig().getInternal().isEnabled()) {
handler.get(InternalInviteManagerHandler.PATH, new InternalInviteManagerHandler(m.getInvite()));
}
ServerConfig serverConfig = m.getConfig().getServer();
httpSrv = Undertow.builder().addHttpListener(serverConfig.getPort(), serverConfig.getHostname()).setHandler(handler).build();

View File

@@ -54,6 +54,8 @@ public class LdapAuthProvider extends LdapBackend implements AuthenticatorProvid
private transient final Logger log = LoggerFactory.getLogger(LdapAuthProvider.class);
public static final char[] CHARACTERS_TO_ESCAPE = ",#+<>;\"=*\\\\".toCharArray();
private PhoneNumberUtil phoneUtil = PhoneNumberUtil.getInstance();
public LdapAuthProvider(LdapConfig cfg, MatrixConfig mxCfg) {
@@ -94,7 +96,8 @@ public class LdapAuthProvider extends LdapBackend implements AuthenticatorProvid
return BackendAuthResult.failure();
}
String userFilter = "(" + getUidAtt() + "=" + userFilterValue + ")";
String filteredValue = escape(userFilterValue);
String userFilter = "(" + getUidAtt() + "=" + filteredValue + ")";
userFilter = buildWithFilter(userFilter, getCfg().getAuth().getFilter());
Set<String> attributes = new HashSet<>();
@@ -167,4 +170,16 @@ public class LdapAuthProvider extends LdapBackend implements AuthenticatorProvid
}
}
private String escape(String raw) {
StringBuilder sb = new StringBuilder();
boolean escape;
for (char c : raw.toCharArray()) {
escape = false;
for (int i = 0; i < CHARACTERS_TO_ESCAPE.length && !escape; i++) {
escape = CHARACTERS_TO_ESCAPE[i] == c;
}
sb.append(escape ? "\\" + c : c);
}
return sb.toString();
}
}

View File

@@ -20,6 +20,7 @@
package io.kamax.mxisd.backend.ldap;
import io.kamax.matrix.MatrixID;
import io.kamax.matrix._MatrixID;
import io.kamax.mxisd.config.MatrixConfig;
import io.kamax.mxisd.config.ldap.LdapConfig;
@@ -116,10 +117,20 @@ public abstract class LdapBackend {
public String buildMatrixIdFromUid(String uid) {
String uidType = getCfg().getAttribute().getUid().getType();
String localpart = uid.toLowerCase();
if (!StringUtils.equals(uid, localpart)) {
log.info("UID {} from LDAP has been changed to lowercase to match the Synapse specifications", uid);
}
if (StringUtils.equals(UID, uidType)) {
return "@" + uid + ":" + mxCfg.getDomain();
if(getCfg().isActiveDirectory()) {
localpart = new UPN(uid.toLowerCase()).getMXID();
}
return "@" + localpart + ":" + mxCfg.getDomain();
} else if (StringUtils.equals(MATRIX_ID, uidType)) {
return uid;
return localpart;
} else {
throw new IllegalArgumentException("Bind type " + uidType + " is not supported");
}
@@ -128,6 +139,10 @@ public abstract class LdapBackend {
public String buildUidFromMatrixId(_MatrixID mxId) {
String uidType = getCfg().getAttribute().getUid().getType();
if (StringUtils.equals(UID, uidType)) {
if(getCfg().isActiveDirectory()) {
return new UPN(mxId).getUPN();
}
return mxId.getLocalPart();
} else if (StringUtils.equals(MATRIX_ID, uidType)) {
return mxId.getId();
@@ -169,4 +184,58 @@ public abstract class LdapBackend {
return values;
}
private class UPN {
private String login;
private String domain;
public UPN(String userPrincipalName) {
String[] uidParts = userPrincipalName.split("@");
if (uidParts.length != 2) {
throw new IllegalArgumentException(String.format("Wrong userPrincipalName provided: %s", userPrincipalName));
}
this.login = uidParts[0];
this.domain = uidParts[1];
}
public UPN(_MatrixID mxid) {
String[] idParts = mxid.getLocalPart().split("/");
if (idParts.length != 2) {
if(idParts.length == 1 && !StringUtils.isEmpty(getCfg().getDefaultDomain())) {
throw new IllegalArgumentException(String.format(
"Local part of mxid %s does not contains domain separator and default domain is not configured",
mxid.getLocalPart()
));
}
this.domain = getCfg().getDefaultDomain();
} else {
this.domain = idParts[1];
}
this.login = idParts[0];
}
public String getLogin() {
return login;
}
public String getDomain() {
return domain;
}
public String getMXID() {
if(StringUtils.equalsIgnoreCase(getCfg().getDefaultDomain(), this.domain)) {
return this.login;
}
return new StringBuilder(this.login).append("/").append(this.domain).toString();
}
public String getUPN() {
return new StringBuilder(this.login).append("@").append(this.domain).toString();
}
}
}

View File

@@ -0,0 +1,24 @@
package io.kamax.mxisd.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class InternalAPIConfig {
private final static Logger log = LoggerFactory.getLogger(InternalAPIConfig.class);
private boolean enabled = false;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public void build() {
log.info("--- Internal API config ---");
log.info("Internal API enabled: {}", isEnabled());
}
}

View File

@@ -67,6 +67,7 @@ public class InvitationConfig {
private boolean recursive = true;
private long timer = 5;
private PeriodDimension period = PeriodDimension.minutes;
public boolean isRecursive() {
return recursive;
@@ -84,6 +85,13 @@ public class InvitationConfig {
this.timer = timer;
}
public PeriodDimension getPeriod() {
return period;
}
public void setPeriod(PeriodDimension period) {
this.period = period;
}
}
public static class SenderPolicy {
@@ -115,6 +123,7 @@ public class InvitationConfig {
private Expiration expiration = new Expiration();
private Resolution resolution = new Resolution();
private Policies policy = new Policies();
private boolean fullDisplayName = false;
public Expiration getExpiration() {
return expiration;
@@ -140,11 +149,26 @@ public class InvitationConfig {
this.policy = policy;
}
public boolean isFullDisplayName() {
return fullDisplayName;
}
public void setFullDisplayName(boolean fullDisplayName) {
this.fullDisplayName = fullDisplayName;
}
public void build() {
log.info("--- Invite config ---");
log.info("Expiration: {}", GsonUtil.get().toJson(getExpiration()));
log.info("Resolution: {}", GsonUtil.get().toJson(getResolution()));
log.info("Policies: {}", GsonUtil.get().toJson(getPolicy()));
log.info("Print full display name on invitation: {}", isFullDisplayName());
}
public enum PeriodDimension {
minutes,
seconds
}
}

View File

@@ -118,6 +118,7 @@ public class MxisdConfig {
private PolicyConfig policy = new PolicyConfig();
private HashingConfig hashing = new HashingConfig();
private LoggingConfig logging = new LoggingConfig();
private InternalAPIConfig internal = new InternalAPIConfig();
public AppServiceConfig getAppsvc() {
return appsvc;
@@ -358,6 +359,14 @@ public class MxisdConfig {
return this;
}
public InternalAPIConfig getInternal() {
return internal;
}
public void setInternal(InternalAPIConfig internal) {
this.internal = internal;
}
public MxisdConfig build() {
getLogging().build();
@@ -394,6 +403,7 @@ public class MxisdConfig {
getWordpress().build();
getPolicy().build();
getHashing().build(getMatrix());
getInternal().build();
return this;
}

View File

@@ -291,6 +291,9 @@ public abstract class LdapConfig {
private boolean enabled;
private String filter;
private boolean activeDirectory;
private String defaultDomain;
private Connection connection = new Connection();
private Attribute attribute = new Attribute();
private Auth auth = new Auth();
@@ -316,6 +319,22 @@ public abstract class LdapConfig {
this.filter = filter;
}
public boolean isActiveDirectory() {
return activeDirectory;
}
public void setActiveDirectory(boolean activeDirectory) {
this.activeDirectory = activeDirectory;
}
public String getDefaultDomain() {
return defaultDomain;
}
public void setDefaultDomain(String defaultDomain) {
this.defaultDomain = defaultDomain;
}
public Connection getConnection() {
return connection;
}
@@ -407,6 +426,15 @@ public abstract class LdapConfig {
throw new ConfigurationException("ldap.identity.token");
}
if(isActiveDirectory()) {
if(!StringUtils.equals(LdapBackend.UID, uidType)) {
throw new IllegalArgumentException(String.format(
"Attribute UID type should be set to %s in Active Directory mode",
LdapBackend.UID
));
}
}
// Build queries
attribute.getThreepid().forEach((k, v) -> {
if (StringUtils.isBlank(identity.getMedium().get(k))) {

View File

@@ -23,5 +23,6 @@ package io.kamax.mxisd.exception;
public class InvalidParamException extends RuntimeException {
public InvalidParamException() {
super("The chosen hash algorithm is invalid or disallowed");
}
}

View File

@@ -23,5 +23,6 @@ package io.kamax.mxisd.exception;
public class InvalidPepperException extends RuntimeException {
public InvalidPepperException() {
super("The provided pepper is invalid or expired");
}
}

View File

@@ -0,0 +1,28 @@
/*
* ma1sd - Matrix Identity Server Daemon
* Copyright (C) 2020 Anatoliy SAblin
*
* https://www.github.com/ma1uta/ma1sd/
*
* 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.exception;
public class TermsNotSignedException extends RuntimeException {
public TermsNotSignedException() {
super("Please accept our updated terms of service before continuing");
}
}

View File

@@ -20,11 +20,19 @@
package io.kamax.mxisd.http;
import static io.kamax.mxisd.util.RestClientUtils.urlEncode;
public class IsAPIv1 {
public static final String Base = "/_matrix/identity/api/v1";
public static String getValidate(String medium, String sid, String secret, String token) {
return String.format("%s/validate/%s/submitToken?sid=%s&client_secret=%s&token=%s", Base, medium, sid, secret, token);
return String.format("%s/validate/%s/submitToken?sid=%s&client_secret=%s&token=%s",
Base,
medium,
urlEncode(sid),
urlEncode(secret),
urlEncode(token)
);
}
}

View File

@@ -33,6 +33,7 @@ import io.kamax.mxisd.util.RestClientUtils;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.form.FormData;
import io.undertow.util.Headers;
import io.undertow.util.HttpString;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
@@ -189,7 +190,7 @@ public abstract class BasicHttpHandler implements HttpHandler {
}
protected void respond(HttpServerExchange ex, int status, String errCode, String error) {
respond(ex, status, buildErrorBody(ex, errCode, error));
respond(ex, status, buildErrorBody(ex, errCode, error != null ? error : "An error has occurred"));
}
protected void handleException(HttpServerExchange exchange, HttpMatrixException ex) {
@@ -203,26 +204,34 @@ public abstract class BasicHttpHandler implements HttpHandler {
}
protected void proxyPost(HttpServerExchange exchange, JsonObject body, CloseableHttpClient client, ClientDnsOverwrite dns) {
proxyPost(exchange, body, client, dns, false);
}
protected void proxyPost(HttpServerExchange exchange, JsonObject body, CloseableHttpClient client, ClientDnsOverwrite dns,
boolean defaultJsonResponse) {
String target = dns.transform(URI.create(exchange.getRequestURL())).toString();
log.info("Requesting remote: {}", target);
HttpPost req = RestClientUtils.post(target, GsonUtil.get(), body);
exchange.getRequestHeaders().forEach(header -> {
header.forEach(v -> {
String name = header.getHeaderName().toString();
if (!StringUtils.startsWithIgnoreCase(name, "content-")) {
req.addHeader(name, v);
}
});
});
exchange.getRequestHeaders().forEach(header -> header.forEach(v -> {
String name = header.getHeaderName().toString();
if (!StringUtils.startsWithIgnoreCase(name, "content-")) {
req.addHeader(name, v);
}
}));
boolean missingJsonResponse = true;
try (CloseableHttpResponse res = client.execute(req)) {
exchange.setStatusCode(res.getStatusLine().getStatusCode());
for (Header h : res.getAllHeaders()) {
for (HeaderElement el : h.getElements()) {
missingJsonResponse = !Headers.CONTENT_TYPE_STRING.equalsIgnoreCase(h.getName());
exchange.getResponseHeaders().add(HttpString.tryFromString(h.getName()), el.getValue());
}
}
if (defaultJsonResponse && missingJsonResponse) {
exchange.getRequestHeaders().add(Headers.CONTENT_TYPE, "application/json");
}
res.getEntity().writeTo(exchange.getOutputStream());
exchange.endExchange();
} catch (IOException e) {

View File

@@ -23,6 +23,7 @@ package io.kamax.mxisd.http.undertow.handler;
import io.kamax.mxisd.auth.AccountManager;
import io.kamax.mxisd.config.PolicyConfig;
import io.kamax.mxisd.exception.InvalidCredentialsException;
import io.kamax.mxisd.exception.TermsNotSignedException;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import org.slf4j.Logger;
@@ -66,7 +67,7 @@ public class CheckTermsHandler extends BasicHttpHandler {
if (!accountManager.isTermAccepted(token, policies)) {
log.error("Non accepting request from: {}", exchange.getHostAndPort());
throw new InvalidCredentialsException();
throw new TermsNotSignedException();
}
log.trace("Access granted");
child.handleRequest(exchange);

View File

@@ -82,7 +82,10 @@ public class SaneHandler extends BasicHttpHandler {
} catch (InvalidJsonException e) {
respond(exchange, HttpStatus.SC_BAD_REQUEST, e.getErrorCode(), e.getError());
} catch (InvalidCredentialsException e) {
log.error("Unauthorized: ", e);
respond(exchange, HttpStatus.SC_UNAUTHORIZED, "M_UNAUTHORIZED", e.getMessage());
} catch (TermsNotSignedException e) {
respond(exchange, HttpStatus.SC_FORBIDDEN, "M_TERMS_NOT_SIGNED", e.getMessage());
} catch (ObjectNotFoundException e) {
respond(exchange, HttpStatus.SC_NOT_FOUND, "M_NOT_FOUND", e.getMessage());
} catch (NotImplementedException e) {

View File

@@ -0,0 +1,30 @@
package io.kamax.mxisd.http.undertow.handler.internal;
import com.google.gson.JsonObject;
import io.kamax.mxisd.http.undertow.handler.BasicHttpHandler;
import io.kamax.mxisd.invitation.InvitationManager;
import io.undertow.server.HttpServerExchange;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class InternalInviteManagerHandler extends BasicHttpHandler {
public static final String PATH = "/_ma1sd/internal/admin/inv_manager";
private final InvitationManager invitationManager;
private final ExecutorService executors = Executors.newFixedThreadPool(1);
public InternalInviteManagerHandler(InvitationManager invitationManager) {
this.invitationManager = invitationManager;
}
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
executors.submit(invitationManager::doMaintenance);
JsonObject obj = new JsonObject();
obj.addProperty("result", "ok");
respond(exchange, obj);
}
}

View File

@@ -71,7 +71,7 @@ public class Register3pidRequestTokenHandler extends BasicHttpHandler {
throw new NotAllowedException("Your " + medium + " address cannot be used for registration");
}
proxyPost(exchange, body, client, dns);
proxyPost(exchange, body, client, dns, true);
}
}

View File

@@ -99,14 +99,14 @@ public class InvitationManager {
private Map<String, IThreePidInviteReply> invitations = new ConcurrentHashMap<>();
public InvitationManager(
MxisdConfig mxisdCfg,
IStorage storage,
LookupStrategy lookupMgr,
KeyManager keyMgr,
SignatureManager signMgr,
HomeserverFederationResolver resolver,
NotificationManager notifMgr,
ProfileManager profileMgr
MxisdConfig mxisdCfg,
IStorage storage,
LookupStrategy lookupMgr,
KeyManager keyMgr,
SignatureManager signMgr,
HomeserverFederationResolver resolver,
NotificationManager notifMgr,
ProfileManager profileMgr
) {
this.cfg = requireValid(mxisdCfg);
this.srvCfg = mxisdCfg.getServer();
@@ -124,11 +124,11 @@ public class InvitationManager {
io.getProperties().putIfAbsent(CreatedAtPropertyKey, defaultCreateTs);
log.debug("Processing invite {}", GsonUtil.get().toJson(io));
ThreePidInvite invite = new ThreePidInvite(
MatrixID.asAcceptable(io.getSender()),
io.getMedium(),
io.getAddress(),
io.getRoomId(),
io.getProperties()
MatrixID.asAcceptable(io.getSender()),
io.getMedium(),
io.getAddress(),
io.getRoomId(),
io.getProperties()
);
ThreePidInviteReply reply = new ThreePidInviteReply(io.getId(), invite, io.getToken(), "", Collections.emptyList());
@@ -155,7 +155,17 @@ public class InvitationManager {
log.error("Error when running background maintenance", t);
}
}
}, 5000L, TimeUnit.MILLISECONDS.convert(cfg.getResolution().getTimer(), TimeUnit.MINUTES));
}, 5000L, TimeUnit.MILLISECONDS.convert(cfg.getResolution().getTimer(), getTimeUnit()));
}
private TimeUnit getTimeUnit() {
switch (cfg.getResolution().getPeriod()) {
case seconds:
return TimeUnit.SECONDS;
case minutes:
default:
return TimeUnit.MINUTES;
}
}
private InvitationConfig requireValid(MxisdConfig cfg) {
@@ -176,7 +186,8 @@ public class InvitationManager {
if (StringUtils.isBlank(cfg.getInvite().getExpiration().getResolveTo())) {
String localpart = cfg.getAppsvc().getUser().getInviteExpired();
if (StringUtils.isBlank(localpart)) {
throw new ConfigurationException("Could not compute the Invitation expiration resolution target from App service user: not set");
throw new ConfigurationException(
"Could not compute the Invitation expiration resolution target from App service user: not set");
}
cfg.getInvite().getExpiration().setResolveTo(MatrixID.asAcceptable(localpart, cfg.getMatrix().getDomain()).getId());
@@ -198,7 +209,8 @@ public class InvitationManager {
}
private String getIdForLog(IThreePidInviteReply reply) {
return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite().getMedium() + ":" + reply.getInvite().getAddress();
return reply.getInvite().getSender().getId() + ":" + reply.getInvite().getRoomId() + ":" + reply.getInvite()
.getMedium() + ":" + reply.getInvite().getAddress();
}
private Optional<SingleLookupReply> lookup3pid(String medium, String address) {
@@ -252,13 +264,16 @@ public class InvitationManager {
}
String invId = computeId(invitation);
log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(), invitation.getRoomId());
log.info("Handling invite for {}:{} from {} in room {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender(),
invitation.getRoomId());
IThreePidInviteReply reply = invitations.get(invId);
if (reply != null) {
log.info("Invite is already pending for {}:{}, returning data", invitation.getMedium(), invitation.getAddress());
if (!StringUtils.equals(invitation.getRoomId(), reply.getInvite().getRoomId())) {
log.info("Sending new notification as new invite room {} is different from the original {}", invitation.getRoomId(), reply.getInvite().getRoomId());
notifMgr.sendForReply(new ThreePidInviteReply(reply.getId(), invitation, reply.getToken(), reply.getDisplayName(), reply.getPublicKeys()));
log.info("Sending new notification as new invite room {} is different from the original {}", invitation.getRoomId(),
reply.getInvite().getRoomId());
notifMgr.sendForReply(
new ThreePidInviteReply(reply.getId(), invitation, reply.getToken(), reply.getDisplayName(), reply.getPublicKeys()));
} else {
// FIXME we should check attempt and send if bigger
}
@@ -272,7 +287,7 @@ public class InvitationManager {
}
String token = RandomStringUtils.randomAlphanumeric(64);
String displayName = invitation.getAddress().substring(0, 3) + "...";
String displayName = getInvitedDisplayName(invitation.getAddress());
KeyIdentifier pKeyId = keyMgr.getServerSigningKey().getId();
KeyIdentifier eKeyId = keyMgr.generateKey(KeyType.Ephemeral);
@@ -295,11 +310,20 @@ public class InvitationManager {
log.info("Storing invite under ID {}", invId);
storage.insertInvite(reply);
invitations.put(invId, reply);
log.info("A new invite has been created for {}:{} on HS {}", invitation.getMedium(), invitation.getAddress(), invitation.getSender().getDomain());
log.info("A new invite has been created for {}:{} on HS {}", invitation.getMedium(), invitation.getAddress(),
invitation.getSender().getDomain());
return reply;
}
private String getInvitedDisplayName(String origin) {
if (cfg.isFullDisplayName()) {
return origin;
} else {
return origin.substring(0, 3) + "...";
}
}
public boolean hasInvite(ThreePid tpid) {
for (IThreePidInviteReply reply : invitations.values()) {
if (!StringUtils.equals(tpid.getMedium(), reply.getInvite().getMedium())) {
@@ -385,8 +409,10 @@ public class InvitationManager {
public void publishMappingIfInvited(ThreePidMapping threePid) {
log.info("Looking up possible pending invites for {}:{}", threePid.getMedium(), threePid.getValue());
for (IThreePidInviteReply reply : invitations.values()) {
if (StringUtils.equalsIgnoreCase(reply.getInvite().getMedium(), threePid.getMedium()) && StringUtils.equalsIgnoreCase(reply.getInvite().getAddress(), threePid.getValue())) {
log.info("{}:{} has an invite pending on HS {}, publishing mapping", threePid.getMedium(), threePid.getValue(), reply.getInvite().getSender().getDomain());
if (StringUtils.equalsIgnoreCase(reply.getInvite().getMedium(), threePid.getMedium()) && StringUtils
.equalsIgnoreCase(reply.getInvite().getAddress(), threePid.getValue())) {
log.info("{}:{} has an invite pending on HS {}, publishing mapping", threePid.getMedium(), threePid.getValue(),
reply.getInvite().getSender().getDomain());
publishMapping(reply, threePid.getMxid());
}
}

View File

@@ -23,10 +23,10 @@ package io.kamax.mxisd.storage.ormlite;
import com.j256.ormlite.dao.CloseableWrappedIterable;
import com.j256.ormlite.dao.Dao;
import com.j256.ormlite.dao.DaoManager;
import com.j256.ormlite.db.PostgresDatabaseType;
import com.j256.ormlite.db.SqliteDatabaseType;
import com.j256.ormlite.jdbc.JdbcConnectionSource;
import com.j256.ormlite.jdbc.JdbcPooledConnectionSource;
import com.j256.ormlite.jdbc.db.PostgresDatabaseType;
import com.j256.ormlite.jdbc.db.SqliteDatabaseType;
import com.j256.ormlite.stmt.QueryBuilder;
import com.j256.ormlite.support.ConnectionSource;
import com.j256.ormlite.table.TableUtils;
@@ -88,6 +88,7 @@ public class OrmLiteSqlStorage implements IStorage {
public static final String FIX_ACCEPTED_DAO = "2019_12_09__2254__fix_accepted_dao";
public static final String FIX_HASH_DAO_UNIQUE_INDEX = "2020_03_22__1153__fix_hash_dao_unique_index";
public static final String CHANGE_TYPE_TO_TEXT_INVITE = "2020_04_21__2338__change_type_table_invites";
public static final String CHANGE_TYPE_TO_TEXT_INVITE_HISTORY = "2020_10_26__2200__change_type_table_invite_history";
}
private Dao<ThreePidInviteIO, String> invDao;
@@ -177,6 +178,11 @@ public class OrmLiteSqlStorage implements IStorage {
fixInviteTableColumnType(connPol);
changelogDao.create(new ChangelogDao(Migrations.CHANGE_TYPE_TO_TEXT_INVITE, new Date(), "Modify column type to text."));
}
ChangelogDao fixInviteHistoryTableColumnType = changelogDao.queryForId(Migrations.CHANGE_TYPE_TO_TEXT_INVITE_HISTORY);
if (fixInviteHistoryTableColumnType == null) {
fixInviteHistoryTableColumnType(connPol);
changelogDao.create(new ChangelogDao(Migrations.CHANGE_TYPE_TO_TEXT_INVITE_HISTORY, new Date(), "Modify column type to text."));
}
}
private void fixAcceptedDao(ConnectionSource connPool) throws SQLException {
@@ -204,6 +210,20 @@ public class OrmLiteSqlStorage implements IStorage {
}
}
private void fixInviteHistoryTableColumnType(ConnectionSource connPool) throws SQLException {
LOGGER.info("Migration: {}", Migrations.CHANGE_TYPE_TO_TEXT_INVITE_HISTORY);
if (StorageConfig.BackendEnum.postgresql == backend) {
invDao.executeRawNoArgs("alter table invite_3pid_history alter column \"resolvedTo\" type text");
invDao.executeRawNoArgs("alter table invite_3pid_history alter column id type text");
invDao.executeRawNoArgs("alter table invite_3pid_history alter column token type text");
invDao.executeRawNoArgs("alter table invite_3pid_history alter column sender type text");
invDao.executeRawNoArgs("alter table invite_3pid_history alter column medium type text");
invDao.executeRawNoArgs("alter table invite_3pid_history alter column address type text");
invDao.executeRawNoArgs("alter table invite_3pid_history alter column \"roomId\" type text");
invDao.executeRawNoArgs("alter table invite_3pid_history alter column properties type text");
}
}
private <V, K> Dao<V, K> createDaoAndTable(ConnectionSource connPool, Class<V> c) throws SQLException {
return createDaoAndTable(connPool, c, false);
}