##// END OF EJS Templates
Reject LDAP authentication requests with blank password. Per RFC4513 these should be treated as anonymous binds. See the Security Considerations (Section 6.3.1) for more details on this issue.
Shawn K. O'Shea -
r1659:40db9e08 beta
parent child Browse files
Show More
@@ -1,151 +1,154 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 rhodecode.controllers.changelog
3 rhodecode.controllers.changelog
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 RhodeCode authentication library for LDAP
6 RhodeCode authentication library for LDAP
7
7
8 :created_on: Created on Nov 17, 2010
8 :created_on: Created on Nov 17, 2010
9 :author: marcink
9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
11 :license: GPLv3, see COPYING for more details.
12 """
12 """
13 # This program is free software: you can redistribute it and/or modify
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
16 # (at your option) any later version.
17 #
17 #
18 # This program is distributed in the hope that it will be useful,
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
21 # GNU General Public License for more details.
22 #
22 #
23 # You should have received a copy of the GNU General Public License
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
25
26 import logging
26 import logging
27
27
28 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
28 from rhodecode.lib.exceptions import LdapConnectionError, LdapUsernameError, \
29 LdapPasswordError
29 LdapPasswordError
30
30
31 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
32
32
33
33
34 try:
34 try:
35 import ldap
35 import ldap
36 except ImportError:
36 except ImportError:
37 # means that python-ldap is not installed
37 # means that python-ldap is not installed
38 pass
38 pass
39
39
40
40
41 class AuthLdap(object):
41 class AuthLdap(object):
42
42
43 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
43 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
44 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
44 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
45 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))',
45 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))',
46 search_scope='SUBTREE',
46 search_scope='SUBTREE',
47 attr_login='uid'):
47 attr_login='uid'):
48 self.ldap_version = ldap_version
48 self.ldap_version = ldap_version
49 ldap_server_type = 'ldap'
49 ldap_server_type = 'ldap'
50
50
51 self.TLS_KIND = tls_kind
51 self.TLS_KIND = tls_kind
52
52
53 if self.TLS_KIND == 'LDAPS':
53 if self.TLS_KIND == 'LDAPS':
54 port = port or 689
54 port = port or 689
55 ldap_server_type = ldap_server_type + 's'
55 ldap_server_type = ldap_server_type + 's'
56
56
57 OPT_X_TLS_DEMAND = 2
57 OPT_X_TLS_DEMAND = 2
58 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
58 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
59 OPT_X_TLS_DEMAND)
59 OPT_X_TLS_DEMAND)
60 self.LDAP_SERVER_ADDRESS = server
60 self.LDAP_SERVER_ADDRESS = server
61 self.LDAP_SERVER_PORT = port
61 self.LDAP_SERVER_PORT = port
62
62
63 #USE FOR READ ONLY BIND TO LDAP SERVER
63 #USE FOR READ ONLY BIND TO LDAP SERVER
64 self.LDAP_BIND_DN = bind_dn
64 self.LDAP_BIND_DN = bind_dn
65 self.LDAP_BIND_PASS = bind_pass
65 self.LDAP_BIND_PASS = bind_pass
66
66
67 self.LDAP_SERVER = "%s://%s:%s" % (ldap_server_type,
67 self.LDAP_SERVER = "%s://%s:%s" % (ldap_server_type,
68 self.LDAP_SERVER_ADDRESS,
68 self.LDAP_SERVER_ADDRESS,
69 self.LDAP_SERVER_PORT)
69 self.LDAP_SERVER_PORT)
70
70
71 self.BASE_DN = base_dn
71 self.BASE_DN = base_dn
72 self.LDAP_FILTER = ldap_filter
72 self.LDAP_FILTER = ldap_filter
73 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
73 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
74 self.attr_login = attr_login
74 self.attr_login = attr_login
75
75
76 def authenticate_ldap(self, username, password):
76 def authenticate_ldap(self, username, password):
77 """Authenticate a user via LDAP and return his/her LDAP properties.
77 """Authenticate a user via LDAP and return his/her LDAP properties.
78
78
79 Raises AuthenticationError if the credentials are rejected, or
79 Raises AuthenticationError if the credentials are rejected, or
80 EnvironmentError if the LDAP server can't be reached.
80 EnvironmentError if the LDAP server can't be reached.
81
81
82 :param username: username
82 :param username: username
83 :param password: password
83 :param password: password
84 """
84 """
85
85
86 from rhodecode.lib.helpers import chop_at
86 from rhodecode.lib.helpers import chop_at
87
87
88 uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS)
88 uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS)
89
89
90 if not password:
91 log.debug("Attempt to authenticate LDAP user with blank password rejected.")
92 raise LdapPasswordError()
90 if "," in username:
93 if "," in username:
91 raise LdapUsernameError("invalid character in username: ,")
94 raise LdapUsernameError("invalid character in username: ,")
92 try:
95 try:
93 if hasattr(ldap,'OPT_X_TLS_CACERTDIR'):
96 if hasattr(ldap,'OPT_X_TLS_CACERTDIR'):
94 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
97 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
95 '/etc/openldap/cacerts')
98 '/etc/openldap/cacerts')
96 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
99 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
97 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
100 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
98 ldap.set_option(ldap.OPT_TIMEOUT, 20)
101 ldap.set_option(ldap.OPT_TIMEOUT, 20)
99 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
102 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
100 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
103 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
101 if self.TLS_KIND != 'PLAIN':
104 if self.TLS_KIND != 'PLAIN':
102 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
105 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
103 server = ldap.initialize(self.LDAP_SERVER)
106 server = ldap.initialize(self.LDAP_SERVER)
104 if self.ldap_version == 2:
107 if self.ldap_version == 2:
105 server.protocol = ldap.VERSION2
108 server.protocol = ldap.VERSION2
106 else:
109 else:
107 server.protocol = ldap.VERSION3
110 server.protocol = ldap.VERSION3
108
111
109 if self.TLS_KIND == 'START_TLS':
112 if self.TLS_KIND == 'START_TLS':
110 server.start_tls_s()
113 server.start_tls_s()
111
114
112 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
115 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
113 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
116 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
114
117
115 filt = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login,
118 filt = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login,
116 username)
119 username)
117 log.debug("Authenticating %r filt %s at %s", self.BASE_DN,
120 log.debug("Authenticating %r filt %s at %s", self.BASE_DN,
118 filt, self.LDAP_SERVER)
121 filt, self.LDAP_SERVER)
119 lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE,
122 lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE,
120 filt)
123 filt)
121
124
122 if not lobjects:
125 if not lobjects:
123 raise ldap.NO_SUCH_OBJECT()
126 raise ldap.NO_SUCH_OBJECT()
124
127
125 for (dn, _attrs) in lobjects:
128 for (dn, _attrs) in lobjects:
126 if dn is None:
129 if dn is None:
127 continue
130 continue
128
131
129 try:
132 try:
130 server.simple_bind_s(dn, password)
133 server.simple_bind_s(dn, password)
131 attrs = server.search_ext_s(dn, ldap.SCOPE_BASE,
134 attrs = server.search_ext_s(dn, ldap.SCOPE_BASE,
132 '(objectClass=*)')[0][1]
135 '(objectClass=*)')[0][1]
133 break
136 break
134
137
135 except ldap.INVALID_CREDENTIALS, e:
138 except ldap.INVALID_CREDENTIALS, e:
136 log.debug("LDAP rejected password for user '%s' (%s): %s",
139 log.debug("LDAP rejected password for user '%s' (%s): %s",
137 uid, username, dn)
140 uid, username, dn)
138
141
139 else:
142 else:
140 log.debug("No matching LDAP objects for authentication "
143 log.debug("No matching LDAP objects for authentication "
141 "of '%s' (%s)", uid, username)
144 "of '%s' (%s)", uid, username)
142 raise LdapPasswordError()
145 raise LdapPasswordError()
143
146
144 except ldap.NO_SUCH_OBJECT, e:
147 except ldap.NO_SUCH_OBJECT, e:
145 log.debug("LDAP says no such user '%s' (%s)", uid, username)
148 log.debug("LDAP says no such user '%s' (%s)", uid, username)
146 raise LdapUsernameError()
149 raise LdapUsernameError()
147 except ldap.SERVER_DOWN, e:
150 except ldap.SERVER_DOWN, e:
148 raise LdapConnectionError("LDAP can't access "
151 raise LdapConnectionError("LDAP can't access "
149 "authentication server")
152 "authentication server")
150
153
151 return (dn, attrs)
154 return (dn, attrs)
General Comments 0
You need to be logged in to leave comments. Login now