Add support for LDAP over TLS.

pull/483/head
Clement Michaud 2019-12-06 09:15:54 +01:00 committed by Clément Michaud
parent 336276be98
commit e21da43fd6
17 changed files with 425 additions and 57 deletions

View File

@ -54,9 +54,10 @@ authentication_backend:
# than one instance and therefore is recommended for
# production.
ldap:
# The url of the ldap server
# The url to the ldap server. Scheme can be ldap:// or ldaps://
url: ldap://127.0.0.1
# Skip verifying the server certificate (to allow self-signed certificate).
skip_verify: false
# The base dn for every entries
base_dn: dc=example,dc=com

View File

@ -1,20 +1,20 @@
version: '3'
version: "3"
services:
openldap:
image: clems4ever/openldap
image: osixia/openldap:1.3.0
hostname: ldap.example.com
environment:
- SLAPD_ORGANISATION=MyCompany
- SLAPD_DOMAIN=example.com
- SLAPD_PASSWORD=password
- SLAPD_CONFIG_PASSWORD=password
- SLAPD_ADDITIONAL_MODULES=memberof
- SLAPD_ADDITIONAL_SCHEMAS=openldap
- SLAPD_FORCE_RECONFIGURE=true
- LDAP_ORGANISATION=MyCompany
- LDAP_DOMAIN=example.com
- LDAP_ADMIN_PASSWORD=password
- LDAP_CONFIG_PASSWORD=password
- LDAP_ADDITIONAL_MODULES=memberof
- LDAP_ADDITIONAL_SCHEMAS=openldap
- LDAP_FORCE_RECONFIGURE=true
- LDAP_TLS_VERIFY_CLIENT=try
volumes:
- ./example/compose/ldap/base.ldif:/etc/ldap.dist/prepopulate/base.ldif
- ./example/compose/ldap/access.rules:/etc/ldap.dist/prepopulate/access.rules
ports:
- "389:389"
- ./example/compose/ldap/ldif:/container/service/slapd/assets/config/bootstrap/ldif/custom
command:
- --copy-service
networks:
- authelianet

View File

@ -10,7 +10,8 @@ default_redirection_url: https://home.example.com:8080
authentication_backend:
ldap:
url: ldap-service:389
url: ldaps://ldap-service
skip_verify: true
base_dn: dc=example,dc=com
additional_users_dn: ou=users
users_filter: (cn={0})

View File

@ -18,27 +18,35 @@ spec:
spec:
containers:
- name: ldap
image: clems4ever/authelia-test-ldap
image: osixia/openldap:1.3.0
ports:
- containerPort: 389
- containerPort: 636
args: ["--copy-service", "--loglevel", "debug"]
env:
- name: SLAPD_ORGANISATION
- name: LDAP_ORGANISATION
value: MyCompany
- name: SLAPD_DOMAIN
- name: LDAP_DOMAIN
value: example.com
- name: SLAPD_PASSWORD
- name: LDAP_ADMIN_PASSWORD
value: password
- name: SLAPD_CONFIG_PASSWORD
- name: LDAP_CONFIG_PASSWORD
value: password
- name: SLAPD_ADDITIONAL_MODULES
- name: LDAP_ADDITIONAL_MODULES
value: memberof
- name: SLAPD_ADDITIONAL_SCHEMAS
- name: LDAP_ADDITIONAL_SCHEMAS
value: openldap
- name: SLAPD_FORCE_RECONFIGURE
- name: LDAP_FORCE_RECONFIGURE
value: "true"
- name: LDAP_TLS_VERIFY_CLIENT
value: try
volumeMounts:
- name: config-volume
mountPath: /etc/ldap.dist/prepopulate
mountPath: /container/service/slapd/assets/config/bootstrap/ldif/custom/base.ldif
subPath: base.ldif
- name: config-volume
mountPath: /container/service/slapd/assets/config/bootstrap/ldif/custom/access.rules
subPath: access.rules
volumes:
- name: config-volume
configMap:

View File

@ -9,4 +9,4 @@ spec:
app: ldap
ports:
- protocol: TCP
port: 389
port: 636

8
go.sum
View File

