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