##// END OF EJS Templates
logging: improve consistency of auth plugins logs.
marcink -
r2604:bf544c2f default
parent child Browse files
Show More
@@ -1,285 +1,285 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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 _settings_unsafe_keys = ['app_password']
193
193
194 def includeme(self, config):
194 def includeme(self, config):
195 config.add_authn_plugin(self)
195 config.add_authn_plugin(self)
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
196 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_view(
197 config.add_view(
198 'rhodecode.authentication.views.AuthnPluginViewBase',
198 'rhodecode.authentication.views.AuthnPluginViewBase',
199 attr='settings_get',
199 attr='settings_get',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
200 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 request_method='GET',
201 request_method='GET',
202 route_name='auth_home',
202 route_name='auth_home',
203 context=CrowdAuthnResource)
203 context=CrowdAuthnResource)
204 config.add_view(
204 config.add_view(
205 'rhodecode.authentication.views.AuthnPluginViewBase',
205 'rhodecode.authentication.views.AuthnPluginViewBase',
206 attr='settings_post',
206 attr='settings_post',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
207 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 request_method='POST',
208 request_method='POST',
209 route_name='auth_home',
209 route_name='auth_home',
210 context=CrowdAuthnResource)
210 context=CrowdAuthnResource)
211
211
212 def get_settings_schema(self):
212 def get_settings_schema(self):
213 return CrowdSettingsSchema()
213 return CrowdSettingsSchema()
214
214
215 def get_display_name(self):
215 def get_display_name(self):
216 return _('CROWD')
216 return _('CROWD')
217
217
218 @hybrid_property
218 @hybrid_property
219 def name(self):
219 def name(self):
220 return "crowd"
220 return "crowd"
221
221
222 def use_fake_password(self):
222 def use_fake_password(self):
223 return True
223 return True
224
224
225 def user_activation_state(self):
225 def user_activation_state(self):
226 def_user_perms = User.get_default_user().AuthUser().permissions['global']
226 def_user_perms = User.get_default_user().AuthUser().permissions['global']
227 return 'hg.extern_activate.auto' in def_user_perms
227 return 'hg.extern_activate.auto' in def_user_perms
228
228
229 def auth(self, userobj, username, password, settings, **kwargs):
229 def auth(self, userobj, username, password, settings, **kwargs):
230 """
230 """
231 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,
232 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()),
233 authenticate this user's login attempt.
233 authenticate this user's login attempt.
234
234
235 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:
236
236
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
237 see: RhodeCodeAuthPluginBase.auth_func_attrs
238 This is later validated for correctness
238 This is later validated for correctness
239 """
239 """
240 if not username or not password:
240 if not username or not password:
241 log.debug('Empty username or password skipping...')
241 log.debug('Empty username or password skipping...')
242 return None
242 return None
243
243
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
244 log.debug("Crowd settings: \n%s" % (formatted_json(settings)))
245 server = CrowdServer(**settings)
245 server = CrowdServer(**settings)
246 server.set_credentials(settings["app_name"], settings["app_password"])
246 server.set_credentials(settings["app_name"], settings["app_password"])
247 crowd_user = server.user_auth(username, password)
247 crowd_user = server.user_auth(username, password)
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
248 log.debug("Crowd returned: \n%s" % (formatted_json(crowd_user)))
249 if not crowd_user["status"]:
249 if not crowd_user["status"]:
250 return None
250 return None
251
251
252 res = server.user_groups(crowd_user["name"])
252 res = server.user_groups(crowd_user["name"])
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
253 log.debug("Crowd groups: \n%s" % (formatted_json(res)))
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
254 crowd_user["groups"] = [x["name"] for x in res["groups"]]
255
255
256 # old attrs fetched from RhodeCode database
256 # old attrs fetched from RhodeCode database
257 admin = getattr(userobj, 'admin', False)
257 admin = getattr(userobj, 'admin', False)
258 active = getattr(userobj, 'active', True)
258 active = getattr(userobj, 'active', True)
259 email = getattr(userobj, 'email', '')
259 email = getattr(userobj, 'email', '')
260 username = getattr(userobj, 'username', username)
260 username = getattr(userobj, 'username', username)
261 firstname = getattr(userobj, 'firstname', '')
261 firstname = getattr(userobj, 'firstname', '')
262 lastname = getattr(userobj, 'lastname', '')
262 lastname = getattr(userobj, 'lastname', '')
263 extern_type = getattr(userobj, 'extern_type', '')
263 extern_type = getattr(userobj, 'extern_type', '')
264
264
265 user_attrs = {
265 user_attrs = {
266 'username': username,
266 'username': username,
267 'firstname': crowd_user["first-name"] or firstname,
267 'firstname': crowd_user["first-name"] or firstname,
268 'lastname': crowd_user["last-name"] or lastname,
268 'lastname': crowd_user["last-name"] or lastname,
269 'groups': crowd_user["groups"],
269 'groups': crowd_user["groups"],
270 'user_group_sync': True,
270 'user_group_sync': True,
271 'email': crowd_user["email"] or email,
271 'email': crowd_user["email"] or email,
272 'admin': admin,
272 'admin': admin,
273 'active': active,
273 'active': active,
274 'active_from_extern': crowd_user.get('active'),
274 'active_from_extern': crowd_user.get('active'),
275 'extern_name': crowd_user["name"],
275 'extern_name': crowd_user["name"],
276 'extern_type': extern_type,
276 'extern_type': extern_type,
277 }
277 }
278
278
279 # set an admin if we're in admin_groups of crowd
279 # set an admin if we're in admin_groups of crowd
280 for group in settings["admin_groups"]:
280 for group in settings["admin_groups"]:
281 if group in user_attrs["groups"]:
281 if group in user_attrs["groups"]:
282 user_attrs["admin"] = True
282 user_attrs["admin"] = True
283 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
283 log.debug("Final crowd user object: \n%s" % (formatted_json(user_attrs)))
284 log.info('user %s authenticated correctly' % user_attrs['username'])
284 log.info('user `%s` authenticated correctly' % user_attrs['username'])
285 return user_attrs
285 return user_attrs
@@ -1,167 +1,167 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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 Jasig CAS
22 RhodeCode authentication plugin for Jasig CAS
23 http://www.jasig.org/cas
23 http://www.jasig.org/cas
24 """
24 """
25
25
26
26
27 import colander
27 import colander
28 import logging
28 import logging
29 import rhodecode
29 import rhodecode
30 import urllib
30 import urllib
31 import urllib2
31 import urllib2
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
39 from rhodecode.lib.utils2 import safe_unicode
39 from rhodecode.lib.utils2 import safe_unicode
40 from rhodecode.model.db import User
40 from rhodecode.model.db import User
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 def plugin_factory(plugin_id, *args, **kwds):
45 def plugin_factory(plugin_id, *args, **kwds):
46 """
46 """
47 Factory function that is called during plugin discovery.
47 Factory function that is called during plugin discovery.
48 It returns the plugin instance.
48 It returns the plugin instance.
49 """
49 """
50 plugin = RhodeCodeAuthPlugin(plugin_id)
50 plugin = RhodeCodeAuthPlugin(plugin_id)
51 return plugin
51 return plugin
52
52
53
53
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
54 class JasigCasAuthnResource(AuthnPluginResourceBase):
55 pass
55 pass
56
56
57
57
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
58 class JasigCasSettingsSchema(AuthnPluginSettingsSchemaBase):
59 service_url = colander.SchemaNode(
59 service_url = colander.SchemaNode(
60 colander.String(),
60 colander.String(),
61 default='https://domain.com/cas/v1/tickets',
61 default='https://domain.com/cas/v1/tickets',
62 description=_('The url of the Jasig CAS REST service'),
62 description=_('The url of the Jasig CAS REST service'),
63 preparer=strip_whitespace,
63 preparer=strip_whitespace,
64 title=_('URL'),
64 title=_('URL'),
65 widget='string')
65 widget='string')
66
66
67
67
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
68 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
69
69
70 def includeme(self, config):
70 def includeme(self, config):
71 config.add_authn_plugin(self)
71 config.add_authn_plugin(self)
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
72 config.add_authn_resource(self.get_id(), JasigCasAuthnResource(self))
73 config.add_view(
73 config.add_view(
74 'rhodecode.authentication.views.AuthnPluginViewBase',
74 'rhodecode.authentication.views.AuthnPluginViewBase',
75 attr='settings_get',
75 attr='settings_get',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
76 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
77 request_method='GET',
77 request_method='GET',
78 route_name='auth_home',
78 route_name='auth_home',
79 context=JasigCasAuthnResource)
79 context=JasigCasAuthnResource)
80 config.add_view(
80 config.add_view(
81 'rhodecode.authentication.views.AuthnPluginViewBase',
81 'rhodecode.authentication.views.AuthnPluginViewBase',
82 attr='settings_post',
82 attr='settings_post',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
83 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
84 request_method='POST',
84 request_method='POST',
85 route_name='auth_home',
85 route_name='auth_home',
86 context=JasigCasAuthnResource)
86 context=JasigCasAuthnResource)
87
87
88 def get_settings_schema(self):
88 def get_settings_schema(self):
89 return JasigCasSettingsSchema()
89 return JasigCasSettingsSchema()
90
90
91 def get_display_name(self):
91 def get_display_name(self):
92 return _('Jasig-CAS')
92 return _('Jasig-CAS')
93
93
94 @hybrid_property
94 @hybrid_property
95 def name(self):
95 def name(self):
96 return "jasig-cas"
96 return "jasig-cas"
97
97
98 @property
98 @property
99 def is_headers_auth(self):
99 def is_headers_auth(self):
100 return True
100 return True
101
101
102 def use_fake_password(self):
102 def use_fake_password(self):
103 return True
103 return True
104
104
105 def user_activation_state(self):
105 def user_activation_state(self):
106 def_user_perms = User.get_default_user().AuthUser().permissions['global']
106 def_user_perms = User.get_default_user().AuthUser().permissions['global']
107 return 'hg.extern_activate.auto' in def_user_perms
107 return 'hg.extern_activate.auto' in def_user_perms
108
108
109 def auth(self, userobj, username, password, settings, **kwargs):
109 def auth(self, userobj, username, password, settings, **kwargs):
110 """
110 """
111 Given a user object (which may be null), username, a plaintext password,
111 Given a user object (which may be null), username, a plaintext password,
112 and a settings object (containing all the keys needed as listed in settings()),
112 and a settings object (containing all the keys needed as listed in settings()),
113 authenticate this user's login attempt.
113 authenticate this user's login attempt.
114
114
115 Return None on failure. On success, return a dictionary of the form:
115 Return None on failure. On success, return a dictionary of the form:
116
116
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
117 see: RhodeCodeAuthPluginBase.auth_func_attrs
118 This is later validated for correctness
118 This is later validated for correctness
119 """
119 """
120 if not username or not password:
120 if not username or not password:
121 log.debug('Empty username or password skipping...')
121 log.debug('Empty username or password skipping...')
122 return None
122 return None
123
123
124 log.debug("Jasig CAS settings: %s", settings)
124 log.debug("Jasig CAS settings: %s", settings)
125 params = urllib.urlencode({'username': username, 'password': password})
125 params = urllib.urlencode({'username': username, 'password': password})
126 headers = {"Content-type": "application/x-www-form-urlencoded",
126 headers = {"Content-type": "application/x-www-form-urlencoded",
127 "Accept": "text/plain",
127 "Accept": "text/plain",
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
128 "User-Agent": "RhodeCode-auth-%s" % rhodecode.__version__}
129 url = settings["service_url"]
129 url = settings["service_url"]
130
130
131 log.debug("Sent Jasig CAS: \n%s",
131 log.debug("Sent Jasig CAS: \n%s",
132 {"url": url, "body": params, "headers": headers})
132 {"url": url, "body": params, "headers": headers})
133 request = urllib2.Request(url, params, headers)
133 request = urllib2.Request(url, params, headers)
134 try:
134 try:
135 response = urllib2.urlopen(request)
135 response = urllib2.urlopen(request)
136 except urllib2.HTTPError as e:
136 except urllib2.HTTPError as e:
137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
137 log.debug("HTTPError when requesting Jasig CAS (status code: %d)" % e.code)
138 return None
138 return None
139 except urllib2.URLError as e:
139 except urllib2.URLError as e:
140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
140 log.debug("URLError when requesting Jasig CAS url: %s " % url)
141 return None
141 return None
142
142
143 # old attrs fetched from RhodeCode database
143 # old attrs fetched from RhodeCode database
144 admin = getattr(userobj, 'admin', False)
144 admin = getattr(userobj, 'admin', False)
145 active = getattr(userobj, 'active', True)
145 active = getattr(userobj, 'active', True)
146 email = getattr(userobj, 'email', '')
146 email = getattr(userobj, 'email', '')
147 username = getattr(userobj, 'username', username)
147 username = getattr(userobj, 'username', username)
148 firstname = getattr(userobj, 'firstname', '')
148 firstname = getattr(userobj, 'firstname', '')
149 lastname = getattr(userobj, 'lastname', '')
149 lastname = getattr(userobj, 'lastname', '')
150 extern_type = getattr(userobj, 'extern_type', '')
150 extern_type = getattr(userobj, 'extern_type', '')
151
151
152 user_attrs = {
152 user_attrs = {
153 'username': username,
153 'username': username,
154 'firstname': safe_unicode(firstname or username),
154 'firstname': safe_unicode(firstname or username),
155 'lastname': safe_unicode(lastname or ''),
155 'lastname': safe_unicode(lastname or ''),
156 'groups': [],
156 'groups': [],
157 'user_group_sync': False,
157 'user_group_sync': False,
158 'email': email or '',
158 'email': email or '',
159 'admin': admin or False,
159 'admin': admin or False,
160 'active': active,
160 'active': active,
161 'active_from_extern': True,
161 'active_from_extern': True,
162 'extern_name': username,
162 'extern_name': username,
163 'extern_type': extern_type,
163 'extern_type': extern_type,
164 }
164 }
165
165
166 log.info('user %s authenticated correctly' % user_attrs['username'])
166 log.info('user `%s` authenticated correctly' % user_attrs['username'])
167 return user_attrs
167 return user_attrs
@@ -1,498 +1,498 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 import re
25 import re
26 import colander
26 import colander
27 import logging
27 import logging
28 import traceback
28 import traceback
29 import string
29 import string
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, chop_at, hybrid_property)
33 RhodeCodeExternalAuthPlugin, chop_at, 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.exceptions import (
37 from rhodecode.lib.exceptions import (
38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
38 LdapConnectionError, LdapUsernameError, LdapPasswordError, LdapImportError
39 )
39 )
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
40 from rhodecode.lib.utils2 import safe_unicode, safe_str
41 from rhodecode.model.db import User
41 from rhodecode.model.db import User
42 from rhodecode.model.validators import Missing
42 from rhodecode.model.validators import Missing
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46 try:
46 try:
47 import ldap
47 import ldap
48 except ImportError:
48 except ImportError:
49 # means that python-ldap is not installed, we use Missing object to mark
49 # means that python-ldap is not installed, we use Missing object to mark
50 # ldap lib is Missing
50 # ldap lib is Missing
51 ldap = Missing
51 ldap = Missing
52
52
53
53
54 class LdapError(Exception):
54 class LdapError(Exception):
55 pass
55 pass
56
56
57 def plugin_factory(plugin_id, *args, **kwds):
57 def plugin_factory(plugin_id, *args, **kwds):
58 """
58 """
59 Factory function that is called during plugin discovery.
59 Factory function that is called during plugin discovery.
60 It returns the plugin instance.
60 It returns the plugin instance.
61 """
61 """
62 plugin = RhodeCodeAuthPlugin(plugin_id)
62 plugin = RhodeCodeAuthPlugin(plugin_id)
63 return plugin
63 return plugin
64
64
65
65
66 class LdapAuthnResource(AuthnPluginResourceBase):
66 class LdapAuthnResource(AuthnPluginResourceBase):
67 pass
67 pass
68
68
69
69
70 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
70 class LdapSettingsSchema(AuthnPluginSettingsSchemaBase):
71 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
71 tls_kind_choices = ['PLAIN', 'LDAPS', 'START_TLS']
72 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
72 tls_reqcert_choices = ['NEVER', 'ALLOW', 'TRY', 'DEMAND', 'HARD']
73 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
73 search_scope_choices = ['BASE', 'ONELEVEL', 'SUBTREE']
74
74
75 host = colander.SchemaNode(
75 host = colander.SchemaNode(
76 colander.String(),
76 colander.String(),
77 default='',
77 default='',
78 description=_('Host[s] of the LDAP Server \n'
78 description=_('Host[s] of the LDAP Server \n'
79 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
79 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
80 'Multiple servers can be specified using commas'),
80 'Multiple servers can be specified using commas'),
81 preparer=strip_whitespace,
81 preparer=strip_whitespace,
82 title=_('LDAP Host'),
82 title=_('LDAP Host'),
83 widget='string')
83 widget='string')
84 port = colander.SchemaNode(
84 port = colander.SchemaNode(
85 colander.Int(),
85 colander.Int(),
86 default=389,
86 default=389,
87 description=_('Custom port that the LDAP server is listening on. '
87 description=_('Custom port that the LDAP server is listening on. '
88 'Default value is: 389'),
88 'Default value is: 389'),
89 preparer=strip_whitespace,
89 preparer=strip_whitespace,
90 title=_('Port'),
90 title=_('Port'),
91 validator=colander.Range(min=0, max=65536),
91 validator=colander.Range(min=0, max=65536),
92 widget='int')
92 widget='int')
93 dn_user = colander.SchemaNode(
93 dn_user = colander.SchemaNode(
94 colander.String(),
94 colander.String(),
95 default='',
95 default='',
96 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
96 description=_('Optional user DN/account to connect to LDAP if authentication is required. \n'
97 'e.g., cn=admin,dc=mydomain,dc=com, or '
97 'e.g., cn=admin,dc=mydomain,dc=com, or '
98 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
98 'uid=root,cn=users,dc=mydomain,dc=com, or admin@mydomain.com'),
99 missing='',
99 missing='',
100 preparer=strip_whitespace,
100 preparer=strip_whitespace,
101 title=_('Account'),
101 title=_('Account'),
102 widget='string')
102 widget='string')
103 dn_pass = colander.SchemaNode(
103 dn_pass = colander.SchemaNode(
104 colander.String(),
104 colander.String(),
105 default='',
105 default='',
106 description=_('Password to authenticate for given user DN.'),
106 description=_('Password to authenticate for given user DN.'),
107 missing='',
107 missing='',
108 preparer=strip_whitespace,
108 preparer=strip_whitespace,
109 title=_('Password'),
109 title=_('Password'),
110 widget='password')
110 widget='password')
111 tls_kind = colander.SchemaNode(
111 tls_kind = colander.SchemaNode(
112 colander.String(),
112 colander.String(),
113 default=tls_kind_choices[0],
113 default=tls_kind_choices[0],
114 description=_('TLS Type'),
114 description=_('TLS Type'),
115 title=_('Connection Security'),
115 title=_('Connection Security'),
116 validator=colander.OneOf(tls_kind_choices),
116 validator=colander.OneOf(tls_kind_choices),
117 widget='select')
117 widget='select')
118 tls_reqcert = colander.SchemaNode(
118 tls_reqcert = colander.SchemaNode(
119 colander.String(),
119 colander.String(),
120 default=tls_reqcert_choices[0],
120 default=tls_reqcert_choices[0],
121 description=_('Require Cert over TLS?. Self-signed and custom '
121 description=_('Require Cert over TLS?. Self-signed and custom '
122 'certificates can be used when\n `RhodeCode Certificate` '
122 'certificates can be used when\n `RhodeCode Certificate` '
123 'found in admin > settings > system info page is extended.'),
123 'found in admin > settings > system info page is extended.'),
124 title=_('Certificate Checks'),
124 title=_('Certificate Checks'),
125 validator=colander.OneOf(tls_reqcert_choices),
125 validator=colander.OneOf(tls_reqcert_choices),
126 widget='select')
126 widget='select')
127 base_dn = colander.SchemaNode(
127 base_dn = colander.SchemaNode(
128 colander.String(),
128 colander.String(),
129 default='',
129 default='',
130 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
130 description=_('Base DN to search. Dynamic bind is supported. Add `$login` marker '
131 'in it to be replaced with current user credentials \n'
131 'in it to be replaced with current user credentials \n'
132 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
132 '(e.g., dc=mydomain,dc=com, or ou=Users,dc=mydomain,dc=com)'),
133 missing='',
133 missing='',
134 preparer=strip_whitespace,
134 preparer=strip_whitespace,
135 title=_('Base DN'),
135 title=_('Base DN'),
136 widget='string')
136 widget='string')
137 filter = colander.SchemaNode(
137 filter = colander.SchemaNode(
138 colander.String(),
138 colander.String(),
139 default='',
139 default='',
140 description=_('Filter to narrow results \n'
140 description=_('Filter to narrow results \n'
141 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
141 '(e.g., (&(objectCategory=Person)(objectClass=user)), or \n'
142 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
142 '(memberof=cn=rc-login,ou=groups,ou=company,dc=mydomain,dc=com)))'),
143 missing='',
143 missing='',
144 preparer=strip_whitespace,
144 preparer=strip_whitespace,
145 title=_('LDAP Search Filter'),
145 title=_('LDAP Search Filter'),
146 widget='string')
146 widget='string')
147
147
148 search_scope = colander.SchemaNode(
148 search_scope = colander.SchemaNode(
149 colander.String(),
149 colander.String(),
150 default=search_scope_choices[2],
150 default=search_scope_choices[2],
151 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
151 description=_('How deep to search LDAP. If unsure set to SUBTREE'),
152 title=_('LDAP Search Scope'),
152 title=_('LDAP Search Scope'),
153 validator=colander.OneOf(search_scope_choices),
153 validator=colander.OneOf(search_scope_choices),
154 widget='select')
154 widget='select')
155 attr_login = colander.SchemaNode(
155 attr_login = colander.SchemaNode(
156 colander.String(),
156 colander.String(),
157 default='uid',
157 default='uid',
158 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
158 description=_('LDAP Attribute to map to user name (e.g., uid, or sAMAccountName)'),
159 preparer=strip_whitespace,
159 preparer=strip_whitespace,
160 title=_('Login Attribute'),
160 title=_('Login Attribute'),
161 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
161 missing_msg=_('The LDAP Login attribute of the CN must be specified'),
162 widget='string')
162 widget='string')
163 attr_firstname = colander.SchemaNode(
163 attr_firstname = colander.SchemaNode(
164 colander.String(),
164 colander.String(),
165 default='',
165 default='',
166 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
166 description=_('LDAP Attribute to map to first name (e.g., givenName)'),
167 missing='',
167 missing='',
168 preparer=strip_whitespace,
168 preparer=strip_whitespace,
169 title=_('First Name Attribute'),
169 title=_('First Name Attribute'),
170 widget='string')
170 widget='string')
171 attr_lastname = colander.SchemaNode(
171 attr_lastname = colander.SchemaNode(
172 colander.String(),
172 colander.String(),
173 default='',
173 default='',
174 description=_('LDAP Attribute to map to last name (e.g., sn)'),
174 description=_('LDAP Attribute to map to last name (e.g., sn)'),
175 missing='',
175 missing='',
176 preparer=strip_whitespace,
176 preparer=strip_whitespace,
177 title=_('Last Name Attribute'),
177 title=_('Last Name Attribute'),
178 widget='string')
178 widget='string')
179 attr_email = colander.SchemaNode(
179 attr_email = colander.SchemaNode(
180 colander.String(),
180 colander.String(),
181 default='',
181 default='',
182 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
182 description=_('LDAP Attribute to map to email address (e.g., mail).\n'
183 'Emails are a crucial part of RhodeCode. \n'
183 'Emails are a crucial part of RhodeCode. \n'
184 'If possible add a valid email attribute to ldap users.'),
184 'If possible add a valid email attribute to ldap users.'),
185 missing='',
185 missing='',
186 preparer=strip_whitespace,
186 preparer=strip_whitespace,
187 title=_('Email Attribute'),
187 title=_('Email Attribute'),
188 widget='string')
188 widget='string')
189
189
190
190
191 class AuthLdap(object):
191 class AuthLdap(object):
192
192
193 def _build_servers(self):
193 def _build_servers(self):
194 return ', '.join(
194 return ', '.join(
195 ["{}://{}:{}".format(
195 ["{}://{}:{}".format(
196 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
196 self.ldap_server_type, host.strip(), self.LDAP_SERVER_PORT)
197 for host in self.SERVER_ADDRESSES])
197 for host in self.SERVER_ADDRESSES])
198
198
199 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
199 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
200 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
200 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
201 search_scope='SUBTREE', attr_login='uid',
201 search_scope='SUBTREE', attr_login='uid',
202 ldap_filter=''):
202 ldap_filter=''):
203 if ldap == Missing:
203 if ldap == Missing:
204 raise LdapImportError("Missing or incompatible ldap library")
204 raise LdapImportError("Missing or incompatible ldap library")
205
205
206 self.debug = False
206 self.debug = False
207 self.ldap_version = ldap_version
207 self.ldap_version = ldap_version
208 self.ldap_server_type = 'ldap'
208 self.ldap_server_type = 'ldap'
209
209
210 self.TLS_KIND = tls_kind
210 self.TLS_KIND = tls_kind
211
211
212 if self.TLS_KIND == 'LDAPS':
212 if self.TLS_KIND == 'LDAPS':
213 port = port or 689
213 port = port or 689
214 self.ldap_server_type += 's'
214 self.ldap_server_type += 's'
215
215
216 OPT_X_TLS_DEMAND = 2
216 OPT_X_TLS_DEMAND = 2
217 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
217 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
218 OPT_X_TLS_DEMAND)
218 OPT_X_TLS_DEMAND)
219 # split server into list
219 # split server into list
220 self.SERVER_ADDRESSES = server.split(',')
220 self.SERVER_ADDRESSES = server.split(',')
221 self.LDAP_SERVER_PORT = port
221 self.LDAP_SERVER_PORT = port
222
222
223 # USE FOR READ ONLY BIND TO LDAP SERVER
223 # USE FOR READ ONLY BIND TO LDAP SERVER
224 self.attr_login = attr_login
224 self.attr_login = attr_login
225
225
226 self.LDAP_BIND_DN = safe_str(bind_dn)
226 self.LDAP_BIND_DN = safe_str(bind_dn)
227 self.LDAP_BIND_PASS = safe_str(bind_pass)
227 self.LDAP_BIND_PASS = safe_str(bind_pass)
228 self.LDAP_SERVER = self._build_servers()
228 self.LDAP_SERVER = self._build_servers()
229 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
229 self.SEARCH_SCOPE = getattr(ldap, 'SCOPE_%s' % search_scope)
230 self.BASE_DN = safe_str(base_dn)
230 self.BASE_DN = safe_str(base_dn)
231 self.LDAP_FILTER = safe_str(ldap_filter)
231 self.LDAP_FILTER = safe_str(ldap_filter)
232
232
233 def _get_ldap_conn(self):
233 def _get_ldap_conn(self):
234 if self.debug:
234 if self.debug:
235 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
235 ldap.set_option(ldap.OPT_DEBUG_LEVEL, 255)
236
236
237 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
237 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
238 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
238 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
239 '/etc/openldap/cacerts')
239 '/etc/openldap/cacerts')
240 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
240 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
241 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
241 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
242 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 60 * 10)
242 ldap.set_option(ldap.OPT_NETWORK_TIMEOUT, 60 * 10)
243 ldap.set_option(ldap.OPT_TIMEOUT, 60 * 10)
243 ldap.set_option(ldap.OPT_TIMEOUT, 60 * 10)
244
244
245 if self.TLS_KIND != 'PLAIN':
245 if self.TLS_KIND != 'PLAIN':
246 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
246 ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, self.TLS_REQCERT)
247
247
248 log.debug('initializing LDAP connection to:%s', self.LDAP_SERVER)
248 log.debug('initializing LDAP connection to:%s', self.LDAP_SERVER)
249 ldap_conn = ldap.initialize(self.LDAP_SERVER)
249 ldap_conn = ldap.initialize(self.LDAP_SERVER)
250 if self.ldap_version == 2:
250 if self.ldap_version == 2:
251 ldap_conn.protocol = ldap.VERSION2
251 ldap_conn.protocol = ldap.VERSION2
252 else:
252 else:
253 ldap_conn.protocol = ldap.VERSION3
253 ldap_conn.protocol = ldap.VERSION3
254
254
255 if self.TLS_KIND == 'START_TLS':
255 if self.TLS_KIND == 'START_TLS':
256 ldap_conn.start_tls_s()
256 ldap_conn.start_tls_s()
257
257
258 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
258 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
259 log.debug('Trying simple_bind with password and given login DN: %s',
259 log.debug('Trying simple_bind with password and given login DN: %s',
260 self.LDAP_BIND_DN)
260 self.LDAP_BIND_DN)
261 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
261 ldap_conn.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
262
262
263 return ldap_conn
263 return ldap_conn
264
264
265 def get_uid(self, username):
265 def get_uid(self, username):
266 uid = username
266 uid = username
267 for server_addr in self.SERVER_ADDRESSES:
267 for server_addr in self.SERVER_ADDRESSES:
268 uid = chop_at(username, "@%s" % server_addr)
268 uid = chop_at(username, "@%s" % server_addr)
269 return uid
269 return uid
270
270
271 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
271 def fetch_attrs_from_simple_bind(self, server, dn, username, password):
272 try:
272 try:
273 log.debug('Trying simple bind with %s', dn)
273 log.debug('Trying simple bind with %s', dn)
274 server.simple_bind_s(dn, safe_str(password))
274 server.simple_bind_s(dn, safe_str(password))
275 user = server.search_ext_s(
275 user = server.search_ext_s(
276 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
276 dn, ldap.SCOPE_BASE, '(objectClass=*)', )[0]
277 _, attrs = user
277 _, attrs = user
278 return attrs
278 return attrs
279
279
280 except ldap.INVALID_CREDENTIALS:
280 except ldap.INVALID_CREDENTIALS:
281 log.debug(
281 log.debug(
282 "LDAP rejected password for user '%s': %s, org_exc:",
282 "LDAP rejected password for user '%s': %s, org_exc:",
283 username, dn, exc_info=True)
283 username, dn, exc_info=True)
284
284
285 def authenticate_ldap(self, username, password):
285 def authenticate_ldap(self, username, password):
286 """
286 """
287 Authenticate a user via LDAP and return his/her LDAP properties.
287 Authenticate a user via LDAP and return his/her LDAP properties.
288
288
289 Raises AuthenticationError if the credentials are rejected, or
289 Raises AuthenticationError if the credentials are rejected, or
290 EnvironmentError if the LDAP server can't be reached.
290 EnvironmentError if the LDAP server can't be reached.
291
291
292 :param username: username
292 :param username: username
293 :param password: password
293 :param password: password
294 """
294 """
295
295
296 uid = self.get_uid(username)
296 uid = self.get_uid(username)
297
297
298 if not password:
298 if not password:
299 msg = "Authenticating user %s with blank password not allowed"
299 msg = "Authenticating user %s with blank password not allowed"
300 log.warning(msg, username)
300 log.warning(msg, username)
301 raise LdapPasswordError(msg)
301 raise LdapPasswordError(msg)
302 if "," in username:
302 if "," in username:
303 raise LdapUsernameError(
303 raise LdapUsernameError(
304 "invalid character `,` in username: `{}`".format(username))
304 "invalid character `,` in username: `{}`".format(username))
305 ldap_conn = None
305 ldap_conn = None
306 try:
306 try:
307 ldap_conn = self._get_ldap_conn()
307 ldap_conn = self._get_ldap_conn()
308 filter_ = '(&%s(%s=%s))' % (
308 filter_ = '(&%s(%s=%s))' % (
309 self.LDAP_FILTER, self.attr_login, username)
309 self.LDAP_FILTER, self.attr_login, username)
310 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
310 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
311 filter_, self.LDAP_SERVER)
311 filter_, self.LDAP_SERVER)
312 lobjects = ldap_conn.search_ext_s(
312 lobjects = ldap_conn.search_ext_s(
313 self.BASE_DN, self.SEARCH_SCOPE, filter_)
313 self.BASE_DN, self.SEARCH_SCOPE, filter_)
314
314
315 if not lobjects:
315 if not lobjects:
316 log.debug("No matching LDAP objects for authentication "
316 log.debug("No matching LDAP objects for authentication "
317 "of UID:'%s' username:(%s)", uid, username)
317 "of UID:'%s' username:(%s)", uid, username)
318 raise ldap.NO_SUCH_OBJECT()
318 raise ldap.NO_SUCH_OBJECT()
319
319
320 log.debug('Found matching ldap object, trying to authenticate')
320 log.debug('Found matching ldap object, trying to authenticate')
321 for (dn, _attrs) in lobjects:
321 for (dn, _attrs) in lobjects:
322 if dn is None:
322 if dn is None:
323 continue
323 continue
324
324
325 user_attrs = self.fetch_attrs_from_simple_bind(
325 user_attrs = self.fetch_attrs_from_simple_bind(
326 ldap_conn, dn, username, password)
326 ldap_conn, dn, username, password)
327 if user_attrs:
327 if user_attrs:
328 break
328 break
329
329
330 else:
330 else:
331 raise LdapPasswordError(
331 raise LdapPasswordError(
332 'Failed to authenticate user `{}`'
332 'Failed to authenticate user `{}`'
333 'with given password'.format(username))
333 'with given password'.format(username))
334
334
335 except ldap.NO_SUCH_OBJECT:
335 except ldap.NO_SUCH_OBJECT:
336 log.debug("LDAP says no such user '%s' (%s), org_exc:",
336 log.debug("LDAP says no such user '%s' (%s), org_exc:",
337 uid, username, exc_info=True)
337 uid, username, exc_info=True)
338 raise LdapUsernameError('Unable to find user')
338 raise LdapUsernameError('Unable to find user')
339 except ldap.SERVER_DOWN:
339 except ldap.SERVER_DOWN:
340 org_exc = traceback.format_exc()
340 org_exc = traceback.format_exc()
341 raise LdapConnectionError(
341 raise LdapConnectionError(
342 "LDAP can't access authentication "
342 "LDAP can't access authentication "
343 "server, org_exc:%s" % org_exc)
343 "server, org_exc:%s" % org_exc)
344 finally:
344 finally:
345 if ldap_conn:
345 if ldap_conn:
346 log.debug('ldap: connection release')
346 log.debug('ldap: connection release')
347 try:
347 try:
348 ldap_conn.unbind_s()
348 ldap_conn.unbind_s()
349 except Exception:
349 except Exception:
350 # for any reason this can raise exception we must catch it
350 # for any reason this can raise exception we must catch it
351 # to not crush the server
351 # to not crush the server
352 pass
352 pass
353
353
354 return dn, user_attrs
354 return dn, user_attrs
355
355
356
356
357 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
357 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
358 # used to define dynamic binding in the
358 # used to define dynamic binding in the
359 DYNAMIC_BIND_VAR = '$login'
359 DYNAMIC_BIND_VAR = '$login'
360 _settings_unsafe_keys = ['dn_pass']
360 _settings_unsafe_keys = ['dn_pass']
361
361
362 def includeme(self, config):
362 def includeme(self, config):
363 config.add_authn_plugin(self)
363 config.add_authn_plugin(self)
364 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
364 config.add_authn_resource(self.get_id(), LdapAuthnResource(self))
365 config.add_view(
365 config.add_view(
366 'rhodecode.authentication.views.AuthnPluginViewBase',
366 'rhodecode.authentication.views.AuthnPluginViewBase',
367 attr='settings_get',
367 attr='settings_get',
368 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
368 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
369 request_method='GET',
369 request_method='GET',
370 route_name='auth_home',
370 route_name='auth_home',
371 context=LdapAuthnResource)
371 context=LdapAuthnResource)
372 config.add_view(
372 config.add_view(
373 'rhodecode.authentication.views.AuthnPluginViewBase',
373 'rhodecode.authentication.views.AuthnPluginViewBase',
374 attr='settings_post',
374 attr='settings_post',
375 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
375 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
376 request_method='POST',
376 request_method='POST',
377 route_name='auth_home',
377 route_name='auth_home',
378 context=LdapAuthnResource)
378 context=LdapAuthnResource)
379
379
380 def get_settings_schema(self):
380 def get_settings_schema(self):
381 return LdapSettingsSchema()
381 return LdapSettingsSchema()
382
382
383 def get_display_name(self):
383 def get_display_name(self):
384 return _('LDAP')
384 return _('LDAP')
385
385
386 @hybrid_property
386 @hybrid_property
387 def name(self):
387 def name(self):
388 return "ldap"
388 return "ldap"
389
389
390 def use_fake_password(self):
390 def use_fake_password(self):
391 return True
391 return True
392
392
393 def user_activation_state(self):
393 def user_activation_state(self):
394 def_user_perms = User.get_default_user().AuthUser().permissions['global']
394 def_user_perms = User.get_default_user().AuthUser().permissions['global']
395 return 'hg.extern_activate.auto' in def_user_perms
395 return 'hg.extern_activate.auto' in def_user_perms
396
396
397 def try_dynamic_binding(self, username, password, current_args):
397 def try_dynamic_binding(self, username, password, current_args):
398 """
398 """
399 Detects marker inside our original bind, and uses dynamic auth if
399 Detects marker inside our original bind, and uses dynamic auth if
400 present
400 present
401 """
401 """
402
402
403 org_bind = current_args['bind_dn']
403 org_bind = current_args['bind_dn']
404 passwd = current_args['bind_pass']
404 passwd = current_args['bind_pass']
405
405
406 def has_bind_marker(username):
406 def has_bind_marker(username):
407 if self.DYNAMIC_BIND_VAR in username:
407 if self.DYNAMIC_BIND_VAR in username:
408 return True
408 return True
409
409
410 # we only passed in user with "special" variable
410 # we only passed in user with "special" variable
411 if org_bind and has_bind_marker(org_bind) and not passwd:
411 if org_bind and has_bind_marker(org_bind) and not passwd:
412 log.debug('Using dynamic user/password binding for ldap '
412 log.debug('Using dynamic user/password binding for ldap '
413 'authentication. Replacing `%s` with username',
413 'authentication. Replacing `%s` with username',
414 self.DYNAMIC_BIND_VAR)
414 self.DYNAMIC_BIND_VAR)
415 current_args['bind_dn'] = org_bind.replace(
415 current_args['bind_dn'] = org_bind.replace(
416 self.DYNAMIC_BIND_VAR, username)
416 self.DYNAMIC_BIND_VAR, username)
417 current_args['bind_pass'] = password
417 current_args['bind_pass'] = password
418
418
419 return current_args
419 return current_args
420
420
421 def auth(self, userobj, username, password, settings, **kwargs):
421 def auth(self, userobj, username, password, settings, **kwargs):
422 """
422 """
423 Given a user object (which may be null), username, a plaintext password,
423 Given a user object (which may be null), username, a plaintext password,
424 and a settings object (containing all the keys needed as listed in
424 and a settings object (containing all the keys needed as listed in
425 settings()), authenticate this user's login attempt.
425 settings()), authenticate this user's login attempt.
426
426
427 Return None on failure. On success, return a dictionary of the form:
427 Return None on failure. On success, return a dictionary of the form:
428
428
429 see: RhodeCodeAuthPluginBase.auth_func_attrs
429 see: RhodeCodeAuthPluginBase.auth_func_attrs
430 This is later validated for correctness
430 This is later validated for correctness
431 """
431 """
432
432
433 if not username or not password:
433 if not username or not password:
434 log.debug('Empty username or password skipping...')
434 log.debug('Empty username or password skipping...')
435 return None
435 return None
436
436
437 ldap_args = {
437 ldap_args = {
438 'server': settings.get('host', ''),
438 'server': settings.get('host', ''),
439 'base_dn': settings.get('base_dn', ''),
439 'base_dn': settings.get('base_dn', ''),
440 'port': settings.get('port'),
440 'port': settings.get('port'),
441 'bind_dn': settings.get('dn_user'),
441 'bind_dn': settings.get('dn_user'),
442 'bind_pass': settings.get('dn_pass'),
442 'bind_pass': settings.get('dn_pass'),
443 'tls_kind': settings.get('tls_kind'),
443 'tls_kind': settings.get('tls_kind'),
444 'tls_reqcert': settings.get('tls_reqcert'),
444 'tls_reqcert': settings.get('tls_reqcert'),
445 'search_scope': settings.get('search_scope'),
445 'search_scope': settings.get('search_scope'),
446 'attr_login': settings.get('attr_login'),
446 'attr_login': settings.get('attr_login'),
447 'ldap_version': 3,
447 'ldap_version': 3,
448 'ldap_filter': settings.get('filter'),
448 'ldap_filter': settings.get('filter'),
449 }
449 }
450
450
451 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
451 ldap_attrs = self.try_dynamic_binding(username, password, ldap_args)
452
452
453 log.debug('Checking for ldap authentication.')
453 log.debug('Checking for ldap authentication.')
454
454
455 try:
455 try:
456 aldap = AuthLdap(**ldap_args)
456 aldap = AuthLdap(**ldap_args)
457 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
457 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username, password)
458 log.debug('Got ldap DN response %s', user_dn)
458 log.debug('Got ldap DN response %s', user_dn)
459
459
460 def get_ldap_attr(k):
460 def get_ldap_attr(k):
461 return ldap_attrs.get(settings.get(k), [''])[0]
461 return ldap_attrs.get(settings.get(k), [''])[0]
462
462
463 # old attrs fetched from RhodeCode database
463 # old attrs fetched from RhodeCode database
464 admin = getattr(userobj, 'admin', False)
464 admin = getattr(userobj, 'admin', False)
465 active = getattr(userobj, 'active', True)
465 active = getattr(userobj, 'active', True)
466 email = getattr(userobj, 'email', '')
466 email = getattr(userobj, 'email', '')
467 username = getattr(userobj, 'username', username)
467 username = getattr(userobj, 'username', username)
468 firstname = getattr(userobj, 'firstname', '')
468 firstname = getattr(userobj, 'firstname', '')
469 lastname = getattr(userobj, 'lastname', '')
469 lastname = getattr(userobj, 'lastname', '')
470 extern_type = getattr(userobj, 'extern_type', '')
470 extern_type = getattr(userobj, 'extern_type', '')
471
471
472 groups = []
472 groups = []
473 user_attrs = {
473 user_attrs = {
474 'username': username,
474 'username': username,
475 'firstname': safe_unicode(
475 'firstname': safe_unicode(
476 get_ldap_attr('attr_firstname') or firstname),
476 get_ldap_attr('attr_firstname') or firstname),
477 'lastname': safe_unicode(
477 'lastname': safe_unicode(
478 get_ldap_attr('attr_lastname') or lastname),
478 get_ldap_attr('attr_lastname') or lastname),
479 'groups': groups,
479 'groups': groups,
480 'user_group_sync': False,
480 'user_group_sync': False,
481 'email': get_ldap_attr('attr_email') or email,
481 'email': get_ldap_attr('attr_email') or email,
482 'admin': admin,
482 'admin': admin,
483 'active': active,
483 'active': active,
484 'active_from_extern': None,
484 'active_from_extern': None,
485 'extern_name': user_dn,
485 'extern_name': user_dn,
486 'extern_type': extern_type,
486 'extern_type': extern_type,
487 }
487 }
488 log.debug('ldap user: %s', user_attrs)
488 log.debug('ldap user: %s', user_attrs)
489 log.info('user %s authenticated correctly', user_attrs['username'])
489 log.info('user `%s` authenticated correctly', user_attrs['username'])
490
490
491 return user_attrs
491 return user_attrs
492
492
493 except (LdapUsernameError, LdapPasswordError, LdapImportError):
493 except (LdapUsernameError, LdapPasswordError, LdapImportError):
494 log.exception("LDAP related exception")
494 log.exception("LDAP related exception")
495 return None
495 return None
496 except (Exception,):
496 except (Exception,):
497 log.exception("Other exception")
497 log.exception("Other exception")
498 return None
498 return None
@@ -1,161 +1,161 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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 library for PAM
22 RhodeCode authentication library for PAM
23 """
23 """
24
24
25 import colander
25 import colander
26 import grp
26 import grp
27 import logging
27 import logging
28 import pam
28 import pam
29 import pwd
29 import pwd
30 import re
30 import re
31 import socket
31 import socket
32
32
33 from rhodecode.translation import _
33 from rhodecode.translation import _
34 from rhodecode.authentication.base import (
34 from rhodecode.authentication.base import (
35 RhodeCodeExternalAuthPlugin, hybrid_property)
35 RhodeCodeExternalAuthPlugin, hybrid_property)
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
36 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
37 from rhodecode.authentication.routes import AuthnPluginResourceBase
38 from rhodecode.lib.colander_utils import strip_whitespace
38 from rhodecode.lib.colander_utils import strip_whitespace
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 PamAuthnResource(AuthnPluginResourceBase):
52 class PamAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class PamSettingsSchema(AuthnPluginSettingsSchemaBase):
57 service = colander.SchemaNode(
57 service = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='login',
59 default='login',
60 description=_('PAM service name to use for authentication.'),
60 description=_('PAM service name to use for authentication.'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('PAM service name'),
62 title=_('PAM service name'),
63 widget='string')
63 widget='string')
64 gecos = colander.SchemaNode(
64 gecos = colander.SchemaNode(
65 colander.String(),
65 colander.String(),
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
66 default='(?P<last_name>.+),\s*(?P<first_name>\w+)',
67 description=_('Regular expression for extracting user name/email etc. '
67 description=_('Regular expression for extracting user name/email etc. '
68 'from Unix userinfo.'),
68 'from Unix userinfo.'),
69 preparer=strip_whitespace,
69 preparer=strip_whitespace,
70 title=_('Gecos Regex'),
70 title=_('Gecos Regex'),
71 widget='string')
71 widget='string')
72
72
73
73
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
74 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
75 # PAM authentication can be slow. Repository operations involve a lot of
75 # PAM authentication can be slow. Repository operations involve a lot of
76 # auth calls. Little caching helps speedup push/pull operations significantly
76 # auth calls. Little caching helps speedup push/pull operations significantly
77 AUTH_CACHE_TTL = 4
77 AUTH_CACHE_TTL = 4
78
78
79 def includeme(self, config):
79 def includeme(self, config):
80 config.add_authn_plugin(self)
80 config.add_authn_plugin(self)
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
81 config.add_authn_resource(self.get_id(), PamAuthnResource(self))
82 config.add_view(
82 config.add_view(
83 'rhodecode.authentication.views.AuthnPluginViewBase',
83 'rhodecode.authentication.views.AuthnPluginViewBase',
84 attr='settings_get',
84 attr='settings_get',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
85 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
86 request_method='GET',
86 request_method='GET',
87 route_name='auth_home',
87 route_name='auth_home',
88 context=PamAuthnResource)
88 context=PamAuthnResource)
89 config.add_view(
89 config.add_view(
90 'rhodecode.authentication.views.AuthnPluginViewBase',
90 'rhodecode.authentication.views.AuthnPluginViewBase',
91 attr='settings_post',
91 attr='settings_post',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
92 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
93 request_method='POST',
93 request_method='POST',
94 route_name='auth_home',
94 route_name='auth_home',
95 context=PamAuthnResource)
95 context=PamAuthnResource)
96
96
97 def get_display_name(self):
97 def get_display_name(self):
98 return _('PAM')
98 return _('PAM')
99
99
100 @hybrid_property
100 @hybrid_property
101 def name(self):
101 def name(self):
102 return "pam"
102 return "pam"
103
103
104 def get_settings_schema(self):
104 def get_settings_schema(self):
105 return PamSettingsSchema()
105 return PamSettingsSchema()
106
106
107 def use_fake_password(self):
107 def use_fake_password(self):
108 return True
108 return True
109
109
110 def auth(self, userobj, username, password, settings, **kwargs):
110 def auth(self, userobj, username, password, settings, **kwargs):
111 if not username or not password:
111 if not username or not password:
112 log.debug('Empty username or password skipping...')
112 log.debug('Empty username or password skipping...')
113 return None
113 return None
114
114
115 auth_result = pam.authenticate(username, password, settings["service"])
115 auth_result = pam.authenticate(username, password, settings["service"])
116
116
117 if not auth_result:
117 if not auth_result:
118 log.error("PAM was unable to authenticate user: %s" % (username, ))
118 log.error("PAM was unable to authenticate user: %s" % (username, ))
119 return None
119 return None
120
120
121 log.debug('Got PAM response %s' % (auth_result, ))
121 log.debug('Got PAM response %s' % (auth_result, ))
122
122
123 # old attrs fetched from RhodeCode database
123 # old attrs fetched from RhodeCode database
124 default_email = "%s@%s" % (username, socket.gethostname())
124 default_email = "%s@%s" % (username, socket.gethostname())
125 admin = getattr(userobj, 'admin', False)
125 admin = getattr(userobj, 'admin', False)
126 active = getattr(userobj, 'active', True)
126 active = getattr(userobj, 'active', True)
127 email = getattr(userobj, 'email', '') or default_email
127 email = getattr(userobj, 'email', '') or default_email
128 username = getattr(userobj, 'username', username)
128 username = getattr(userobj, 'username', username)
129 firstname = getattr(userobj, 'firstname', '')
129 firstname = getattr(userobj, 'firstname', '')
130 lastname = getattr(userobj, 'lastname', '')
130 lastname = getattr(userobj, 'lastname', '')
131 extern_type = getattr(userobj, 'extern_type', '')
131 extern_type = getattr(userobj, 'extern_type', '')
132
132
133 user_attrs = {
133 user_attrs = {
134 'username': username,
134 'username': username,
135 'firstname': firstname,
135 'firstname': firstname,
136 'lastname': lastname,
136 'lastname': lastname,
137 'groups': [g.gr_name for g in grp.getgrall()
137 'groups': [g.gr_name for g in grp.getgrall()
138 if username in g.gr_mem],
138 if username in g.gr_mem],
139 'user_group_sync': True,
139 'user_group_sync': True,
140 'email': email,
140 'email': email,
141 'admin': admin,
141 'admin': admin,
142 'active': active,
142 'active': active,
143 'active_from_extern': None,
143 'active_from_extern': None,
144 'extern_name': username,
144 'extern_name': username,
145 'extern_type': extern_type,
145 'extern_type': extern_type,
146 }
146 }
147
147
148 try:
148 try:
149 user_data = pwd.getpwnam(username)
149 user_data = pwd.getpwnam(username)
150 regex = settings["gecos"]
150 regex = settings["gecos"]
151 match = re.search(regex, user_data.pw_gecos)
151 match = re.search(regex, user_data.pw_gecos)
152 if match:
152 if match:
153 user_attrs["firstname"] = match.group('first_name')
153 user_attrs["firstname"] = match.group('first_name')
154 user_attrs["lastname"] = match.group('last_name')
154 user_attrs["lastname"] = match.group('last_name')
155 except Exception:
155 except Exception:
156 log.warning("Cannot extract additional info for PAM user")
156 log.warning("Cannot extract additional info for PAM user")
157 pass
157 pass
158
158
159 log.debug("pamuser: %s", user_attrs)
159 log.debug("pamuser: %s", user_attrs)
160 log.info('user %s authenticated correctly' % user_attrs['username'])
160 log.info('user `%s` authenticated correctly' % user_attrs['username'])
161 return user_attrs
161 return user_attrs
@@ -1,143 +1,143 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2018 RhodeCode GmbH
3 # Copyright (C) 2012-2018 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 built in internal auth
22 RhodeCode authentication plugin for built in internal auth
23 """
23 """
24
24
25 import logging
25 import logging
26
26
27 from rhodecode.translation import _
27 from rhodecode.translation import _
28
28
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
29 from rhodecode.authentication.base import RhodeCodeAuthPluginBase, hybrid_property
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
30 from rhodecode.authentication.routes import AuthnPluginResourceBase
31 from rhodecode.lib.utils2 import safe_str
31 from rhodecode.lib.utils2 import safe_str
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33
33
34 log = logging.getLogger(__name__)
34 log = logging.getLogger(__name__)
35
35
36
36
37 def plugin_factory(plugin_id, *args, **kwds):
37 def plugin_factory(plugin_id, *args, **kwds):
38 plugin = RhodeCodeAuthPlugin(plugin_id)
38 plugin = RhodeCodeAuthPlugin(plugin_id)
39 return plugin
39 return plugin
40
40
41
41
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
42 class RhodecodeAuthnResource(AuthnPluginResourceBase):
43 pass
43 pass
44
44
45
45
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
46 class RhodeCodeAuthPlugin(RhodeCodeAuthPluginBase):
47
47
48 def includeme(self, config):
48 def includeme(self, config):
49 config.add_authn_plugin(self)
49 config.add_authn_plugin(self)
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
50 config.add_authn_resource(self.get_id(), RhodecodeAuthnResource(self))
51 config.add_view(
51 config.add_view(
52 'rhodecode.authentication.views.AuthnPluginViewBase',
52 'rhodecode.authentication.views.AuthnPluginViewBase',
53 attr='settings_get',
53 attr='settings_get',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
54 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
55 request_method='GET',
55 request_method='GET',
56 route_name='auth_home',
56 route_name='auth_home',
57 context=RhodecodeAuthnResource)
57 context=RhodecodeAuthnResource)
58 config.add_view(
58 config.add_view(
59 'rhodecode.authentication.views.AuthnPluginViewBase',
59 'rhodecode.authentication.views.AuthnPluginViewBase',
60 attr='settings_post',
60 attr='settings_post',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
61 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
62 request_method='POST',
62 request_method='POST',
63 route_name='auth_home',
63 route_name='auth_home',
64 context=RhodecodeAuthnResource)
64 context=RhodecodeAuthnResource)
65
65
66 def get_display_name(self):
66 def get_display_name(self):
67 return _('Rhodecode')
67 return _('Rhodecode')
68
68
69 @hybrid_property
69 @hybrid_property
70 def name(self):
70 def name(self):
71 return "rhodecode"
71 return "rhodecode"
72
72
73 def user_activation_state(self):
73 def user_activation_state(self):
74 def_user_perms = User.get_default_user().AuthUser().permissions['global']
74 def_user_perms = User.get_default_user().AuthUser().permissions['global']
75 return 'hg.register.auto_activate' in def_user_perms
75 return 'hg.register.auto_activate' in def_user_perms
76
76
77 def allows_authentication_from(
77 def allows_authentication_from(
78 self, user, allows_non_existing_user=True,
78 self, user, allows_non_existing_user=True,
79 allowed_auth_plugins=None, allowed_auth_sources=None):
79 allowed_auth_plugins=None, allowed_auth_sources=None):
80 """
80 """
81 Custom method for this auth that doesn't accept non existing users.
81 Custom method for this auth that doesn't accept non existing users.
82 We know that user exists in our database.
82 We know that user exists in our database.
83 """
83 """
84 allows_non_existing_user = False
84 allows_non_existing_user = False
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
85 return super(RhodeCodeAuthPlugin, self).allows_authentication_from(
86 user, allows_non_existing_user=allows_non_existing_user)
86 user, allows_non_existing_user=allows_non_existing_user)
87
87
88 def auth(self, userobj, username, password, settings, **kwargs):
88 def auth(self, userobj, username, password, settings, **kwargs):
89 if not userobj:
89 if not userobj:
90 log.debug('userobj was:%s skipping' % (userobj, ))
90 log.debug('userobj was:%s skipping' % (userobj, ))
91 return None
91 return None
92 if userobj.extern_type != self.name:
92 if userobj.extern_type != self.name:
93 log.warning(
93 log.warning(
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
94 "userobj:%s extern_type mismatch got:`%s` expected:`%s`" %
95 (userobj, userobj.extern_type, self.name))
95 (userobj, userobj.extern_type, self.name))
96 return None
96 return None
97
97
98 user_attrs = {
98 user_attrs = {
99 "username": userobj.username,
99 "username": userobj.username,
100 "firstname": userobj.firstname,
100 "firstname": userobj.firstname,
101 "lastname": userobj.lastname,
101 "lastname": userobj.lastname,
102 "groups": [],
102 "groups": [],
103 'user_group_sync': False,
103 'user_group_sync': False,
104 "email": userobj.email,
104 "email": userobj.email,
105 "admin": userobj.admin,
105 "admin": userobj.admin,
106 "active": userobj.active,
106 "active": userobj.active,
107 "active_from_extern": userobj.active,
107 "active_from_extern": userobj.active,
108 "extern_name": userobj.user_id,
108 "extern_name": userobj.user_id,
109 "extern_type": userobj.extern_type,
109 "extern_type": userobj.extern_type,
110 }
110 }
111
111
112 log.debug("User attributes:%s" % (user_attrs, ))
112 log.debug("User attributes:%s" % (user_attrs, ))
113 if userobj.active:
113 if userobj.active:
114 from rhodecode.lib import auth
114 from rhodecode.lib import auth
115 crypto_backend = auth.crypto_backend()
115 crypto_backend = auth.crypto_backend()
116 password_encoded = safe_str(password)
116 password_encoded = safe_str(password)
117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
117 password_match, new_hash = crypto_backend.hash_check_with_upgrade(
118 password_encoded, userobj.password or '')
118 password_encoded, userobj.password or '')
119
119
120 if password_match and new_hash:
120 if password_match and new_hash:
121 log.debug('user %s properly authenticated, but '
121 log.debug('user %s properly authenticated, but '
122 'requires hash change to bcrypt', userobj)
122 'requires hash change to bcrypt', userobj)
123 # if password match, and we use OLD deprecated hash,
123 # if password match, and we use OLD deprecated hash,
124 # we should migrate this user hash password to the new hash
124 # we should migrate this user hash password to the new hash
125 # we store the new returned by hash_check_with_upgrade function
125 # we store the new returned by hash_check_with_upgrade function
126 user_attrs['_hash_migrate'] = new_hash
126 user_attrs['_hash_migrate'] = new_hash
127
127
128 if userobj.username == User.DEFAULT_USER and userobj.active:
128 if userobj.username == User.DEFAULT_USER and userobj.active:
129 log.info(
129 log.info(
130 'user %s authenticated correctly as anonymous user', userobj)
130 'user `%s` authenticated correctly as anonymous user', userobj)
131 return user_attrs
131 return user_attrs
132
132
133 elif userobj.username == username and password_match:
133 elif userobj.username == username and password_match:
134 log.info('user %s authenticated correctly', userobj)
134 log.info('user `%s` authenticated correctly', userobj)
135 return user_attrs
135 return user_attrs
136 log.info("user %s had a bad password when "
136 log.info("user %s had a bad password when "
137 "authenticating on this plugin", userobj)
137 "authenticating on this plugin", userobj)
138 return None
138 return None
139 else:
139 else:
140 log.warning(
140 log.warning(
141 'user `%s` failed to authenticate via %s, reason: account not '
141 'user `%s` failed to authenticate via %s, reason: account not '
142 'active.', username, self.name)
142 'active.', username, self.name)
143 return None
143 return None
@@ -1,109 +1,109 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2018 RhodeCode GmbH
3 # Copyright (C) 2010-2018 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 External module for testing plugins
22 External module for testing plugins
23
23
24 rhodecode.tests.auth_external_test
24 rhodecode.tests.auth_external_test
25
25
26 """
26 """
27 import logging
27 import logging
28 import traceback
28 import traceback
29
29
30 from rhodecode.authentication.base import (
30 from rhodecode.authentication.base import (
31 RhodeCodeExternalAuthPlugin, hybrid_property)
31 RhodeCodeExternalAuthPlugin, hybrid_property)
32 from rhodecode.model.db import User
32 from rhodecode.model.db import User
33 from rhodecode.lib.ext_json import formatted_json
33 from rhodecode.lib.ext_json import formatted_json
34
34
35 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
36
36
37
37
38 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
38 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
39 def __init__(self):
39 def __init__(self):
40 self._logger = logging.getLogger(__name__)
40 self._logger = logging.getLogger(__name__)
41
41
42 @hybrid_property
42 @hybrid_property
43 def allows_creating_users(self):
43 def allows_creating_users(self):
44 return True
44 return True
45
45
46 @hybrid_property
46 @hybrid_property
47 def name(self):
47 def name(self):
48 return "external_test"
48 return "external_test"
49
49
50 def settings(self):
50 def settings(self):
51 settings = [
51 settings = [
52 ]
52 ]
53 return settings
53 return settings
54
54
55 def use_fake_password(self):
55 def use_fake_password(self):
56 return True
56 return True
57
57
58 def user_activation_state(self):
58 def user_activation_state(self):
59 def_user_perms = User.get_default_user().AuthUser().permissions['global']
59 def_user_perms = User.get_default_user().AuthUser().permissions['global']
60 return 'hg.extern_activate.auto' in def_user_perms
60 return 'hg.extern_activate.auto' in def_user_perms
61
61
62 def auth(self, userobj, username, password, settings, **kwargs):
62 def auth(self, userobj, username, password, settings, **kwargs):
63 """
63 """
64 Given a user object (which may be null), username, a plaintext password,
64 Given a user object (which may be null), username, a plaintext password,
65 and a settings object (containing all the keys needed as listed in settings()),
65 and a settings object (containing all the keys needed as listed in settings()),
66 authenticate this user's login attempt.
66 authenticate this user's login attempt.
67
67
68 Return None on failure. On success, return a dictionary of the form:
68 Return None on failure. On success, return a dictionary of the form:
69
69
70 see: RhodeCodeAuthPluginBase.auth_func_attrs
70 see: RhodeCodeAuthPluginBase.auth_func_attrs
71 This is later validated for correctness
71 This is later validated for correctness
72 """
72 """
73
73
74 if not username or not password:
74 if not username or not password:
75 log.debug('Empty username or password skipping...')
75 log.debug('Empty username or password skipping...')
76 return None
76 return None
77
77
78 try:
78 try:
79 user_dn = username
79 user_dn = username
80
80
81 # # old attrs fetched from RhodeCode database
81 # # old attrs fetched from RhodeCode database
82 admin = getattr(userobj, 'admin', False)
82 admin = getattr(userobj, 'admin', False)
83 active = getattr(userobj, 'active', True)
83 active = getattr(userobj, 'active', True)
84 email = getattr(userobj, 'email', '')
84 email = getattr(userobj, 'email', '')
85 firstname = getattr(userobj, 'firstname', '')
85 firstname = getattr(userobj, 'firstname', '')
86 lastname = getattr(userobj, 'lastname', '')
86 lastname = getattr(userobj, 'lastname', '')
87 extern_type = getattr(userobj, 'extern_type', '')
87 extern_type = getattr(userobj, 'extern_type', '')
88 #
88 #
89 user_attrs = {
89 user_attrs = {
90 'username': username,
90 'username': username,
91 'firstname': firstname,
91 'firstname': firstname,
92 'lastname': lastname,
92 'lastname': lastname,
93 'groups': [],
93 'groups': [],
94 'email': '%s@rhodecode.com' % username,
94 'email': '%s@rhodecode.com' % username,
95 'admin': admin,
95 'admin': admin,
96 'active': active,
96 'active': active,
97 "active_from_extern": None,
97 "active_from_extern": None,
98 'extern_name': user_dn,
98 'extern_name': user_dn,
99 'extern_type': extern_type,
99 'extern_type': extern_type,
100 }
100 }
101
101
102 log.debug('EXTERNAL user: \n%s' % formatted_json(user_attrs))
102 log.debug('EXTERNAL user: \n%s' % formatted_json(user_attrs))
103 log.info('user %s authenticated correctly' % user_attrs['username'])
103 log.info('user `%s` authenticated correctly' % user_attrs['username'])
104
104
105 return user_attrs
105 return user_attrs
106
106
107 except (Exception,):
107 except (Exception,):
108 log.error(traceback.format_exc())
108 log.error(traceback.format_exc())
109 return None
109 return None
General Comments 0
You need to be logged in to leave comments. Login now