##// END OF EJS Templates
Improve LDAP authentication...
Thayne Harbaugh -
r991:b232a36c issue-108
parent child Browse files
Show More
@@ -48,15 +48,33 b' log = logging.getLogger(__name__)'
48 48
49 49 class LdapSettingsController(BaseController):
50 50
51 search_scope_choices = [('BASE', _('BASE'),),
52 ('ONELEVEL', _('ONELEVEL'),),
53 ('SUBTREE', _('SUBTREE'),),
54 ]
55 search_scope_default = 'SUBTREE'
56
57 tls_reqcert_choices = [('NEVER', _('NEVER'),),
58 ('ALLOW', _('ALLOW'),),
59 ('TRY', _('TRY'),),
60 ('DEMAND', _('DEMAND'),),
61 ('HARD', _('HARD'),),
62 ]
63 tls_reqcert_default = 'DEMAND'
64
51 65 @LoginRequired()
52 66 @HasPermissionAllDecorator('hg.admin')
53 67 def __before__(self):
54 68 c.admin_user = session.get('admin_user')
55 69 c.admin_username = session.get('admin_username')
70 c.search_scope_choices = self.search_scope_choices
71 c.tls_reqcert_choices = self.tls_reqcert_choices
56 72 super(LdapSettingsController, self).__before__()
57 73
58 74 def index(self):
59 75 defaults = SettingsModel().get_ldap_settings()
76 c.search_scope_cur = defaults.get('ldap_search_scope')
77 c.tls_reqcert_cur = defaults.get('ldap_tls_reqcert')
60 78
61 79 return htmlfill.render(
62 80 render('admin/ldap/ldap.html'),
@@ -68,7 +86,8 b' class LdapSettingsController(BaseControl'
68 86 """POST ldap create and store ldap settings"""
69 87
70 88 settings_model = SettingsModel()
71 _form = LdapSettingsForm()()
89 _form = LdapSettingsForm([x[0] for x in self.tls_reqcert_choices],
90 [x[0] for x in self.search_scope_choices])()
72 91
73 92 try:
74 93 form_result = _form.to_python(dict(request.POST))
@@ -91,6 +110,9 b' class LdapSettingsController(BaseControl'
91 110
92 111 except formencode.Invalid, errors:
93 112
113 c.search_scope_cur = self.search_scope_default
114 c.tls_reqcert_cur = self.search_scope_default
115
94 116 return htmlfill.render(
95 117 render('admin/ldap/ldap.html'),
96 118 defaults=errors.value,
@@ -103,7 +103,7 b' def authenticate(username, password):'
103 103 user = user_model.get_by_username(username, cache=False)
104 104
105 105 log.debug('Authenticating user using RhodeCode account')
106 if user is not None and user.is_ldap is False:
106 if user is not None and not user.ldap_dn:
107 107 if user.active:
108 108
109 109 if user.username == 'default' and user.active:
@@ -122,7 +122,7 b' def authenticate(username, password):'
122 122 user_obj = user_model.get_by_username(username, cache=False,
123 123 case_insensitive=True)
124 124
125 if user_obj is not None and user_obj.is_ldap is False:
125 if user_obj is not None and not user_obj.ldap_dn:
126 126 log.debug('this user already exists as non ldap')
127 127 return False
128 128
@@ -141,15 +141,25 b' def authenticate(username, password):'
141 141 'bind_dn':ldap_settings.get('ldap_dn_user'),
142 142 'bind_pass':ldap_settings.get('ldap_dn_pass'),
143 143 'use_ldaps':ldap_settings.get('ldap_ldaps'),
144 'tls_reqcert':ldap_settings.get('ldap_tls_reqcert'),
145 'ldap_filter':ldap_settings.get('ldap_filter'),
146 'search_scope':ldap_settings.get('ldap_search_scope'),
147 'attr_login':ldap_settings.get('ldap_attr_login'),
144 148 'ldap_version':3,
145 149 }
146 150 log.debug('Checking for ldap authentication')
147 151 try:
148 152 aldap = AuthLdap(**kwargs)
149 res = aldap.authenticate_ldap(username, password)
150 log.debug('Got ldap response %s', res)
153 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
154 log.debug('Got ldap DN response %s', user_dn)
151 155
152 if user_model.create_ldap(username, password):
156 user_attrs = {
157 'name' : ldap_attrs[ldap_settings.get('ldap_attr_firstname')][0],
158 'lastname' : ldap_attrs[ldap_settings.get('ldap_attr_lastname')][0],
159 'email' : ldap_attrs[ldap_settings.get('ldap_attr_email')][0],
160 }
161
162 if user_model.create_ldap(username, password, user_dn, user_attrs):
153 163 log.info('created new ldap user')
154 164
155 165 return True
@@ -36,11 +36,15 b' except ImportError:'
36 36 class AuthLdap(object):
37 37
38 38 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
39 use_ldaps=False, ldap_version=3):
39 use_ldaps=False, tls_reqcert='DEMAND', ldap_version=3,
40 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))',
41 search_scope='SUBTREE',
42 attr_login='uid'):
40 43 self.ldap_version = ldap_version
41 44 if use_ldaps:
42 45 port = port or 689
43 46 self.LDAP_USE_LDAPS = use_ldaps
47 self.TLS_REQCERT = ldap.__dict__['OPT_X_TLS_' + tls_reqcert]
44 48 self.LDAP_SERVER_ADDRESS = server
45 49 self.LDAP_SERVER_PORT = port
46 50
@@ -55,6 +59,10 b' class AuthLdap(object):'
55 59 self.LDAP_SERVER_PORT)
56 60
57 61 self.BASE_DN = base_dn
62 self.LDAP_FILTER = ldap_filter
63 self.SEARCH_SCOPE = ldap.__dict__['SCOPE_' + search_scope]
64 self.attr_login = attr_login
65
58 66
59 67 def authenticate_ldap(self, username, password):
60 68 """Authenticate a user via LDAP and return his/her LDAP properties.
@@ -74,7 +82,13 b' class AuthLdap(object):'
74 82 raise LdapUsernameError("invalid character in username: ,")
75 83 try:
76 84 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR, '/etc/openldap/cacerts')
85 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
86 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
87 ldap.set_option(ldap.OPT_TIMEOUT, 20)
77 88 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
89 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
90 if self.LDAP_USE_LDAPS:
91 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
78 92 server = ldap.initialize(self.LDAP_SERVER)
79 93 if self.ldap_version == 2:
80 94 server.protocol = ldap.VERSION2
@@ -84,21 +98,29 b' class AuthLdap(object):'
84 98 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
85 99 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
86 100
87 dn = self.BASE_DN % {'user':uid}
88 log.debug("Authenticating %r at %s", dn, self.LDAP_SERVER)
89 server.simple_bind_s(dn, password)
101 filt = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login, username)
102 log.debug("Authenticating %r filt %s at %s", self.BASE_DN, filt, self.LDAP_SERVER)
103 lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE, filt)
104
105 if not lobjects:
106 raise ldap.NO_SUCH_OBJECT()
90 107
91 properties = server.search_s(dn, ldap.SCOPE_SUBTREE)
92 if not properties:
93 raise ldap.NO_SUCH_OBJECT()
108 for (dn, attrs) in lobjects:
109 try:
110 server.simple_bind_s(dn, password)
111 break
112
113 except ldap.INVALID_CREDENTIALS, e:
114 log.debug("LDAP rejected password for user '%s' (%s): %s", uid, username, dn)
115
116 else:
117 log.debug("No matching LDAP objecs for authentication of '%s' (%s)", uid, username)
118 raise LdapPasswordError()
119
94 120 except ldap.NO_SUCH_OBJECT, e:
95 121 log.debug("LDAP says no such user '%s' (%s)", uid, username)
96 122 raise LdapUsernameError()
97 except ldap.INVALID_CREDENTIALS, e:
98 log.debug("LDAP rejected password for user '%s' (%s)", uid, username)
99 raise LdapPasswordError()
100 123 except ldap.SERVER_DOWN, e:
101 124 raise LdapConnectionError("LDAP can't access authentication server")
102 125
103 return properties[0]
104
126 return (dn, attrs)
@@ -336,7 +336,10 b' class DbManage(object):'
336 336
337 337 try:
338 338 for k in ['ldap_active', 'ldap_host', 'ldap_port', 'ldap_ldaps',
339 'ldap_dn_user', 'ldap_dn_pass', 'ldap_base_dn']:
339 'ldap_tls_reqcert', 'ldap_dn_user', 'ldap_dn_pass',
340 'ldap_base_dn', 'ldap_filter', 'ldap_search_scope',
341 'ldap_attr_login', 'ldap_attr_firstname', 'ldap_attr_lastname',
342 'ldap_attr_email']:
340 343
341 344 setting = RhodeCodeSettings(k, '')
342 345 self.sa.add(setting)
@@ -105,7 +105,7 b' class User(Base, BaseModel):'
105 105 lastname = Column("lastname", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
106 106 email = Column("email", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
107 107 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
108 is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False)
108 ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
109 109
110 110 user_log = relationship('UserLog', cascade='all')
111 111 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
@@ -334,23 +334,14 b' class LdapLibValidator(formencode.valida'
334 334 raise LdapImportError
335 335 return value
336 336
337 class BaseDnValidator(formencode.validators.FancyValidator):
337 class AttrLoginValidator(formencode.validators.FancyValidator):
338 338
339 339 def to_python(self, value, state):
340 340
341 try:
342 value % {'user':'valid'}
343
344 if value.find('%(user)s') == -1:
345 raise formencode.Invalid(_("You need to specify %(user)s in "
346 "template for example uid=%(user)s "
347 ",dc=company...") ,
348 value, state)
349
350 except KeyError:
351 raise formencode.Invalid(_("Wrong template used, only %(user)s "
352 "is an valid entry") ,
353 value, state)
341 if not value or not isinstance(value, (str, unicode)):
342 raise formencode.Invalid(_("The LDAP Login attribute of the CN must be specified "
343 "- this is the name of the attribute that is equivalent to 'username'"),
344 value, state)
354 345
355 346 return value
356 347
@@ -521,7 +512,7 b' def DefaultPermissionsForm(perms_choices'
521 512 return _DefaultPermissionsForm
522 513
523 514
524 def LdapSettingsForm():
515 def LdapSettingsForm(tls_reqcert_choices, search_scope_choices):
525 516 class _LdapSettingsForm(formencode.Schema):
526 517 allow_extra_fields = True
527 518 filter_extra_fields = True
@@ -530,8 +521,15 b' def LdapSettingsForm():'
530 521 ldap_host = UnicodeString(strip=True,)
531 522 ldap_port = Number(strip=True,)
532 523 ldap_ldaps = StringBoolean(if_missing=False)
524 ldap_tls_reqcert = OneOf(tls_reqcert_choices)
533 525 ldap_dn_user = UnicodeString(strip=True,)
534 526 ldap_dn_pass = UnicodeString(strip=True,)
535 ldap_base_dn = All(BaseDnValidator, UnicodeString(strip=True,))
527 ldap_base_dn = UnicodeString(strip=True,)
528 ldap_filter = UnicodeString(strip=True,)
529 ldap_search_scope = OneOf(search_scope_choices)
530 ldap_attr_login = All(AttrLoginValidator, UnicodeString(strip=True,))
531 ldap_attr_firstname = UnicodeString(strip=True,)
532 ldap_attr_lastname = UnicodeString(strip=True,)
533 ldap_attr_email = UnicodeString(strip=True,)
536 534
537 535 return _LdapSettingsForm
@@ -72,10 +72,18 b' class SettingsModel(BaseModel):'
72 72 ldap_host
73 73 ldap_port
74 74 ldap_ldaps
75 ldap_tls_reqcert
75 76 ldap_dn_user
76 77 ldap_dn_pass
77 78 ldap_base_dn
79 ldap_filter
80 ldap_search_scope
81 ldap_attr_login
82 ldap_attr_firstname
83 ldap_attr_lastname
84 ldap_attr_email
78 85 """
86 # ldap_search_scope
79 87
80 88 r = self.sa.query(RhodeCodeSettings)\
81 89 .filter(RhodeCodeSettings.app_settings_name\
@@ -75,25 +75,27 b' class UserModel(BaseModel):'
75 75 self.sa.rollback()
76 76 raise
77 77
78 def create_ldap(self, username, password):
78 def create_ldap(self, username, password, user_dn, attrs):
79 79 """
80 80 Checks if user is in database, if not creates this user marked
81 81 as ldap user
82 82 :param username:
83 83 :param password:
84 :param user_dn:
85 :param attrs:
84 86 """
85 87 from rhodecode.lib.auth import get_crypt_password
86 88 log.debug('Checking for such ldap account in RhodeCode database')
87 89 if self.get_by_username(username, case_insensitive=True) is None:
88 90 try:
89 91 new_user = User()
90 new_user.username = username.lower()#add ldap account always lowercase
92 new_user.username = username.lower() # add ldap account always lowercase
91 93 new_user.password = get_crypt_password(password)
92 new_user.email = '%s@ldap.server' % username
94 new_user.email = attrs['email']
93 95 new_user.active = True
94 new_user.is_ldap = True
95 new_user.name = '%s@ldap' % username
96 new_user.lastname = ''
96 new_user.ldap_dn = user_dn
97 new_user.name = attrs['name']
98 new_user.lastname = attrs['lastname']
97 99
98 100
99 101 self.sa.add(new_user)
@@ -21,13 +21,13 b''
21 21 <div class="title">
22 22 ${self.breadcrumbs()}
23 23 </div>
24 <h3>${_('LDAP administration')}</h3>
25 24 ${h.form(url('ldap_settings'))}
26 25 <div class="form">
27 26 <div class="fields">
28 27
28 <h3>${_('Connection settings')}</h3>
29 29 <div class="field">
30 <div class="label label-checkbox"><label for="ldap_active">${_('Enable ldap')}</label></div>
30 <div class="label label-checkbox"><label for="ldap_active">${_('Enable LDAP')}</label></div>
31 31 <div class="checkboxes"><div class="checkbox">${h.checkbox('ldap_active',True,class_='small')}</div></div>
32 32 </div>
33 33 <div class="field">
@@ -39,10 +39,6 b''
39 39 <div class="input">${h.text('ldap_port',class_='small')}</div>
40 40 </div>
41 41 <div class="field">
42 <div class="label label-checkbox"><label for="ldap_ldaps">${_('Enable LDAPS')}</label></div>
43 <div class="checkboxes"><div class="checkbox">${h.checkbox('ldap_ldaps',True,class_='small')}</div></div>
44 </div>
45 <div class="field">
46 42 <div class="label"><label for="ldap_dn_user">${_('Account')}</label></div>
47 43 <div class="input">${h.text('ldap_dn_user',class_='small')}</div>
48 44 </div>
@@ -51,9 +47,43 b''
51 47 <div class="input">${h.password('ldap_dn_pass',class_='small')}</div>
52 48 </div>
53 49 <div class="field">
50 <div class="label label-checkbox"><label for="ldap_ldaps">${_('Enable LDAPS')}</label></div>
51 <div class="checkboxes"><div class="checkbox">${h.checkbox('ldap_ldaps',True,class_='small')}</div></div>
52 </div>
53 <div class="field">
54 <div class="label"><label for="ldap_tls_reqcert">${_('Certificate Checks')}</label></div>
55 <div class="select">${h.select('ldap_tls_reqcert',c.tls_reqcert_cur,c.tls_reqcert_choices,class_='small')}</div>
56 </div>
57 <h3>${_('Search settings')}</h3>
58 <div class="field">
54 59 <div class="label"><label for="ldap_base_dn">${_('Base DN')}</label></div>
55 60 <div class="input">${h.text('ldap_base_dn',class_='small')}</div>
56 61 </div>
62 <div class="field">
63 <div class="label"><label for="ldap_filter">${_('LDAP Filter')}</label></div>
64 <div class="input">${h.text('ldap_filter',class_='small')}</div>
65 </div>
66 <div class="field">
67 <div class="label"><label for="ldap_search_scope">${_('LDAP Search Scope')}</label></div>
68 <div class="select">${h.select('ldap_search_scope',c.search_scope_cur,c.search_scope_choices,class_='small')}</div>
69 </div>
70 <h3>${_('Attribute mappings')}</h3>
71 <div class="field">
72 <div class="label"><label for="ldap_attr_login">${_('Login Attribute')}</label></div>
73 <div class="input">${h.text('ldap_attr_login',class_='small')}</div>
74 </div>
75 <div class="field">
76 <div class="label"><label for="ldap_attr_firstname">${_('First Name Attribute')}</label></div>
77 <div class="input">${h.text('ldap_attr_firstname',class_='small')}</div>
78 </div>
79 <div class="field">
80 <div class="label"><label for="ldap_attr_lastname">${_('Last Name Attribute')}</label></div>
81 <div class="input">${h.text('ldap_attr_lastname',class_='small')}</div>
82 </div>
83 <div class="field">
84 <div class="label"><label for="ldap_attr_email">${_('E-mail Attribute')}</label></div>
85 <div class="input">${h.text('ldap_attr_email',class_='small')}</div>
86 </div>
57 87
58 88 <div class="buttons">
59 89 ${h.submit('save','Save',class_="ui-button")}
@@ -49,6 +49,15 b''
49 49
50 50 <div class="field">
51 51 <div class="label">
52 <label for="ldap_dn">${_('LDAP DN')}:</label>
53 </div>
54 <div class="input">
55 ${h.text('ldap_dn',class_='small')}
56 </div>
57 </div>
58
59 <div class="field">
60 <div class="label">
52 61 <label for="new_password">${_('New password')}:</label>
53 62 </div>
54 63 <div class="input">
@@ -231,4 +240,4 b''
231 240 });
232 241 </script>
233 242 </div>
234 </%def> No newline at end of file
243 </%def>
@@ -49,7 +49,7 b''
49 49 <td>${user.last_login}</td>
50 50 <td>${h.bool2icon(user.active)}</td>
51 51 <td>${h.bool2icon(user.admin)}</td>
52 <td>${h.bool2icon(user.is_ldap)}</td>
52 <td>${h.bool2icon(bool(user.ldap_dn))}</td>
53 53 <td>
54 54 ${h.form(url('user', id=user.user_id),method='delete')}
55 55 ${h.submit('remove_','delete',id="remove_user_%s" % user.user_id,
General Comments 0
You need to be logged in to leave comments. Login now