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