@ -1,6 +1,7 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
cloud.google.com/go v0.41.0 h1:NFvqUTDnSNYPX5oReekmB+D+90jrJIcVImxQ3qrBVgM=
cloud.google.com/go v0.41.0/go.mod h1:OauMR7DV8fzvZIl2qg6rkaIhD/vmgk4iwEw/h6ercmg=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc=
@ -76,8 +77,10 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
@ -179,6 +182,7 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
go.mongodb.org/mongo-driver v1.1.3 h1:++7u8r9adKhGR+I79NfEtYrk2ktjenErXM99PSufIoI=
go.mongodb.org/mongo-driver v1.1.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0 h1:C9hSCOW830chIVkdja34wa6Ky+IzWllkUinR+BtRZd4=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
golang.org/x/crypto v0.0.0-20181112202954-3d3f9f413869/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
@ -210,6 +214,7 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -250,6 +255,7 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190624190245-7f2218787638 h1:uIfBkD8gLczr4XDgYpt/qJYds2YJwZRNw4zs7wSnNhk=
golang.org/x/tools v0.0.0-20190624190245-7f2218787638/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0 h1:9sdfJOzWlkqPltHAuzT2Cp+yrBeY1KRVYgms8soxMwM=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@ -263,9 +269,11 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63 h1:UsSJe9fhWNSz6emfIGPpH5DF23t7ALo2Pf3sC+/hsdg=
google.golang.org/genproto v0.0.0-20190626174449-989357319d63/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d h1:TxyelI5cVkbREznMhfzycHdkp5cLA7DpE+GKjSslYhM=
gopkg.in/asn1-ber.v1 v1.0.0-20181015200546-f715ec2f112d/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=

View File

@ -0,0 +1,78 @@
package authentication
import (
"crypto/tls"
"gopkg.in/ldap.v3"
)
// ********************* CONNECTION *********************
// LDAPConnection interface representing a connection to the ldap.
type LDAPConnection interface {
Bind(username, password string) error
Close()
Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error)
Modify(modifyRequest *ldap.ModifyRequest) error
}
// LDAPConnectionImpl the production implementation of an ldap connection
type LDAPConnectionImpl struct {
conn *ldap.Conn
}
// NewLDAPConnectionImpl create a new ldap connection
func NewLDAPConnectionImpl(conn *ldap.Conn) *LDAPConnectionImpl {
return &LDAPConnectionImpl{conn}
}
func (lc *LDAPConnectionImpl) Bind(username, password string) error {
return lc.conn.Bind(username, password)
}
func (lc *LDAPConnectionImpl) Close() {
lc.conn.Close()
}
func (lc *LDAPConnectionImpl) Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
return lc.conn.Search(searchRequest)
}
func (lc *LDAPConnectionImpl) Modify(modifyRequest *ldap.ModifyRequest) error {
return lc.conn.Modify(modifyRequest)
}
// ********************* FACTORY ***********************
// LDAPConnectionFactory an interface of factory of ldap connections
type LDAPConnectionFactory interface {
DialTLS(network, addr string, config *tls.Config) (LDAPConnection, error)
Dial(network, addr string) (LDAPConnection, error)
}
// LDAPConnectionFactoryImpl the production implementation of an ldap connection factory.
type LDAPConnectionFactoryImpl struct{}
// NewLDAPConnectionFactoryImpl create a concrete ldap connection factory
func NewLDAPConnectionFactoryImpl() *LDAPConnectionFactoryImpl {
return &LDAPConnectionFactoryImpl{}
}
// DialTLS contact ldap server over TLS.
func (lcf *LDAPConnectionFactoryImpl) DialTLS(network, addr string, config *tls.Config) (LDAPConnection, error) {
conn, err := ldap.DialTLS(network, addr, config)
if err != nil {
return nil, err
}
return NewLDAPConnectionImpl(conn), nil
}
// Dial contact ldap server over raw tcp.
func (lcf *LDAPConnectionFactoryImpl) Dial(network, addr string) (LDAPConnection, error) {
conn, err := ldap.Dial(network, addr)
if err != nil {
return nil, err
}
return NewLDAPConnectionImpl(conn), nil
}

View File

