Add support for LDAP over TLS.
parent
336276be98
commit
e21da43fd6
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -9,4 +9,4 @@ spec:
|
|||
app: ldap
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 389
|
||||
port: 636
|
||||
|
|
8
go.sum
8
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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"`
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue