From 85236793e155ce3a6044767d029a2e93131d5995 Mon Sep 17 00:00:00 2001 From: Maxime Dor Date: Mon, 4 Sep 2017 03:08:19 +0200 Subject: [PATCH] Skeleton to support LDAP Auth --- application.example.yaml | 102 ++++++++---- .../io/kamax/mxisd/config/LdapConfig.groovy | 156 ------------------ .../config/ldap/LdapAttributeConfig.java | 29 ++++ .../config/ldap/LdapAttributeUidConfig.java | 29 ++++ .../mxisd/config/ldap/LdapAuthConfig.java | 20 +++ .../kamax/mxisd/config/ldap/LdapConfig.groovy | 125 ++++++++++++++ .../config/ldap/LdapConnectionConfig.java | 65 ++++++++ .../mxisd/config/ldap/LdapIdentityConfig.java | 28 ++++ .../mxisd/lookup/provider/LdapProvider.groovy | 52 ++++-- 9 files changed, 407 insertions(+), 199 deletions(-) delete mode 100644 src/main/groovy/io/kamax/mxisd/config/LdapConfig.groovy create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java create mode 100644 src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java diff --git a/application.example.yaml b/application.example.yaml index 36908d4..0d6307c 100644 --- a/application.example.yaml +++ b/application.example.yaml @@ -97,41 +97,83 @@ lookup: ldap: + + # Global enable/disable switch enabled: true - tls: false - host: 'localhost' - port: 389 - bindDn: 'CN=Matrix Identity Server,CN=Users,DC=example,DC=org' - bindPassword: 'password' - baseDn: 'CN=Users,DC=example,DC=org' - # How should we resolve the Matrix ID in case of a match using the attribute. + # Connection configuration to the LDAP server + connection: + + # If the connection should be secure + tls: false + + # Host to connect to + host: 'localhost' + + # Port to connect to + port: 389 + + # Bind DN to use when performing lookups + bindDn: 'CN=Matrix Identity Server,CN=Users,DC=example,DC=org' + + # Bind password to use + bindPassword: 'password' + + # Base DN used in all queries + baseDn: 'CN=Users,DC=example,DC=org' + + # How to map Matrix attributes with LDAP attributes when performing lookup/auth + attributes: + + # The username/login that will be looked up or used to build Matrix IDs + uid: + + # How should we resolve the Matrix ID in case of a match using the attribute. + # + # The following type are supported: + # - uid : the attribute only contains the UID part of the Matrix ID. e.g. 'john.doe' in @john.doe:example.org + # - mxid : the attribute contains the full Matrix ID - e.g. '@john.doe:example.org' + type: 'uid' + + # The attribute containing the binding itself. This value will be used differently depending on the type. + # + # /!\ This should match the synapse LDAP Authenticator 'uid' configuration /!\ + # + # Typical values: + # - For type 'uid': 'userPrincipalName' or 'uid' or 'saMAccountName' + # - For type 'mxid', regardless of the directory type, we recommend using 'pager' as it is a standard attribute and + # is typically not used. + value: 'userPrincipalName' + + # The display name of the user + name: 'displayName' + + # Configuration section relating the authentication of users performed via LDAP. # - # The following type are supported: - # - uid : the attribute only contains the UID part of the Matrix ID. e.g. 'john.doe' in @john.doe:example.org - # - mxid : the attribute contains the full Matrix ID - e.g. '@john.doe:example.org' - type: 'uid' + # This can be done using the REST Auth module for synapse and pointing it to the identity server. + # See https://github.com/maxidor/matrix-synapse-rest-auth + auth: - # The attribute containing the binding itself. This value will be used differently depending on the type. - # - # /!\ This should match the synapse LDAP Authenticator 'uid' configuration /!\ - # - # Typical values: - # - For type 'uid': 'userPrincipalName' or 'uid' or 'saMAccountName' - # - For type 'mxid', regardless of the directory type, we recommend using 'pager' as it is a standard attribute and - # is typically not used. - attribute: 'userPrincipalName' - - # Configure each 3PID type with a dedicated query. - mappings: - email: "(|(mailPrimaryAddress=%3pid)(mail=%3pid)(otherMailbox=%3pid))" - - # Phone numbers query. + # What to filter potential users by, typically by using a dedicated group. + # If this value is not set, login check will be performed for all entities within the LDAP # - # Phone numbers use the MSISDN format: https://en.wikipedia.org/wiki/MSISDN - # This format does not include international prefix (+ or 00) and therefore has to be put in the query. - # Adapt this to your needs for each attribute. - msisdn: "(|(telephoneNumber=+%3pid)(mobile=+%3pid)(homePhone=+%3pid)(otherTelephone=+%3pid)(otherMobile=+%3pid)(otherHomePhone=+%3pid))" + # Example: (memberOf=CN=Matrix Users,CN=Users,DC=example,DC=org) + filter: '' + + # Configuration section relating to identity lookups + identity: + + # Configure each 3PID type with a dedicated query. + medium: + # E-mail query + email: "(|(mailPrimaryAddress=%3pid)(mail=%3pid)(otherMailbox=%3pid))" + + # Phone numbers query + # + # Phone numbers use the MSISDN format: https://en.wikipedia.org/wiki/MSISDN + # This format does not include international prefix (+ or 00) and therefore has to be put in the query. + # Adapt this to your needs for each attribute. + msisdn: "(|(telephoneNumber=+%3pid)(mobile=+%3pid)(homePhone=+%3pid)(otherTelephone=+%3pid)(otherMobile=+%3pid)(otherHomePhone=+%3pid))" diff --git a/src/main/groovy/io/kamax/mxisd/config/LdapConfig.groovy b/src/main/groovy/io/kamax/mxisd/config/LdapConfig.groovy deleted file mode 100644 index e90046e..0000000 --- a/src/main/groovy/io/kamax/mxisd/config/LdapConfig.groovy +++ /dev/null @@ -1,156 +0,0 @@ -/* - * mxisd - Matrix Identity Server Daemon - * Copyright (C) 2017 Maxime Dor - * - * https://max.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 . - */ - -package io.kamax.mxisd.config - -import org.apache.commons.lang.StringUtils -import org.slf4j.Logger -import org.slf4j.LoggerFactory -import org.springframework.beans.factory.InitializingBean -import org.springframework.boot.context.properties.ConfigurationProperties -import org.springframework.context.annotation.Configuration - -@Configuration -@ConfigurationProperties(prefix = "ldap") -class LdapConfig implements InitializingBean { - - private Logger log = LoggerFactory.getLogger(LdapConfig.class) - - private boolean enabled - private boolean tls - private String host - private int port - private String baseDn - private String type - private String attribute - private String bindDn - private String bindPassword - private Map mappings - - boolean getEnabled() { - return enabled - } - - void setEnabled(boolean enabled) { - this.enabled = enabled - } - - boolean getTls() { - return tls - } - - void setTls(boolean tls) { - this.tls = tls - } - - String getHost() { - return host - } - - void setHost(String host) { - this.host = host - } - - int getPort() { - return port - } - - void setPort(int port) { - this.port = port - } - - String getBaseDn() { - return baseDn - } - - void setBaseDn(String baseDn) { - this.baseDn = baseDn - } - - String getType() { - return type - } - - void setType(String type) { - this.type = type - } - - String getAttribute() { - return attribute - } - - void setAttribute(String attribute) { - this.attribute = attribute - } - - String getBindDn() { - return bindDn - } - - void setBindDn(String bindDn) { - this.bindDn = bindDn - } - - String getBindPassword() { - return bindPassword - } - - void setBindPassword(String bindPassword) { - this.bindPassword = bindPassword - } - - Map getMappings() { - return mappings - } - - void setMappings(Map mappings) { - this.mappings = mappings - } - - Optional getMapping(String type) { - if (mappings == null) { - return Optional.empty() - } - - return Optional.ofNullable(mappings.get(type)) - } - - @Override - void afterPropertiesSet() throws Exception { - log.info("LDAP enabled: {}", getEnabled()) - - if (!getEnabled()) { - return - } - - log.info("Matrix ID type: {}", getType()) - log.info("LDAP Host: {}", getHost()) - log.info("LDAP Bind DN: {}", getBindDn()) - log.info("LDAP Attribute: {}", getAttribute()) - - if (StringUtils.isBlank(getHost())) { - throw new IllegalStateException("LDAP Host must be configured!") - } - if (StringUtils.isBlank(getAttribute())) { - throw new IllegalStateException("LDAP attribute must be configured!") - } - } - -} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java new file mode 100644 index 0000000..f8139ee --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeConfig.java @@ -0,0 +1,29 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "ldap.attribute") +public class LdapAttributeConfig { + + private LdapAttributeUidConfig uid; + private String name; + + public LdapAttributeUidConfig getUid() { + return uid; + } + + public void setUid(LdapAttributeUidConfig uid) { + this.uid = uid; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java new file mode 100644 index 0000000..771a630 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAttributeUidConfig.java @@ -0,0 +1,29 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "ldap.attribute.uid") +public class LdapAttributeUidConfig { + + private String type; + private String value; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java new file mode 100644 index 0000000..011500e --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapAuthConfig.java @@ -0,0 +1,20 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "ldap.auth") +public class LdapAuthConfig { + + private String filter; + + public String getFilter() { + return filter; + } + + public void setFilter(String filter) { + this.filter = filter; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy new file mode 100644 index 0000000..55fd3d7 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConfig.groovy @@ -0,0 +1,125 @@ +/* + * mxisd - Matrix Identity Server Daemon + * Copyright (C) 2017 Maxime Dor + * + * https://max.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 . + */ + +package io.kamax.mxisd.config.ldap + +import groovy.json.JsonOutput +import org.apache.commons.lang.StringUtils +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Configuration + +import javax.annotation.PostConstruct + +@Configuration +@ConfigurationProperties(prefix = "ldap") +class LdapConfig { + + private Logger log = LoggerFactory.getLogger(LdapConfig.class) + + private boolean enabled + + @Autowired + private LdapConnectionConfig conn + private LdapAttributeConfig attribute + private LdapAuthConfig auth + private LdapIdentityConfig identity + + boolean isEnabled() { + return enabled + } + + void setEnabled(boolean enabled) { + this.enabled = enabled + } + + LdapConnectionConfig getConn() { + return conn + } + + void setConn(LdapConnectionConfig conn) { + this.conn = conn + } + + LdapAttributeConfig getAttribute() { + return attribute + } + + void setAttribute(LdapAttributeConfig attribute) { + this.attribute = attribute + } + + LdapAuthConfig getAuth() { + return auth + } + + void setAuth(LdapAuthConfig auth) { + this.auth = auth + } + + LdapIdentityConfig getIdentity() { + return identity + } + + void setIdentity(LdapIdentityConfig identity) { + this.identity = identity + } + + @PostConstruct + void afterPropertiesSet() { + log.info("--- LDAP Config ---") + log.info("Enabled: {}", isEnabled()) + + if (!isEnabled()) { + return + } + + if (StringUtils.isBlank(conn.getHost())) { + throw new IllegalStateException("LDAP Host must be configured!") + } + + if (1 > conn.getPort() || 65535 < conn.getPort()) { + throw new IllegalStateException("LDAP port is not valid") + } + + if (StringUtils.isBlank(attribute.getUid().getType())) { + throw new IllegalStateException("Attribute UID Type cannot be empty") + } + + + if (StringUtils.isBlank(attribute.getUid().getValue())) { + throw new IllegalStateException("Attribute UID value cannot be empty") + } + + + log.info("Conn: {}", JsonOutput.toJson(conn)) + log.info("Host: {}", conn.getHost()) + log.info("Port: {}", conn.getPort()) + log.info("Bind DN: {}", conn.getBindDn()) + log.info("Base DN: {}", conn.getBaseDn()) + + log.info("Attribute: {}", JsonOutput.toJson(attribute)) + log.info("Auth: {}", JsonOutput.toJson(auth)) + log.info("Identity: {}", JsonOutput.toJson(identity)) + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java new file mode 100644 index 0000000..7f85e5d --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapConnectionConfig.java @@ -0,0 +1,65 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "ldap.connection") +public class LdapConnectionConfig { + + private boolean tls; + private String host; + private int port; + private String bindDn; + private String bindPassword; + private String baseDn; + + public boolean isTls() { + return tls; + } + + public void setTls(boolean tls) { + this.tls = tls; + } + + public String getHost() { + return host; + } + + public void setHost(String host) { + this.host = host; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getBindDn() { + return bindDn; + } + + public void setBindDn(String bindDn) { + this.bindDn = bindDn; + } + + public String getBindPassword() { + return bindPassword; + } + + public void setBindPassword(String bindPassword) { + this.bindPassword = bindPassword; + } + + public String getBaseDn() { + return baseDn; + } + + public void setBaseDn(String baseDn) { + this.baseDn = baseDn; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java new file mode 100644 index 0000000..c6839e4 --- /dev/null +++ b/src/main/groovy/io/kamax/mxisd/config/ldap/LdapIdentityConfig.java @@ -0,0 +1,28 @@ +package io.kamax.mxisd.config.ldap; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +@Configuration +@ConfigurationProperties(prefix = "ldap.identity") +public class LdapIdentityConfig { + + private Map medium = new HashMap<>(); + + public Map getMedium() { + return medium; + } + + public Optional getQuery(String key) { + return Optional.ofNullable(medium.get(key)); + } + + public void setMedium(Map medium) { + this.medium = medium; + } + +} diff --git a/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy b/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy index 281fb82..392e5ff 100644 --- a/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy +++ b/src/main/groovy/io/kamax/mxisd/lookup/provider/LdapProvider.groovy @@ -20,8 +20,10 @@ package io.kamax.mxisd.lookup.provider -import io.kamax.mxisd.config.LdapConfig +import io.kamax.mxisd.auth.UserAuthResult +import io.kamax.mxisd.auth.provider.AuthenticatorProvider import io.kamax.mxisd.config.ServerConfig +import io.kamax.mxisd.config.ldap.LdapConfig import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.ThreePidMapping import org.apache.commons.lang.StringUtils @@ -38,7 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component @Component -class LdapProvider implements IThreePidProvider { +class LdapProvider implements IThreePidProvider, AuthenticatorProvider { public static final String UID = "uid" public static final String MATRIX_ID = "mxid" @@ -53,7 +55,28 @@ class LdapProvider implements IThreePidProvider { @Override boolean isEnabled() { - return ldapCfg.getEnabled() + return ldapCfg.isEnabled() + } + + private LdapConnection getConn() { + return new LdapNetworkConnection(ldapCfg.getConn().getHost(), ldapCfg.getConn().getPort(), ldapCfg.getConn().isTls()) + } + + private void bind(LdapConnection conn) { + conn.bind(ldapCfg.getConn().getBindDn(), ldapCfg.getConn().getBindPassword()) + } + + @Override + UserAuthResult authenticate(String id, String password) { + LdapConnection conn = getConn() + try { + bind(conn) + + // TODO finish this + return new UserAuthResult().failure() + } finally { + conn.close() + } } @Override @@ -67,20 +90,22 @@ class LdapProvider implements IThreePidProvider { } Optional lookup(LdapConnection conn, String medium, String value) { - Optional queryOpt = ldapCfg.getMapping(medium) + String uidAttribute = ldapCfg.getAttribute().getUid().getValue() + + Optional queryOpt = ldapCfg.getIdentity().getQuery(medium) if (!queryOpt.isPresent()) { log.warn("{} is not a configured 3PID type for LDAP lookup", medium) return Optional.empty() } String searchQuery = queryOpt.get().replaceAll("%3pid", value) - EntryCursor cursor = conn.search(ldapCfg.getBaseDn(), searchQuery, SearchScope.SUBTREE, ldapCfg.getAttribute()) + EntryCursor cursor = conn.search(ldapCfg.getConn().getBaseDn(), searchQuery, SearchScope.SUBTREE, uidAttribute) try { while (cursor.next()) { Entry entry = cursor.get() log.info("Found possible match, DN: {}", entry.getDn().getName()) - Attribute attribute = entry.get(ldapCfg.getAttribute()) + Attribute attribute = entry.get(uidAttribute) if (attribute == null) { log.info("DN {}: no attribute {}, skpping", entry.getDn(), ldapCfg.getAttribute()) continue @@ -94,12 +119,13 @@ class LdapProvider implements IThreePidProvider { StringBuilder matrixId = new StringBuilder() // TODO Should we turn this block into a map of functions? - if (StringUtils.equals(UID, ldapCfg.getType())) { + String uidType = ldapCfg.getAttribute().getUid().getType() + if (StringUtils.equals(UID, uidType)) { matrixId.append("@").append(data).append(":").append(srvCfg.getName()) - } else if (StringUtils.equals(MATRIX_ID, ldapCfg.getType())) { + } else if (StringUtils.equals(MATRIX_ID, uidType)) { matrixId.append(data) } else { - log.warn("Bind was found but type {} is not supported", ldapCfg.getType()) + log.warn("Bind was found but type {} is not supported", uidType) continue } @@ -119,9 +145,9 @@ class LdapProvider implements IThreePidProvider { Optional find(SingleLookupRequest request) { log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}") - LdapConnection conn = new LdapNetworkConnection(ldapCfg.getHost(), ldapCfg.getPort(), ldapCfg.getTls()) + LdapConnection conn = getConn() try { - conn.bind(ldapCfg.getBindDn(), ldapCfg.getBindPassword()) + bind(conn) Optional mxid = lookup(conn, request.getType(), request.getThreePid()) if (mxid.isPresent()) { @@ -147,9 +173,9 @@ class LdapProvider implements IThreePidProvider { log.info("Looking up {} mappings", mappings.size()) List mappingsFound = new ArrayList<>() - LdapConnection conn = new LdapNetworkConnection(ldapCfg.getHost(), ldapCfg.getPort()) + LdapConnection conn = getConn() try { - conn.bind(ldapCfg.getBindDn(), ldapCfg.getBindPassword()) + bind(conn) for (ThreePidMapping mapping : mappings) { try {