@ -0,0 +1,143 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: internal/authentication/ldap_connection_factory.go
// Package authentication is a generated GoMock package.
package authentication
import (
tls "crypto/tls"
gomock "github.com/golang/mock/gomock"
ldap_v3 "gopkg.in/ldap.v3"
reflect "reflect"
)
// MockLDAPConnection is a mock of LDAPConnection interface
type MockLDAPConnection struct {
ctrl *gomock.Controller
recorder *MockLDAPConnectionMockRecorder
}
// MockLDAPConnectionMockRecorder is the mock recorder for MockLDAPConnection
type MockLDAPConnectionMockRecorder struct {
mock *MockLDAPConnection
}
// NewMockLDAPConnection creates a new mock instance
func NewMockLDAPConnection(ctrl *gomock.Controller) *MockLDAPConnection {
mock := &MockLDAPConnection{ctrl: ctrl}
mock.recorder = &MockLDAPConnectionMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLDAPConnection) EXPECT() *MockLDAPConnectionMockRecorder {
return m.recorder
}
// Bind mocks base method
func (m *MockLDAPConnection) Bind(username, password string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bind", username, password)
ret0, _ := ret[0].(error)
return ret0
}
// Bind indicates an expected call of Bind
func (mr *MockLDAPConnectionMockRecorder) Bind(username, password interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bind", reflect.TypeOf((*MockLDAPConnection)(nil).Bind), username, password)
}
// Close mocks base method
func (m *MockLDAPConnection) Close() {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Close")
}
// Close indicates an expected call of Close
func (mr *MockLDAPConnectionMockRecorder) Close() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockLDAPConnection)(nil).Close))
}
// Search mocks base method
func (m *MockLDAPConnection) Search(searchRequest *ldap_v3.SearchRequest) (*ldap_v3.SearchResult, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Search", searchRequest)
ret0, _ := ret[0].(*ldap_v3.SearchResult)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Search indicates an expected call of Search
func (mr *MockLDAPConnectionMockRecorder) Search(searchRequest interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockLDAPConnection)(nil).Search), searchRequest)
}
// Modify mocks base method
func (m *MockLDAPConnection) Modify(modifyRequest *ldap_v3.ModifyRequest) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Modify", modifyRequest)
ret0, _ := ret[0].(error)
return ret0
}
// Modify indicates an expected call of Modify
func (mr *MockLDAPConnectionMockRecorder) Modify(modifyRequest interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Modify", reflect.TypeOf((*MockLDAPConnection)(nil).Modify), modifyRequest)
}
// MockLDAPConnectionFactory is a mock of LDAPConnectionFactory interface
type MockLDAPConnectionFactory struct {
ctrl *gomock.Controller
recorder *MockLDAPConnectionFactoryMockRecorder
}
// MockLDAPConnectionFactoryMockRecorder is the mock recorder for MockLDAPConnectionFactory
type MockLDAPConnectionFactoryMockRecorder struct {
mock *MockLDAPConnectionFactory
}
// NewMockLDAPConnectionFactory creates a new mock instance
func NewMockLDAPConnectionFactory(ctrl *gomock.Controller) *MockLDAPConnectionFactory {
mock := &MockLDAPConnectionFactory{ctrl: ctrl}
mock.recorder = &MockLDAPConnectionFactoryMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockLDAPConnectionFactory) EXPECT() *MockLDAPConnectionFactoryMockRecorder {
return m.recorder
}
// DialTLS mocks base method
func (m *MockLDAPConnectionFactory) DialTLS(network, addr string, config *tls.Config) (LDAPConnection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "DialTLS", network, addr, config)
ret0, _ := ret[0].(LDAPConnection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// DialTLS indicates an expected call of DialTLS
func (mr *MockLDAPConnectionFactoryMockRecorder) DialTLS(network, addr, config interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialTLS", reflect.TypeOf((*MockLDAPConnectionFactory)(nil).DialTLS), network, addr, config)
}
// Dial mocks base method
func (m *MockLDAPConnectionFactory) Dial(network, addr string) (LDAPConnection, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Dial", network, addr)
ret0, _ := ret[0].(LDAPConnection)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Dial indicates an expected call of Dial
func (mr *MockLDAPConnectionFactoryMockRecorder) Dial(network, addr interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dial", reflect.TypeOf((*MockLDAPConnectionFactory)(nil).Dial), network, addr)
}

View File

@ -1,35 +1,70 @@
package authentication
import (
"crypto/tls"
"fmt"
"net/url"
"strings"
"github.com/clems4ever/authelia/internal/configuration/schema"
"github.com/clems4ever/authelia/internal/logging"
"gopkg.in/ldap.v3"
)
// LDAPUserProvider is a provider using a LDAP or AD as a user database.
type LDAPUserProvider struct {
configuration schema.LDAPAuthenticationBackendConfiguration
}
func (p *LDAPUserProvider) connect(userDN string, password string) (*ldap.Conn, error) {
conn, err := ldap.Dial("tcp", p.configuration.URL)
if err != nil {
return nil, err
}
err = conn.Bind(userDN, password)
if err != nil {
return nil, err
}
return conn, nil
connectionFactory LDAPConnectionFactory
}
// NewLDAPUserProvider creates a new instance of LDAPUserProvider.
func NewLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfiguration) *LDAPUserProvider {
return &LDAPUserProvider{configuration}
return &LDAPUserProvider{
configuration: configuration,
connectionFactory: NewLDAPConnectionFactoryImpl(),
}
}
func NewLDAPUserProviderWithFactory(configuration schema.LDAPAuthenticationBackendConfiguration,
connectionFactory LDAPConnectionFactory) *LDAPUserProvider {
return &LDAPUserProvider{
configuration: configuration,
connectionFactory: connectionFactory,
}
}
func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnection, error) {
var newConnection LDAPConnection
url, err := url.Parse(p.configuration.URL)
if err != nil {
return nil, fmt.Errorf("Unable to parse URL to LDAP: %s", url)
}
if url.Scheme == "ldaps" {
logging.Logger().Debug("LDAP client starts a TLS session")
conn, err := p.connectionFactory.DialTLS("tcp", url.Host, &tls.Config{
InsecureSkipVerify: p.configuration.SkipVerify,
})
if err != nil {
return nil, err
}
newConnection = conn
} else {
logging.Logger().Debug("LDAP client starts a session over raw TCP")
conn, err := p.connectionFactory.Dial("tcp", url.Host)
if err != nil {
return nil, err
}
newConnection = conn
}
if err := newConnection.Bind(userDN, password); err != nil {
return nil, err
}
return newConnection, nil
}
// CheckUserPassword checks if provided password matches for the given user.
@ -54,7 +89,7 @@ func (p *LDAPUserProvider) CheckUserPassword(username string, password string) (
return true, nil
}
func (p *LDAPUserProvider) getUserAttribute(conn *ldap.Conn, username string, attribute string) ([]string, error) {
func (p *LDAPUserProvider) getUserAttribute(conn LDAPConnection, username string, attribute string) ([]string, error) {
client, err := p.connect(p.configuration.User, p.configuration.Password)
if err != nil {
return nil, err
@ -86,7 +121,7 @@ func (p *LDAPUserProvider) getUserAttribute(conn *ldap.Conn, username string, at
return sr.Entries[0].Attributes[0].Values, nil
}
func (p *LDAPUserProvider) getUserDN(conn *ldap.Conn, username string) (string, error) {
func (p *LDAPUserProvider) getUserDN(conn LDAPConnection, username string) (string, error) {
values, err := p.getUserAttribute(conn, username, "dn")
if err != nil {
@ -100,7 +135,7 @@ func (p *LDAPUserProvider) getUserDN(conn *ldap.Conn, username string) (string,
return values[0], nil
}
func (p *LDAPUserProvider) getUserUID(conn *ldap.Conn, username string) (string, error) {
func (p *LDAPUserProvider) getUserUID(conn LDAPConnection, username string) (string, error) {
values, err := p.getUserAttribute(conn, username, "uid")
if err != nil {
@ -114,7 +149,7 @@ func (p *LDAPUserProvider) getUserUID(conn *ldap.Conn, username string) (string,
return values[0], nil
}
func (p *LDAPUserProvider) createGroupsFilter(conn *ldap.Conn, username string) (string, error) {
func (p *LDAPUserProvider) createGroupsFilter(conn LDAPConnection, username string) (string, error) {
if strings.Index(p.configuration.GroupsFilter, "{0}") >= 0 {
return strings.Replace(p.configuration.GroupsFilter, "{0}", username, -1), nil
} else if strings.Index(p.configuration.GroupsFilter, "{dn}") >= 0 {

View File

@ -0,0 +1,57 @@
package authentication
import (
"testing"
"github.com/clems4ever/authelia/internal/configuration/schema"
gomock "github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
)
func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
}, mockFactory)
mockFactory.EXPECT().
Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
_, err := ldap.connect("cn=admin,dc=example,dc=com", "password")
require.NoError(t, err)
}
func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
}, mockFactory)
mockFactory.EXPECT().
DialTLS(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389"), gomock.Any()).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
_, err := ldap.connect("cn=admin,dc=example,dc=com", "password")
require.NoError(t, err)
}

View File

@ -3,6 +3,7 @@ package schema
// LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server.
type LDAPAuthenticationBackendConfiguration struct {
URL string `yaml:"url"`
SkipVerify bool `yaml:"skip_verify"`
BaseDN string `yaml:"base_dn"`
AdditionalUsersDN string `yaml:"additional_users_dn"`
UsersFilter string `yaml:"users_filter"`

View File

@ -2,12 +2,14 @@ package validator
import (
"errors"
"strings"
"fmt"
"net/url"
"github.com/clems4ever/authelia/internal/configuration/schema"
)
var ldapProtocolPrefix = "ldap://"
var ldapsProtocolPrefix = "ldaps://"
func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) {
if configuration.Path == "" {
@ -15,19 +17,30 @@ func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationB
}
}
func validateLdapURL(url string, validator *schema.StructValidator) string {
if strings.HasPrefix(url, ldapProtocolPrefix) {
url = url[len(ldapProtocolPrefix):]
func validateLdapURL(ldapURL string, validator *schema.StructValidator) string {
u, err := url.Parse(ldapURL)
if err != nil {
validator.Push(errors.New("Unable to parse URL to ldap server. The scheme is probably missing: ldap:// or ldaps://"))
return ""
}
portColons := strings.Index(url, ":")
// if no port is provided, we provide the default LDAP port
// TODO(c.michaud): support LDAP over TLS.
if portColons == -1 {
url = url + ":389"
if !(u.Scheme == "ldap" || u.Scheme == "ldaps") {
validator.Push(errors.New("Unknown scheme for ldap url, should be ldap:// or ldaps://"))
return ""
}
return url
if u.Scheme == "ldap" && u.Port() == "" {
u.Host += ":389"
} else if u.Scheme == "ldaps" && u.Port() == "" {
u.Host += ":636"
}
if !u.IsAbs() {
validator.Push(fmt.Errorf("URL to LDAP %s is still not absolute, it should be something like ldap://127.0.0.1:389", u.String()))
}
return u.String()
}
func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {

View File

@ -5,6 +5,7 @@ import (
"github.com/clems4ever/authelia/internal/configuration/schema"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)
@ -119,6 +120,24 @@ func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute()
assert.Equal(suite.T(), "mail", suite.configuration.Ldap.MailAttribute)
}
func (suite *LdapAuthenticationBackendSuite) TestShouldAdaptLDAPURL() {
assert.Equal(suite.T(), "", validateLdapURL("127.0.0.1", suite.validator))
require.Len(suite.T(), suite.validator.Errors(), 1)
assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unknown scheme for ldap url, should be ldap:// or ldaps://")
assert.Equal(suite.T(), "", validateLdapURL("127.0.0.1:636", suite.validator))
require.Len(suite.T(), suite.validator.Errors(), 2)
assert.EqualError(suite.T(), suite.validator.Errors()[1], "Unable to parse URL to ldap server. The scheme is probably missing: ldap:// or ldaps://")
assert.Equal(suite.T(), "ldap://127.0.0.1:389", validateLdapURL("ldap://127.0.0.1", suite.validator))
assert.Equal(suite.T(), "ldap://127.0.0.1:390", validateLdapURL("ldap://127.0.0.1:390", suite.validator))
assert.Equal(suite.T(), "ldap://127.0.0.1:389/abc", validateLdapURL("ldap://127.0.0.1/abc", suite.validator))
assert.Equal(suite.T(), "ldap://127.0.0.1:389/abc?test=abc&x=y", validateLdapURL("ldap://127.0.0.1/abc?test=abc&x=y", suite.validator))
assert.Equal(suite.T(), "ldaps://127.0.0.1:390", validateLdapURL("ldaps://127.0.0.1:390", suite.validator))
assert.Equal(suite.T(), "ldaps://127.0.0.1:636", validateLdapURL("ldaps://127.0.0.1", suite.validator))
}
func TestLdapAuthenticationBackend(t *testing.T) {
suite.Run(t, new(LdapAuthenticationBackendSuite))
}

View File

@ -13,7 +13,10 @@ jwt_secret: very_important_secret
authentication_backend:
ldap:
# The url of the ldap server
url: ldap://openldap
url: ldaps://openldap
# Skip certificate verification (for self-signed certificates)
skip_verify: true
# The base dn for every entries
base_dn: dc=example,dc=com

View File

@ -17,6 +17,7 @@ func init() {
"example/compose/nginx/portal/docker-compose.yml",
"example/compose/smtp/docker-compose.yml",
"example/compose/ldap/docker-compose.yml",
"example/compose/ldap/docker-compose.admin.yml",
})
setup := func(suitePath string) error {