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