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