##// END OF EJS Templates
auth-plugins: define unsafe settings
marcink -
r1623:cfb5e8ae default
parent child Browse files
Show More
@@ -1,283 +1,284 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2017 RhodeCode GmbH
3 # Copyright (C) 2012-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for Atlassian CROWD
22 RhodeCode authentication plugin for Atlassian CROWD
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import base64
27 import base64
28 import logging
28 import logging
29 import urllib2
29 import urllib2
30
30
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwds):
43 def plugin_factory(plugin_id, *args, **kwds):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class CrowdAuthnResource(AuthnPluginResourceBase):
52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 host = colander.SchemaNode(
57 host = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='127.0.0.1',
59 default='127.0.0.1',
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('Host'),
62 title=_('Host'),
63 widget='string')
63 widget='string')
64 port = colander.SchemaNode(
64 port = colander.SchemaNode(
65 colander.Int(),
65 colander.Int(),
66 default=8095,
66 default=8095,
67 description=_('The Port in use by the Atlassian CROWD Server'),
67 description=_('The Port in use by the Atlassian CROWD Server'),
68 preparer=strip_whitespace,
68 preparer=strip_whitespace,
69 title=_('Port'),
69 title=_('Port'),
70 validator=colander.Range(min=0, max=65536),
70 validator=colander.Range(min=0, max=65536),
71 widget='int')
71 widget='int')
72 app_name = colander.SchemaNode(
72 app_name = colander.SchemaNode(
73 colander.String(),
73 colander.String(),
74 default='',
74 default='',
75 description=_('The Application Name to authenticate to CROWD'),
75 description=_('The Application Name to authenticate to CROWD'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('Application Name'),
77 title=_('Application Name'),
78 widget='string')
78 widget='string')
79 app_password = colander.SchemaNode(
79 app_password = colander.SchemaNode(
80 colander.String(),
80 colander.String(),
81 default='',
81 default='',
82 description=_('The password to authenticate to CROWD'),
82 description=_('The password to authenticate to CROWD'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Application Password'),
84 title=_('Application Password'),
85 widget='password')
85 widget='password')
86 admin_groups = colander.SchemaNode(
86 admin_groups = colander.SchemaNode(
87 colander.String(),
87 colander.String(),
88 default='',
88 default='',
89 description=_('A comma separated list of group names that identify '
89 description=_('A comma separated list of group names that identify '
90 'users as RhodeCode Administrators'),
90 'users as RhodeCode Administrators'),
91 missing='',
91 missing='',
92 preparer=strip_whitespace,
92 preparer=strip_whitespace,
93 title=_('Admin Groups'),
93 title=_('Admin Groups'),
94 widget='string')
94 widget='string')
95
95
96
96
97 class CrowdServer(object):
97 class CrowdServer(object):
98 def __init__(self, *args, **kwargs):
98 def __init__(self, *args, **kwargs):
99 """
99 """
100 Create a new CrowdServer object that points to IP/Address 'host',
100 Create a new CrowdServer object that points to IP/Address 'host',
101 on the given port, and using the given method (https/http). user and
101 on the given port, and using the given method (https/http). user and
102 passwd can be set here or with set_credentials. If unspecified,
102 passwd can be set here or with set_credentials. If unspecified,
103 "version" defaults to "latest".
103 "version" defaults to "latest".
104
104
105 example::
105 example::
106
106
107 cserver = CrowdServer(host="127.0.0.1",
107 cserver = CrowdServer(host="127.0.0.1",
108 port="8095",
108 port="8095",
109 user="some_app",
109 user="some_app",
110 passwd="some_passwd",
110 passwd="some_passwd",
111 version="1")
111 version="1")
112 """
112 """
113 if not "port" in kwargs:
113 if not "port" in kwargs:
114 kwargs["port"] = "8095"
114 kwargs["port"] = "8095"
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 kwargs.get("host", "127.0.0.1"),
117 kwargs.get("host", "127.0.0.1"),
118 kwargs.get("port", "8095"))
118 kwargs.get("port", "8095"))
119 self.set_credentials(kwargs.get("user", ""),
119 self.set_credentials(kwargs.get("user", ""),
120 kwargs.get("passwd", ""))
120 kwargs.get("passwd", ""))
121 self._version = kwargs.get("version", "latest")
121 self._version = kwargs.get("version", "latest")
122 self._url_list = None
122 self._url_list = None
123 self._appname = "crowd"
123 self._appname = "crowd"
124
124
125 def set_credentials(self, user, passwd):
125 def set_credentials(self, user, passwd):
126 self.user = user
126 self.user = user
127 self.passwd = passwd
127 self.passwd = passwd
128 self._make_opener()
128 self._make_opener()
129
129
130 def _make_opener(self):
130 def _make_opener(self):
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 mgr.add_password(None, self._uri, self.user, self.passwd)
132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 self.opener = urllib2.build_opener(handler)
134 self.opener = urllib2.build_opener(handler)
135
135
136 def _request(self, url, body=None, headers=None,
136 def _request(self, url, body=None, headers=None,
137 method=None, noformat=False,
137 method=None, noformat=False,
138 empty_response_ok=False):
138 empty_response_ok=False):
139 _headers = {"Content-type": "application/json",
139 _headers = {"Content-type": "application/json",
140 "Accept": "application/json"}
140 "Accept": "application/json"}
141 if self.user and self.passwd:
141 if self.user and self.passwd:
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 _headers["Authorization"] = "Basic %s" % authstring
143 _headers["Authorization"] = "Basic %s" % authstring
144 if headers:
144 if headers:
145 _headers.update(headers)
145 _headers.update(headers)
146 log.debug("Sent crowd: \n%s"
146 log.debug("Sent crowd: \n%s"
147 % (formatted_json({"url": url, "body": body,
147 % (formatted_json({"url": url, "body": body,
148 "headers": _headers})))
148 "headers": _headers})))
149 request = urllib2.Request(url, body, _headers)
149 request = urllib2.Request(url, body, _headers)
150 if method:
150 if method:
151 request.get_method = lambda: method
151 request.get_method = lambda: method
152
152
153 global msg
153 global msg
154 msg = ""
154 msg = ""
155 try:
155 try:
156 rdoc = self.opener.open(request)
156 rdoc = self.opener.open(request)
157 msg = "".join(rdoc.readlines())
157 msg = "".join(rdoc.readlines())
158 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
159 rval = {}
159 rval = {}
160 rval["status"] = True
160 rval["status"] = True
161 rval["error"] = "Response body was empty"
161 rval["error"] = "Response body was empty"
162 elif not noformat:
162 elif not noformat:
163 rval = json.loads(msg)
163 rval = json.loads(msg)
164 rval["status"] = True
164 rval["status"] = True
165 else:
165 else:
166 rval = "".join(rdoc.readlines())
166 rval = "".join(rdoc.readlines())
167 except Exception as e:
167 except Exception as e:
168 if not noformat:
168 if not noformat:
169 rval = {"status": False,
169 rval = {"status": False,
170 "body": body,
170 "body": body,
171 "error": str(e) + "\n" + msg}
171 "error": str(e) + "\n" + msg}
172 else:
172 else:
173 rval = None
173 rval = None
174 return rval
174 return rval
175
175
176 def user_auth(self, username, password):
176 def user_auth(self, username, password):
177 """Authenticate a user against crowd. Returns brief information about
177 """Authenticate a user against crowd. Returns brief information about
178 the user."""
178 the user."""
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 % (self._uri, self._version, username))
180 % (self._uri, self._version, username))
181 body = json.dumps({"value": password})
181 body = json.dumps({"value": password})
182 return self._request(url, body)
182 return self._request(url, body)
183
183
184 def user_groups(self, username):
184 def user_groups(self, username):
185 """Retrieve a list of groups to which this user belongs."""
185 """Retrieve a list of groups to which this user belongs."""
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 % (self._uri, self._version, username))
187 % (self._uri, self._version, username))
188 return self._request(url)
188 return self._request(url)
189
189
190
190
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 _settings_unsafe_keys = ['app_password']
192
193
193 def includeme(self, config):
194 def includeme(self, config):
194 config.add_authn_plugin(self)
195 config.add_authn_plugin(self)
195 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
196 config.add_view(
197 config.add_view(
197 'rhodecode.authentication.views.AuthnPluginViewBase',
198 'rhodecode.authentication.views.AuthnPluginViewBase',
198 attr='settings_get',
199 attr='settings_get',
199 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
200 request_method='GET',
201 request_method='GET',
201 route_name='auth_home',
202 route_name='auth_home',
202 context=CrowdAuthnResource)
203 context=CrowdAuthnResource)
203 config.add_view(
204 config.add_view(
204 'rhodecode.authentication.views.AuthnPluginViewBase',
205 'rhodecode.authentication.views.AuthnPluginViewBase',
205 attr='settings_post',
206 attr='settings_post',
206 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
207 request_method='POST',
208 request_method='POST',
208 route_name='auth_home',
209 route_name='auth_home',
209 context=CrowdAuthnResource)
210 context=CrowdAuthnResource)
210
211
211 def get_settings_schema(self):
212 def get_settings_schema(self):
212 return CrowdSettingsSchema()
213 return CrowdSettingsSchema()
213
214
214 def get_display_name(self):
215 def get_display_name(self):
215 return _('CROWD')
216 return _('CROWD')
216
217
217 @hybrid_property
218 @hybrid_property
218 def name(self):
219 def name(self):
219 return "crowd"
220 return "crowd"
220
221
221 def use_fake_password(self):
222 def use_fake_password(self):
222 return True
223 return True
223
224
224 def user_activation_state(self):
225 def user_activation_state(self):
225 def_user_perms = User.get_default_user().AuthUser.permissions['global']
226 def_user_perms = User.get_default_user().AuthUser.permissions['global']
226 return 'hg.extern_activate.auto' in def_user_perms
227 return 'hg.extern_activate.auto' in def_user_perms
227
228
228 def auth(self, userobj, username, password, settings, **kwargs):
229 def auth(self, userobj, username, password, settings, **kwargs):
229 """
230 """
230 Given a user object (which may be null), username, a plaintext password,
231 Given a user object (which may be null), username, a plaintext password,
231 and a settings object (containing all the keys needed as listed in settings()),
232 and a settings object (containing all the keys needed as listed in settings()),
232 authenticate this user's login attempt.
233 authenticate this user's login attempt.
233
234
234 Return None on failure. On success, return a dictionary of the form:
235 Return None on failure. On success, return a dictionary of the form:
235
236
236 see: RhodeCodeAuthPluginBase.auth_func_attrs
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
237 This is later validated for correctness
238 This is later validated for correctness
238 """
239 """
239 if not username or not password:
240 if not username or not password:
240 log.debug('Empty username or password skipping...')
241 log.debug('Empty username or password skipping...')
241 return None
242 return None
242
243
243 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
244 server = CrowdServer(**settings)
245 server = CrowdServer(**settings)
245 server.set_credentials(settings["app_name"], settings["app_password"])
246 server.set_credentials(settings["app_name"], settings["app_password"])
246 crowd_user = server.user_auth(username, password)
247 crowd_user = server.user_auth(username, password)
247 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
248 if not crowd_user["status"]:
249 if not crowd_user["status"]:
249 return None
250 return None
250
251
251 res = server.user_groups(crowd_user["name"])
252 res = server.user_groups(crowd_user["name"])
252 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
253 crowd_user["groups"] = [x["name"] for x in res["groups"]]
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
254
255
255 # old attrs fetched from RhodeCode database
256 # old attrs fetched from RhodeCode database
256 admin = getattr(userobj, 'admin', False)
257 admin = getattr(userobj, 'admin', False)
257 active = getattr(userobj, 'active', True)
258 active = getattr(userobj, 'active', True)
258 email = getattr(userobj, 'email', '')
259 email = getattr(userobj, 'email', '')
259 username = getattr(userobj, 'username', username)
260 username = getattr(userobj, 'username', username)
260 firstname = getattr(userobj, 'firstname', '')
261 firstname = getattr(userobj, 'firstname', '')
261 lastname = getattr(userobj, 'lastname', '')
262 lastname = getattr(userobj, 'lastname', '')
262 extern_type = getattr(userobj, 'extern_type', '')
263 extern_type = getattr(userobj, 'extern_type', '')
263
264
264 user_attrs = {
265 user_attrs = {
265 'username': username,
266 'username': username,
266 'firstname': crowd_user["first-name"] or firstname,
267 'firstname': crowd_user["first-name"] or firstname,
267 'lastname': crowd_user["last-name"] or lastname,
268 'lastname': crowd_user["last-name"] or lastname,
268 'groups': crowd_user["groups"],
269 'groups': crowd_user["groups"],
269 'email': crowd_user["email"] or email,
270 'email': crowd_user["email"] or email,
270 'admin': admin,
271 'admin': admin,
271 'active': active,
272 'active': active,
272 'active_from_extern': crowd_user.get('active'),
273 'active_from_extern': crowd_user.get('active'),
273 'extern_name': crowd_user["name"],
274 'extern_name': crowd_user["name"],
274 'extern_type': extern_type,
275 'extern_type': extern_type,
275 }
276 }
276
277
277 # set an admin if we're in admin_groups of crowd
278 # set an admin if we're in admin_groups of crowd
278 for group in settings["admin_groups"]:
279 for group in settings["admin_groups"]:
279 if group in user_attrs["groups"]:
280 if group in user_attrs["groups"]:
280 user_attrs["admin"] = True
281 user_attrs["admin"] = True
281 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
282 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
282 log.info('user %s authenticated correctly' % user_attrs['username'])
283 log.info('user %s authenticated correctly' % user_attrs['username'])
283 return user_attrs
284 return user_attrs
@@ -1,473 +1,474 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for LDAP
22 RhodeCode authentication plugin for LDAP
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from rhodecode.translation import _
30 from rhodecode.translation import _
31 from rhodecode.authentication.base import (
31 from rhodecode.authentication.base import (
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
32 RhodeCodeExternalAuthPlugin, chop_at, hybrid_property)
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
33 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
34 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.lib.colander_utils import strip_whitespace
35 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.exceptions import (
36 from rhodecode.lib.exceptions import (
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
37 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 )
38 )
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
39 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41 from rhodecode.model.validators import Missing
41 from rhodecode.model.validators import Missing
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45 try:
45 try:
46 import ldap
46 import ldap
47 except ImportError:
47 except ImportError:
48 # means that python-ldap is not installed, we use Missing object to mark
48 # means that python-ldap is not installed, we use Missing object to mark
49 # ldap lib is Missing
49 # ldap lib is Missing
50 ldap = Missing
50 ldap = Missing
51
51
52
52
53 def plugin_factory(plugin_id, *args, **kwds):
53 def plugin_factory(plugin_id, *args, **kwds):
54 """
54 """
55 Factory function that is called during plugin discovery.
55 Factory function that is called during plugin discovery.
56 It returns the plugin instance.
56 It returns the plugin instance.
57 """
57 """
58 plugin = RhodeCodeAuthPlugin(plugin_id)
58 plugin = RhodeCodeAuthPlugin(plugin_id)
59 return plugin
59 return plugin
60
60
61
61
62 class LdapAuthnResource(AuthnPluginResourceBase):
62 class LdapAuthnResource(AuthnPluginResourceBase):
63 pass
63 pass
64
64
65
65
66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
66 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
67 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
68 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
69 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
70
70
71 host = colander.SchemaNode(
71 host = colander.SchemaNode(
72 colander.String(),
72 colander.String(),
73 default='',
73 default='',
74 description=_('Host of the LDAP Server \n'
74 description=_('Host of the LDAP Server \n'
75 '(e.g., 192.168.2.154, or ldap-server.domain.com'),
75 '(e.g., 192.168.2.154, or ldap-server.domain.com'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('LDAP Host'),
77 title=_('LDAP Host'),
78 widget='string')
78 widget='string')
79 port = colander.SchemaNode(
79 port = colander.SchemaNode(
80 colander.Int(),
80 colander.Int(),
81 default=389,
81 default=389,
82 description=_('Custom port that the LDAP server is listening on. Default: 389'),
82 description=_('Custom port that the LDAP server is listening on. Default: 389'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Port'),
84 title=_('Port'),
85 validator=colander.Range(min=0, max=65536),
85 validator=colander.Range(min=0, max=65536),
86 widget='int')
86 widget='int')
87 dn_user = colander.SchemaNode(
87 dn_user = colander.SchemaNode(
88 colander.String(),
88 colander.String(),
89 default='',
89 default='',
90 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
90 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
91 'e.g., cn=admin,dc=mydomain,dc=com, or '
91 'e.g., cn=admin,dc=mydomain,dc=com, or '
92 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
92 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
93 missing='',
93 missing='',
94 preparer=strip_whitespace,
94 preparer=strip_whitespace,
95 title=_('Account'),
95 title=_('Account'),
96 widget='string')
96 widget='string')
97 dn_pass = colander.SchemaNode(
97 dn_pass = colander.SchemaNode(
98 colander.String(),
98 colander.String(),
99 default='',
99 default='',
100 description=_('Password to authenticate for given user DN.'),
100 description=_('Password to authenticate for given user DN.'),
101 missing='',
101 missing='',
102 preparer=strip_whitespace,
102 preparer=strip_whitespace,
103 title=_('Password'),
103 title=_('Password'),
104 widget='password')
104 widget='password')
105 tls_kind = colander.SchemaNode(
105 tls_kind = colander.SchemaNode(
106 colander.String(),
106 colander.String(),
107 default=tls_kind_choices[0],
107 default=tls_kind_choices[0],
108 description=_('TLS Type'),
108 description=_('TLS Type'),
109 title=_('Connection Security'),
109 title=_('Connection Security'),
110 validator=colander.OneOf(tls_kind_choices),
110 validator=colander.OneOf(tls_kind_choices),
111 widget='select')
111 widget='select')
112 tls_reqcert = colander.SchemaNode(
112 tls_reqcert = colander.SchemaNode(
113 colander.String(),
113 colander.String(),
114 default=tls_reqcert_choices[0],
114 default=tls_reqcert_choices[0],
115 description=_('Require Cert over TLS?'),
115 description=_('Require Cert over TLS?'),
116 title=_('Certificate Checks'),
116 title=_('Certificate Checks'),
117 validator=colander.OneOf(tls_reqcert_choices),
117 validator=colander.OneOf(tls_reqcert_choices),
118 widget='select')
118 widget='select')
119 base_dn = colander.SchemaNode(
119 base_dn = colander.SchemaNode(
120 colander.String(),
120 colander.String(),
121 default='',
121 default='',
122 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
122 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
123 'in it to be replaced with current user credentials \n'
123 'in it to be replaced with current user credentials \n'
124 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
124 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
125 missing='',
125 missing='',
126 preparer=strip_whitespace,
126 preparer=strip_whitespace,
127 title=_('Base DN'),
127 title=_('Base DN'),
128 widget='string')
128 widget='string')
129 filter = colander.SchemaNode(
129 filter = colander.SchemaNode(
130 colander.String(),
130 colander.String(),
131 default='',
131 default='',
132 description=_('Filter to narrow results \n'
132 description=_('Filter to narrow results \n'
133 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
133 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
134 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
134 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
135 missing='',
135 missing='',
136 preparer=strip_whitespace,
136 preparer=strip_whitespace,
137 title=_('LDAP Search Filter'),
137 title=_('LDAP Search Filter'),
138 widget='string')
138 widget='string')
139
139
140 search_scope = colander.SchemaNode(
140 search_scope = colander.SchemaNode(
141 colander.String(),
141 colander.String(),
142 default=search_scope_choices[2],
142 default=search_scope_choices[2],
143 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
143 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
144 title=_('LDAP Search Scope'),
144 title=_('LDAP Search Scope'),
145 validator=colander.OneOf(search_scope_choices),
145 validator=colander.OneOf(search_scope_choices),
146 widget='select')
146 widget='select')
147 attr_login = colander.SchemaNode(
147 attr_login = colander.SchemaNode(
148 colander.String(),
148 colander.String(),
149 default='uid',
149 default='uid',
150 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
150 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
151 preparer=strip_whitespace,
151 preparer=strip_whitespace,
152 title=_('Login Attribute'),
152 title=_('Login Attribute'),
153 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
153 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
154 widget='string')
154 widget='string')
155 attr_firstname = colander.SchemaNode(
155 attr_firstname = colander.SchemaNode(
156 colander.String(),
156 colander.String(),
157 default='',
157 default='',
158 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
158 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
159 missing='',
159 missing='',
160 preparer=strip_whitespace,
160 preparer=strip_whitespace,
161 title=_('First Name Attribute'),
161 title=_('First Name Attribute'),
162 widget='string')
162 widget='string')
163 attr_lastname = colander.SchemaNode(
163 attr_lastname = colander.SchemaNode(
164 colander.String(),
164 colander.String(),
165 default='',
165 default='',
166 description=_('LDAP Attribute to map to last name (e.g., sn)'),
166 description=_('LDAP Attribute to map to last name (e.g., sn)'),
167 missing='',
167 missing='',
168 preparer=strip_whitespace,
168 preparer=strip_whitespace,
169 title=_('Last Name Attribute'),
169 title=_('Last Name Attribute'),
170 widget='string')
170 widget='string')
171 attr_email = colander.SchemaNode(
171 attr_email = colander.SchemaNode(
172 colander.String(),
172 colander.String(),
173 default='',
173 default='',
174 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
174 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
175 'Emails are a crucial part of RhodeCode. \n'
175 'Emails are a crucial part of RhodeCode. \n'
176 'If possible add a valid email attribute to ldap users.'),
176 'If possible add a valid email attribute to ldap users.'),
177 missing='',
177 missing='',
178 preparer=strip_whitespace,
178 preparer=strip_whitespace,
179 title=_('Email Attribute'),
179 title=_('Email Attribute'),
180 widget='string')
180 widget='string')
181
181
182
182
183 class AuthLdap(object):
183 class AuthLdap(object):
184
184
185 def _build_servers(self):
185 def _build_servers(self):
186 return ', '.join(
186 return ', '.join(
187 ["{}://{}:{}".format(
187 ["{}://{}:{}".format(
188 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
188 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
189 for host in self.SERVER_ADDRESSES])
189 for host in self.SERVER_ADDRESSES])
190
190
191 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
191 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
192 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
192 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
193 search_scope='SUBTREE', attr_login='uid',
193 search_scope='SUBTREE', attr_login='uid',
194 ldap_filter=None):
194 ldap_filter=None):
195 if ldap == Missing:
195 if ldap == Missing:
196 raise LdapImportError("Missing or incompatible ldap library")
196 raise LdapImportError("Missing or incompatible ldap library")
197
197
198 self.debug = False
198 self.debug = False
199 self.ldap_version = ldap_version
199 self.ldap_version = ldap_version
200 self.ldap_server_type = 'ldap'
200 self.ldap_server_type = 'ldap'
201
201
202 self.TLS_KIND = tls_kind
202 self.TLS_KIND = tls_kind
203
203
204 if self.TLS_KIND == 'LDAPS':
204 if self.TLS_KIND == 'LDAPS':
205 port = port or 689
205 port = port or 689
206 self.ldap_server_type += 's'
206 self.ldap_server_type += 's'
207
207
208 OPT_X_TLS_DEMAND = 2
208 OPT_X_TLS_DEMAND = 2
209 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
209 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
210 OPT_X_TLS_DEMAND)
210 OPT_X_TLS_DEMAND)
211 # split server into list
211 # split server into list
212 self.SERVER_ADDRESSES = server.split(',')
212 self.SERVER_ADDRESSES = server.split(',')
213 self.LDAP_SERVER_PORT = port
213 self.LDAP_SERVER_PORT = port
214
214
215 # USE FOR READ ONLY BIND TO LDAP SERVER
215 # USE FOR READ ONLY BIND TO LDAP SERVER
216 self.attr_login = attr_login
216 self.attr_login = attr_login
217
217
218 self.LDAP_BIND_DN = safe_str(bind_dn)
218 self.LDAP_BIND_DN = safe_str(bind_dn)
219 self.LDAP_BIND_PASS = safe_str(bind_pass)
219 self.LDAP_BIND_PASS = safe_str(bind_pass)
220 self.LDAP_SERVER = self._build_servers()
220 self.LDAP_SERVER = self._build_servers()
221 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
221 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
222 self.BASE_DN = safe_str(base_dn)
222 self.BASE_DN = safe_str(base_dn)
223 self.LDAP_FILTER = safe_str(ldap_filter)
223 self.LDAP_FILTER = safe_str(ldap_filter)
224
224
225 def _get_ldap_server(self):
225 def _get_ldap_server(self):
226 if self.debug:
226 if self.debug:
227 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
227 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
228 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
228 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
229 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
229 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
230 '/etc/openldap/cacerts')
230 '/etc/openldap/cacerts')
231 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
231 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
232 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
232 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
233 ldap.set_option(ldap.OPT_TIMEOUT, 20)
233 ldap.set_option(ldap.OPT_TIMEOUT, 20)
234 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
234 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 10)
235 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
235 ldap.set_option(ldap.OPT_TIMELIMIT, 15)
236 if self.TLS_KIND != 'PLAIN':
236 if self.TLS_KIND != 'PLAIN':
237 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
237 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
238 server = ldap.initialize(self.LDAP_SERVER)
238 server = ldap.initialize(self.LDAP_SERVER)
239 if self.ldap_version == 2:
239 if self.ldap_version == 2:
240 server.protocol = ldap.VERSION2
240 server.protocol = ldap.VERSION2
241 else:
241 else:
242 server.protocol = ldap.VERSION3
242 server.protocol = ldap.VERSION3
243
243
244 if self.TLS_KIND == 'START_TLS':
244 if self.TLS_KIND == 'START_TLS':
245 server.start_tls_s()
245 server.start_tls_s()
246
246
247 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
247 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
248 log.debug('Trying simple_bind with password and given login DN: %s',
248 log.debug('Trying simple_bind with password and given login DN: %s',
249 self.LDAP_BIND_DN)
249 self.LDAP_BIND_DN)
250 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
250 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
251
251
252 return server
252 return server
253
253
254 def get_uid(self, username):
254 def get_uid(self, username):
255 uid = username
255 uid = username
256 for server_addr in self.SERVER_ADDRESSES:
256 for server_addr in self.SERVER_ADDRESSES:
257 uid = chop_at(username, "@%s" % server_addr)
257 uid = chop_at(username, "@%s" % server_addr)
258 return uid
258 return uid
259
259
260 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
260 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
261 try:
261 try:
262 log.debug('Trying simple bind with %s', dn)
262 log.debug('Trying simple bind with %s', dn)
263 server.simple_bind_s(dn, safe_str(password))
263 server.simple_bind_s(dn, safe_str(password))
264 user = server.search_ext_s(
264 user = server.search_ext_s(
265 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
265 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
266 _, attrs = user
266 _, attrs = user
267 return attrs
267 return attrs
268
268
269 except ldap.INVALID_CREDENTIALS:
269 except ldap.INVALID_CREDENTIALS:
270 log.debug(
270 log.debug(
271 "LDAP rejected password for user '%s': %s, org_exc:",
271 "LDAP rejected password for user '%s': %s, org_exc:",
272 username, dn, exc_info=True)
272 username, dn, exc_info=True)
273
273
274 def authenticate_ldap(self, username, password):
274 def authenticate_ldap(self, username, password):
275 """
275 """
276 Authenticate a user via LDAP and return his/her LDAP properties.
276 Authenticate a user via LDAP and return his/her LDAP properties.
277
277
278 Raises AuthenticationError if the credentials are rejected, or
278 Raises AuthenticationError if the credentials are rejected, or
279 EnvironmentError if the LDAP server can't be reached.
279 EnvironmentError if the LDAP server can't be reached.
280
280
281 :param username: username
281 :param username: username
282 :param password: password
282 :param password: password
283 """
283 """
284
284
285 uid = self.get_uid(username)
285 uid = self.get_uid(username)
286
286
287 if not password:
287 if not password:
288 msg = "Authenticating user %s with blank password not allowed"
288 msg = "Authenticating user %s with blank password not allowed"
289 log.warning(msg, username)
289 log.warning(msg, username)
290 raise LdapPasswordError(msg)
290 raise LdapPasswordError(msg)
291 if "," in username:
291 if "," in username:
292 raise LdapUsernameError("invalid character in username: ,")
292 raise LdapUsernameError("invalid character in username: ,")
293 try:
293 try:
294 server = self._get_ldap_server()
294 server = self._get_ldap_server()
295 filter_ = '(&%s(%s=%s))' % (
295 filter_ = '(&%s(%s=%s))' % (
296 self.LDAP_FILTER, self.attr_login, username)
296 self.LDAP_FILTER, self.attr_login, username)
297 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
297 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
298 filter_, self.LDAP_SERVER)
298 filter_, self.LDAP_SERVER)
299 lobjects = server.search_ext_s(
299 lobjects = server.search_ext_s(
300 self.BASE_DN, self.SEARCH_SCOPE, filter_)
300 self.BASE_DN, self.SEARCH_SCOPE, filter_)
301
301
302 if not lobjects:
302 if not lobjects:
303 log.debug("No matching LDAP objects for authentication "
303 log.debug("No matching LDAP objects for authentication "
304 "of UID:'%s' username:(%s)", uid, username)
304 "of UID:'%s' username:(%s)", uid, username)
305 raise ldap.NO_SUCH_OBJECT()
305 raise ldap.NO_SUCH_OBJECT()
306
306
307 log.debug('Found matching ldap object, trying to authenticate')
307 log.debug('Found matching ldap object, trying to authenticate')
308 for (dn, _attrs) in lobjects:
308 for (dn, _attrs) in lobjects:
309 if dn is None:
309 if dn is None:
310 continue
310 continue
311
311
312 user_attrs = self.fetch_attrs_from_simple_bind(
312 user_attrs = self.fetch_attrs_from_simple_bind(
313 server, dn, username, password)
313 server, dn, username, password)
314 if user_attrs:
314 if user_attrs:
315 break
315 break
316
316
317 else:
317 else:
318 raise LdapPasswordError('Failed to authenticate user '
318 raise LdapPasswordError('Failed to authenticate user '
319 'with given password')
319 'with given password')
320
320
321 except ldap.NO_SUCH_OBJECT:
321 except ldap.NO_SUCH_OBJECT:
322 log.debug("LDAP says no such user '%s' (%s), org_exc:",
322 log.debug("LDAP says no such user '%s' (%s), org_exc:",
323 uid, username, exc_info=True)
323 uid, username, exc_info=True)
324 raise LdapUsernameError('Unable to find user')
324 raise LdapUsernameError('Unable to find user')
325 except ldap.SERVER_DOWN:
325 except ldap.SERVER_DOWN:
326 org_exc = traceback.format_exc()
326 org_exc = traceback.format_exc()
327 raise LdapConnectionError(
327 raise LdapConnectionError(
328 "LDAP can't access authentication "
328 "LDAP can't access authentication "
329 "server, org_exc:%s" % org_exc)
329 "server, org_exc:%s" % org_exc)
330
330
331 return dn, user_attrs
331 return dn, user_attrs
332
332
333
333
334 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
334 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
335 # used to define dynamic binding in the
335 # used to define dynamic binding in the
336 DYNAMIC_BIND_VAR = '$login'
336 DYNAMIC_BIND_VAR = '$login'
337 _settings_unsafe_keys = ['dn_pass']
337
338
338 def includeme(self, config):
339 def includeme(self, config):
339 config.add_authn_plugin(self)
340 config.add_authn_plugin(self)
340 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
341 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
341 config.add_view(
342 config.add_view(
342 'rhodecode.authentication.views.AuthnPluginViewBase',
343 'rhodecode.authentication.views.AuthnPluginViewBase',
343 attr='settings_get',
344 attr='settings_get',
344 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
345 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
345 request_method='GET',
346 request_method='GET',
346 route_name='auth_home',
347 route_name='auth_home',
347 context=LdapAuthnResource)
348 context=LdapAuthnResource)
348 config.add_view(
349 config.add_view(
349 'rhodecode.authentication.views.AuthnPluginViewBase',
350 'rhodecode.authentication.views.AuthnPluginViewBase',
350 attr='settings_post',
351 attr='settings_post',
351 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
352 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
352 request_method='POST',
353 request_method='POST',
353 route_name='auth_home',
354 route_name='auth_home',
354 context=LdapAuthnResource)
355 context=LdapAuthnResource)
355
356
356 def get_settings_schema(self):
357 def get_settings_schema(self):
357 return LdapSettingsSchema()
358 return LdapSettingsSchema()
358
359
359 def get_display_name(self):
360 def get_display_name(self):
360 return _('LDAP')
361 return _('LDAP')
361
362
362 @hybrid_property
363 @hybrid_property
363 def name(self):
364 def name(self):
364 return "ldap"
365 return "ldap"
365
366
366 def use_fake_password(self):
367 def use_fake_password(self):
367 return True
368 return True
368
369
369 def user_activation_state(self):
370 def user_activation_state(self):
370 def_user_perms = User.get_default_user().AuthUser.permissions['global']
371 def_user_perms = User.get_default_user().AuthUser.permissions['global']
371 return 'hg.extern_activate.auto' in def_user_perms
372 return 'hg.extern_activate.auto' in def_user_perms
372
373
373 def try_dynamic_binding(self, username, password, current_args):
374 def try_dynamic_binding(self, username, password, current_args):
374 """
375 """
375 Detects marker inside our original bind, and uses dynamic auth if
376 Detects marker inside our original bind, and uses dynamic auth if
376 present
377 present
377 """
378 """
378
379
379 org_bind = current_args['bind_dn']
380 org_bind = current_args['bind_dn']
380 passwd = current_args['bind_pass']
381 passwd = current_args['bind_pass']
381
382
382 def has_bind_marker(username):
383 def has_bind_marker(username):
383 if self.DYNAMIC_BIND_VAR in username:
384 if self.DYNAMIC_BIND_VAR in username:
384 return True
385 return True
385
386
386 # we only passed in user with "special" variable
387 # we only passed in user with "special" variable
387 if org_bind and has_bind_marker(org_bind) and not passwd:
388 if org_bind and has_bind_marker(org_bind) and not passwd:
388 log.debug('Using dynamic user/password binding for ldap '
389 log.debug('Using dynamic user/password binding for ldap '
389 'authentication. Replacing `%s` with username',
390 'authentication. Replacing `%s` with username',
390 self.DYNAMIC_BIND_VAR)
391 self.DYNAMIC_BIND_VAR)
391 current_args['bind_dn'] = org_bind.replace(
392 current_args['bind_dn'] = org_bind.replace(
392 self.DYNAMIC_BIND_VAR, username)
393 self.DYNAMIC_BIND_VAR, username)
393 current_args['bind_pass'] = password
394 current_args['bind_pass'] = password
394
395
395 return current_args
396 return current_args
396
397
397 def auth(self, userobj, username, password, settings, **kwargs):
398 def auth(self, userobj, username, password, settings, **kwargs):
398 """
399 """
399 Given a user object (which may be null), username, a plaintext password,
400 Given a user object (which may be null), username, a plaintext password,
400 and a settings object (containing all the keys needed as listed in
401 and a settings object (containing all the keys needed as listed in
401 settings()), authenticate this user's login attempt.
402 settings()), authenticate this user's login attempt.
402
403
403 Return None on failure. On success, return a dictionary of the form:
404 Return None on failure. On success, return a dictionary of the form:
404
405
405 see: RhodeCodeAuthPluginBase.auth_func_attrs
406 see: RhodeCodeAuthPluginBase.auth_func_attrs
406 This is later validated for correctness
407 This is later validated for correctness
407 """
408 """
408
409
409 if not username or not password:
410 if not username or not password:
410 log.debug('Empty username or password skipping...')
411 log.debug('Empty username or password skipping...')
411 return None
412 return None
412
413
413 ldap_args = {
414 ldap_args = {
414 'server': settings.get('host', ''),
415 'server': settings.get('host', ''),
415 'base_dn': settings.get('base_dn', ''),
416 'base_dn': settings.get('base_dn', ''),
416 'port': settings.get('port'),
417 'port': settings.get('port'),
417 'bind_dn': settings.get('dn_user'),
418 'bind_dn': settings.get('dn_user'),
418 'bind_pass': settings.get('dn_pass'),
419 'bind_pass': settings.get('dn_pass'),
419 'tls_kind': settings.get('tls_kind'),
420 'tls_kind': settings.get('tls_kind'),
420 'tls_reqcert': settings.get('tls_reqcert'),
421 'tls_reqcert': settings.get('tls_reqcert'),
421 'search_scope': settings.get('search_scope'),
422 'search_scope': settings.get('search_scope'),
422 'attr_login': settings.get('attr_login'),
423 'attr_login': settings.get('attr_login'),
423 'ldap_version': 3,
424 'ldap_version': 3,
424 'ldap_filter': settings.get('filter'),
425 'ldap_filter': settings.get('filter'),
425 }
426 }
426
427
427 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
428 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
428
429
429 log.debug('Checking for ldap authentication.')
430 log.debug('Checking for ldap authentication.')
430
431
431 try:
432 try:
432 aldap = AuthLdap(**ldap_args)
433 aldap = AuthLdap(**ldap_args)
433 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
434 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
434 log.debug('Got ldap DN response %s', user_dn)
435 log.debug('Got ldap DN response %s', user_dn)
435
436
436 def get_ldap_attr(k):
437 def get_ldap_attr(k):
437 return ldap_attrs.get(settings.get(k), [''])[0]
438 return ldap_attrs.get(settings.get(k), [''])[0]
438
439
439 # old attrs fetched from RhodeCode database
440 # old attrs fetched from RhodeCode database
440 admin = getattr(userobj, 'admin', False)
441 admin = getattr(userobj, 'admin', False)
441 active = getattr(userobj, 'active', True)
442 active = getattr(userobj, 'active', True)
442 email = getattr(userobj, 'email', '')
443 email = getattr(userobj, 'email', '')
443 username = getattr(userobj, 'username', username)
444 username = getattr(userobj, 'username', username)
444 firstname = getattr(userobj, 'firstname', '')
445 firstname = getattr(userobj, 'firstname', '')
445 lastname = getattr(userobj, 'lastname', '')
446 lastname = getattr(userobj, 'lastname', '')
446 extern_type = getattr(userobj, 'extern_type', '')
447 extern_type = getattr(userobj, 'extern_type', '')
447
448
448 groups = []
449 groups = []
449 user_attrs = {
450 user_attrs = {
450 'username': username,
451 'username': username,
451 'firstname': safe_unicode(
452 'firstname': safe_unicode(
452 get_ldap_attr('attr_firstname') or firstname),
453 get_ldap_attr('attr_firstname') or firstname),
453 'lastname': safe_unicode(
454 'lastname': safe_unicode(
454 get_ldap_attr('attr_lastname') or lastname),
455 get_ldap_attr('attr_lastname') or lastname),
455 'groups': groups,
456 'groups': groups,
456 'email': get_ldap_attr('attr_email') or email,
457 'email': get_ldap_attr('attr_email') or email,
457 'admin': admin,
458 'admin': admin,
458 'active': active,
459 'active': active,
459 'active_from_extern': None,
460 'active_from_extern': None,
460 'extern_name': user_dn,
461 'extern_name': user_dn,
461 'extern_type': extern_type,
462 'extern_type': extern_type,
462 }
463 }
463 log.debug('ldap user: %s', user_attrs)
464 log.debug('ldap user: %s', user_attrs)
464 log.info('user %s authenticated correctly', user_attrs['username'])
465 log.info('user %s authenticated correctly', user_attrs['username'])
465
466
466 return user_attrs
467 return user_attrs
467
468
468 except (LdapUsernameError, LdapPasswordError, LdapImportError):
469 except (LdapUsernameError, LdapPasswordError, LdapImportError):
469 log.exception("LDAP related exception")
470 log.exception("LDAP related exception")
470 return None
471 return None
471 except (Exception,):
472 except (Exception,):
472 log.exception("Other exception")
473 log.exception("Other exception")
473 return None
474 return None
General Comments 0
You need to be logged in to leave comments. Login now