Skeleton to support LDAP Auth

This commit is contained in:
Maxime Dor
2017-09-04 03:08:19 +02:00
parent 694e62edee
commit 85236793e1
9 changed files with 407 additions and 199 deletions

View File

@@ -97,14 +97,37 @@ lookup:
ldap: ldap:
# Global enable/disable switch
enabled: true enabled: true
# Connection configuration to the LDAP server
connection:
# If the connection should be secure
tls: false tls: false
# Host to connect to
host: 'localhost' host: 'localhost'
# Port to connect to
port: 389 port: 389
# Bind DN to use when performing lookups
bindDn: 'CN=Matrix Identity Server,CN=Users,DC=example,DC=org' bindDn: 'CN=Matrix Identity Server,CN=Users,DC=example,DC=org'
# Bind password to use
bindPassword: 'password' bindPassword: 'password'
# Base DN used in all queries
baseDn: 'CN=Users,DC=example,DC=org' 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. # How should we resolve the Matrix ID in case of a match using the attribute.
# #
# The following type are supported: # The following type are supported:
@@ -120,13 +143,32 @@ ldap:
# - For type 'uid': 'userPrincipalName' or 'uid' or 'saMAccountName' # - 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 # - For type 'mxid', regardless of the directory type, we recommend using 'pager' as it is a standard attribute and
# is typically not used. # is typically not used.
attribute: 'userPrincipalName' value: 'userPrincipalName'
# The display name of the user
name: 'displayName'
# Configuration section relating the authentication of users performed via LDAP.
#
# 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:
# 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
#
# 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. # Configure each 3PID type with a dedicated query.
mappings: medium:
# E-mail query
email: "(|(mailPrimaryAddress=%3pid)(mail=%3pid)(otherMailbox=%3pid))" email: "(|(mailPrimaryAddress=%3pid)(mail=%3pid)(otherMailbox=%3pid))"
# Phone numbers query. # Phone numbers query
# #
# Phone numbers use the MSISDN format: https://en.wikipedia.org/wiki/MSISDN # 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. # This format does not include international prefix (+ or 00) and therefore has to be put in the query.

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, String> 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<String, String> getMappings() {
return mappings
}
void setMappings(Map<String, String> mappings) {
this.mappings = mappings
}
Optional<String> 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!")
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

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

View File

@@ -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;
}
}

View File

@@ -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<String, String> medium = new HashMap<>();
public Map<String, String> getMedium() {
return medium;
}
public Optional<String> getQuery(String key) {
return Optional.ofNullable(medium.get(key));
}
public void setMedium(Map<String, String> medium) {
this.medium = medium;
}
}

View File

@@ -20,8 +20,10 @@
package io.kamax.mxisd.lookup.provider 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.ServerConfig
import io.kamax.mxisd.config.ldap.LdapConfig
import io.kamax.mxisd.lookup.SingleLookupRequest import io.kamax.mxisd.lookup.SingleLookupRequest
import io.kamax.mxisd.lookup.ThreePidMapping import io.kamax.mxisd.lookup.ThreePidMapping
import org.apache.commons.lang.StringUtils import org.apache.commons.lang.StringUtils
@@ -38,7 +40,7 @@ import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
@Component @Component
class LdapProvider implements IThreePidProvider { class LdapProvider implements IThreePidProvider, AuthenticatorProvider {
public static final String UID = "uid" public static final String UID = "uid"
public static final String MATRIX_ID = "mxid" public static final String MATRIX_ID = "mxid"
@@ -53,7 +55,28 @@ class LdapProvider implements IThreePidProvider {
@Override @Override
boolean isEnabled() { 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 @Override
@@ -67,20 +90,22 @@ class LdapProvider implements IThreePidProvider {
} }
Optional<String> lookup(LdapConnection conn, String medium, String value) { Optional<String> lookup(LdapConnection conn, String medium, String value) {
Optional<String> queryOpt = ldapCfg.getMapping(medium) String uidAttribute = ldapCfg.getAttribute().getUid().getValue()
Optional<String> queryOpt = ldapCfg.getIdentity().getQuery(medium)
if (!queryOpt.isPresent()) { if (!queryOpt.isPresent()) {
log.warn("{} is not a configured 3PID type for LDAP lookup", medium) log.warn("{} is not a configured 3PID type for LDAP lookup", medium)
return Optional.empty() return Optional.empty()
} }
String searchQuery = queryOpt.get().replaceAll("%3pid", value) 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 { try {
while (cursor.next()) { while (cursor.next()) {
Entry entry = cursor.get() Entry entry = cursor.get()
log.info("Found possible match, DN: {}", entry.getDn().getName()) log.info("Found possible match, DN: {}", entry.getDn().getName())
Attribute attribute = entry.get(ldapCfg.getAttribute()) Attribute attribute = entry.get(uidAttribute)
if (attribute == null) { if (attribute == null) {
log.info("DN {}: no attribute {}, skpping", entry.getDn(), ldapCfg.getAttribute()) log.info("DN {}: no attribute {}, skpping", entry.getDn(), ldapCfg.getAttribute())
continue continue
@@ -94,12 +119,13 @@ class LdapProvider implements IThreePidProvider {
StringBuilder matrixId = new StringBuilder() StringBuilder matrixId = new StringBuilder()
// TODO Should we turn this block into a map of functions? // 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()) 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) matrixId.append(data)
} else { } 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 continue
} }
@@ -119,9 +145,9 @@ class LdapProvider implements IThreePidProvider {
Optional<?> find(SingleLookupRequest request) { Optional<?> find(SingleLookupRequest request) {
log.info("Performing LDAP lookup ${request.getThreePid()} of type ${request.getType()}") 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 { try {
conn.bind(ldapCfg.getBindDn(), ldapCfg.getBindPassword()) bind(conn)
Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid()) Optional<String> mxid = lookup(conn, request.getType(), request.getThreePid())
if (mxid.isPresent()) { if (mxid.isPresent()) {
@@ -147,9 +173,9 @@ class LdapProvider implements IThreePidProvider {
log.info("Looking up {} mappings", mappings.size()) log.info("Looking up {} mappings", mappings.size())
List<ThreePidMapping> mappingsFound = new ArrayList<>() List<ThreePidMapping> mappingsFound = new ArrayList<>()
LdapConnection conn = new LdapNetworkConnection(ldapCfg.getHost(), ldapCfg.getPort()) LdapConnection conn = getConn()
try { try {
conn.bind(ldapCfg.getBindDn(), ldapCfg.getBindPassword()) bind(conn)
for (ThreePidMapping mapping : mappings) { for (ThreePidMapping mapping : mappings) {
try { try {