##// END OF EJS Templates
auth-crawd: py3 compat
dan -
r4350:365337eb default
parent child Browse files
Show More
@@ -1,295 +1,295 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2012-2020 RhodeCode GmbH
3 # Copyright (C) 2012-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 RhodeCode authentication plugin for Atlassian CROWD
22 RhodeCode authentication plugin for Atlassian CROWD
23 """
23 """
24
24
25
25
26 import colander
26 import colander
27 import base64
27 import base64
28 import logging
28 import logging
29 import urllib2
29 import urllib2
30
30
31 from rhodecode.translation import _
31 from rhodecode.translation import _
32 from rhodecode.authentication.base import (
32 from rhodecode.authentication.base import (
33 RhodeCodeExternalAuthPlugin, hybrid_property)
33 RhodeCodeExternalAuthPlugin, hybrid_property)
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
34 from rhodecode.authentication.schema import AuthnPluginSettingsSchemaBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
35 from rhodecode.authentication.routes import AuthnPluginResourceBase
36 from rhodecode.lib.colander_utils import strip_whitespace
36 from rhodecode.lib.colander_utils import strip_whitespace
37 from rhodecode.lib.ext_json import json, formatted_json
37 from rhodecode.lib.ext_json import json, formatted_json
38 from rhodecode.model.db import User
38 from rhodecode.model.db import User
39
39
40 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
41
41
42
42
43 def plugin_factory(plugin_id, *args, **kwargs):
43 def plugin_factory(plugin_id, *args, **kwargs):
44 """
44 """
45 Factory function that is called during plugin discovery.
45 Factory function that is called during plugin discovery.
46 It returns the plugin instance.
46 It returns the plugin instance.
47 """
47 """
48 plugin = RhodeCodeAuthPlugin(plugin_id)
48 plugin = RhodeCodeAuthPlugin(plugin_id)
49 return plugin
49 return plugin
50
50
51
51
52 class CrowdAuthnResource(AuthnPluginResourceBase):
52 class CrowdAuthnResource(AuthnPluginResourceBase):
53 pass
53 pass
54
54
55
55
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
56 class CrowdSettingsSchema(AuthnPluginSettingsSchemaBase):
57 host = colander.SchemaNode(
57 host = colander.SchemaNode(
58 colander.String(),
58 colander.String(),
59 default='127.0.0.1',
59 default='127.0.0.1',
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
60 description=_('The FQDN or IP of the Atlassian CROWD Server'),
61 preparer=strip_whitespace,
61 preparer=strip_whitespace,
62 title=_('Host'),
62 title=_('Host'),
63 widget='string')
63 widget='string')
64 port = colander.SchemaNode(
64 port = colander.SchemaNode(
65 colander.Int(),
65 colander.Int(),
66 default=8095,
66 default=8095,
67 description=_('The Port in use by the Atlassian CROWD Server'),
67 description=_('The Port in use by the Atlassian CROWD Server'),
68 preparer=strip_whitespace,
68 preparer=strip_whitespace,
69 title=_('Port'),
69 title=_('Port'),
70 validator=colander.Range(min=0, max=65536),
70 validator=colander.Range(min=0, max=65536),
71 widget='int')
71 widget='int')
72 app_name = colander.SchemaNode(
72 app_name = colander.SchemaNode(
73 colander.String(),
73 colander.String(),
74 default='',
74 default='',
75 description=_('The Application Name to authenticate to CROWD'),
75 description=_('The Application Name to authenticate to CROWD'),
76 preparer=strip_whitespace,
76 preparer=strip_whitespace,
77 title=_('Application Name'),
77 title=_('Application Name'),
78 widget='string')
78 widget='string')
79 app_password = colander.SchemaNode(
79 app_password = colander.SchemaNode(
80 colander.String(),
80 colander.String(),
81 default='',
81 default='',
82 description=_('The password to authenticate to CROWD'),
82 description=_('The password to authenticate to CROWD'),
83 preparer=strip_whitespace,
83 preparer=strip_whitespace,
84 title=_('Application Password'),
84 title=_('Application Password'),
85 widget='password')
85 widget='password')
86 admin_groups = colander.SchemaNode(
86 admin_groups = colander.SchemaNode(
87 colander.String(),
87 colander.String(),
88 default='',
88 default='',
89 description=_('A comma separated list of group names that identify '
89 description=_('A comma separated list of group names that identify '
90 'users as RhodeCode Administrators'),
90 'users as RhodeCode Administrators'),
91 missing='',
91 missing='',
92 preparer=strip_whitespace,
92 preparer=strip_whitespace,
93 title=_('Admin Groups'),
93 title=_('Admin Groups'),
94 widget='string')
94 widget='string')
95
95
96
96
97 class CrowdServer(object):
97 class CrowdServer(object):
98 def __init__(self, *args, **kwargs):
98 def __init__(self, *args, **kwargs):
99 """
99 """
100 Create a new CrowdServer object that points to IP/Address 'host',
100 Create a new CrowdServer object that points to IP/Address 'host',
101 on the given port, and using the given method (https/http). user and
101 on the given port, and using the given method (https/http). user and
102 passwd can be set here or with set_credentials. If unspecified,
102 passwd can be set here or with set_credentials. If unspecified,
103 "version" defaults to "latest".
103 "version" defaults to "latest".
104
104
105 example::
105 example::
106
106
107 cserver = CrowdServer(host="127.0.0.1",
107 cserver = CrowdServer(host="127.0.0.1",
108 port="8095",
108 port="8095",
109 user="some_app",
109 user="some_app",
110 passwd="some_passwd",
110 passwd="some_passwd",
111 version="1")
111 version="1")
112 """
112 """
113 if not "port" in kwargs:
113 if not "port" in kwargs:
114 kwargs["port"] = "8095"
114 kwargs["port"] = "8095"
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
115 self._logger = kwargs.get("logger", logging.getLogger(__name__))
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
116 self._uri = "%s://%s:%s/crowd" % (kwargs.get("method", "http"),
117 kwargs.get("host", "127.0.0.1"),
117 kwargs.get("host", "127.0.0.1"),
118 kwargs.get("port", "8095"))
118 kwargs.get("port", "8095"))
119 self.set_credentials(kwargs.get("user", ""),
119 self.set_credentials(kwargs.get("user", ""),
120 kwargs.get("passwd", ""))
120 kwargs.get("passwd", ""))
121 self._version = kwargs.get("version", "latest")
121 self._version = kwargs.get("version", "latest")
122 self._url_list = None
122 self._url_list = None
123 self._appname = "crowd"
123 self._appname = "crowd"
124
124
125 def set_credentials(self, user, passwd):
125 def set_credentials(self, user, passwd):
126 self.user = user
126 self.user = user
127 self.passwd = passwd
127 self.passwd = passwd
128 self._make_opener()
128 self._make_opener()
129
129
130 def _make_opener(self):
130 def _make_opener(self):
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
131 mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
132 mgr.add_password(None, self._uri, self.user, self.passwd)
132 mgr.add_password(None, self._uri, self.user, self.passwd)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
133 handler = urllib2.HTTPBasicAuthHandler(mgr)
134 self.opener = urllib2.build_opener(handler)
134 self.opener = urllib2.build_opener(handler)
135
135
136 def _request(self, url, body=None, headers=None,
136 def _request(self, url, body=None, headers=None,
137 method=None, noformat=False,
137 method=None, noformat=False,
138 empty_response_ok=False):
138 empty_response_ok=False):
139 _headers = {"Content-type": "application/json",
139 _headers = {"Content-type": "application/json",
140 "Accept": "application/json"}
140 "Accept": "application/json"}
141 if self.user and self.passwd:
141 if self.user and self.passwd:
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
142 authstring = base64.b64encode("%s:%s" % (self.user, self.passwd))
143 _headers["Authorization"] = "Basic %s" % authstring
143 _headers["Authorization"] = "Basic %s" % authstring
144 if headers:
144 if headers:
145 _headers.update(headers)
145 _headers.update(headers)
146 log.debug("Sent crowd: \n%s"
146 log.debug("Sent crowd: \n%s"
147 % (formatted_json({"url": url, "body": body,
147 % (formatted_json({"url": url, "body": body,
148 "headers": _headers})))
148 "headers": _headers})))
149 request = urllib2.Request(url, body, _headers)
149 request = urllib2.Request(url, body, _headers)
150 if method:
150 if method:
151 request.get_method = lambda: method
151 request.get_method = lambda: method
152
152
153 global msg
153 global msg
154 msg = ""
154 msg = ""
155 try:
155 try:
156 rdoc = self.opener.open(request)
156 ret_doc = self.opener.open(request)
157 msg = "".join(rdoc.readlines())
157 msg = ret_doc.read()
158 if not msg and empty_response_ok:
158 if not msg and empty_response_ok:
159 rval = {}
159 ret_val = {}
160 rval["status"] = True
160 ret_val["status"] = True
161 rval["error"] = "Response body was empty"
161 ret_val["error"] = "Response body was empty"
162 elif not noformat:
162 elif not noformat:
163 rval = json.loads(msg)
163 ret_val = json.loads(msg)
164 rval["status"] = True
164 ret_val["status"] = True
165 else:
165 else:
166 rval = "".join(rdoc.readlines())
166 ret_val = msg
167 except Exception as e:
167 except Exception as e:
168 if not noformat:
168 if not noformat:
169 rval = {"status": False,
169 ret_val = {"status": False,
170 "body": body,
170 "body": body,
171 "error": str(e) + "\n" + msg}
171 "error": "{}\n{}".format(e, msg)}
172 else:
172 else:
173 rval = None
173 ret_val = None
174 return rval
174 return ret_val
175
175
176 def user_auth(self, username, password):
176 def user_auth(self, username, password):
177 """Authenticate a user against crowd. Returns brief information about
177 """Authenticate a user against crowd. Returns brief information about
178 the user."""
178 the user."""
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
179 url = ("%s/rest/usermanagement/%s/authentication?username=%s"
180 % (self._uri, self._version, username))
180 % (self._uri, self._version, username))
181 body = json.dumps({"value": password})
181 body = json.dumps({"value": password})
182 return self._request(url, body)
182 return self._request(url, body)
183
183
184 def user_groups(self, username):
184 def user_groups(self, username):
185 """Retrieve a list of groups to which this user belongs."""
185 """Retrieve a list of groups to which this user belongs."""
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
186 url = ("%s/rest/usermanagement/%s/user/group/nested?username=%s"
187 % (self._uri, self._version, username))
187 % (self._uri, self._version, username))
188 return self._request(url)
188 return self._request(url)
189
189
190
190
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
191 class RhodeCodeAuthPlugin(RhodeCodeExternalAuthPlugin):
192 uid = 'crowd'
192 uid = 'crowd'
193 _settings_unsafe_keys = ['app_password']
193 _settings_unsafe_keys = ['app_password']
194
194
195 def includeme(self, config):
195 def includeme(self, config):
196 config.add_authn_plugin(self)
196 config.add_authn_plugin(self)
197 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
197 config.add_authn_resource(self.get_id(), CrowdAuthnResource(self))
198 config.add_view(
198 config.add_view(
199 'rhodecode.authentication.views.AuthnPluginViewBase',
199 'rhodecode.authentication.views.AuthnPluginViewBase',
200 attr='settings_get',
200 attr='settings_get',
201 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
201 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
202 request_method='GET',
202 request_method='GET',
203 route_name='auth_home',
203 route_name='auth_home',
204 context=CrowdAuthnResource)
204 context=CrowdAuthnResource)
205 config.add_view(
205 config.add_view(
206 'rhodecode.authentication.views.AuthnPluginViewBase',
206 'rhodecode.authentication.views.AuthnPluginViewBase',
207 attr='settings_post',
207 attr='settings_post',
208 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
208 renderer='rhodecode:templates/admin/auth/plugin_settings.mako',
209 request_method='POST',
209 request_method='POST',
210 route_name='auth_home',
210 route_name='auth_home',
211 context=CrowdAuthnResource)
211 context=CrowdAuthnResource)
212
212
213 def get_settings_schema(self):
213 def get_settings_schema(self):
214 return CrowdSettingsSchema()
214 return CrowdSettingsSchema()
215
215
216 def get_display_name(self):
216 def get_display_name(self):
217 return _('CROWD')
217 return _('CROWD')
218
218
219 @classmethod
219 @classmethod
220 def docs(cls):
220 def docs(cls):
221 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
221 return "https://docs.rhodecode.com/RhodeCode-Enterprise/auth/auth-crowd.html"
222
222
223 @hybrid_property
223 @hybrid_property
224 def name(self):
224 def name(self):
225 return u"crowd"
225 return u"crowd"
226
226
227 def use_fake_password(self):
227 def use_fake_password(self):
228 return True
228 return True
229
229
230 def user_activation_state(self):
230 def user_activation_state(self):
231 def_user_perms = User.get_default_user().AuthUser().permissions['global']
231 def_user_perms = User.get_default_user().AuthUser().permissions['global']
232 return 'hg.extern_activate.auto' in def_user_perms
232 return 'hg.extern_activate.auto' in def_user_perms
233
233
234 def auth(self, userobj, username, password, settings, **kwargs):
234 def auth(self, userobj, username, password, settings, **kwargs):
235 """
235 """
236 Given a user object (which may be null), username, a plaintext password,
236 Given a user object (which may be null), username, a plaintext password,
237 and a settings object (containing all the keys needed as listed in settings()),
237 and a settings object (containing all the keys needed as listed in settings()),
238 authenticate this user's login attempt.
238 authenticate this user's login attempt.
239
239
240 Return None on failure. On success, return a dictionary of the form:
240 Return None on failure. On success, return a dictionary of the form:
241
241
242 see: RhodeCodeAuthPluginBase.auth_func_attrs
242 see: RhodeCodeAuthPluginBase.auth_func_attrs
243 This is later validated for correctness
243 This is later validated for correctness
244 """
244 """
245 if not username or not password:
245 if not username or not password:
246 log.debug('Empty username or password skipping...')
246 log.debug('Empty username or password skipping...')
247 return None
247 return None
248
248
249 log.debug("Crowd settings: \n%s", formatted_json(settings))
249 log.debug("Crowd settings: \n%s", formatted_json(settings))
250 server = CrowdServer(**settings)
250 server = CrowdServer(**settings)
251 server.set_credentials(settings["app_name"], settings["app_password"])
251 server.set_credentials(settings["app_name"], settings["app_password"])
252 crowd_user = server.user_auth(username, password)
252 crowd_user = server.user_auth(username, password)
253 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
253 log.debug("Crowd returned: \n%s", formatted_json(crowd_user))
254 if not crowd_user["status"]:
254 if not crowd_user["status"]:
255 return None
255 return None
256
256
257 res = server.user_groups(crowd_user["name"])
257 res = server.user_groups(crowd_user["name"])
258 log.debug("Crowd groups: \n%s", formatted_json(res))
258 log.debug("Crowd groups: \n%s", formatted_json(res))
259 crowd_user["groups"] = [x["name"] for x in res["groups"]]
259 crowd_user["groups"] = [x["name"] for x in res["groups"]]
260
260
261 # old attrs fetched from RhodeCode database
261 # old attrs fetched from RhodeCode database
262 admin = getattr(userobj, 'admin', False)
262 admin = getattr(userobj, 'admin', False)
263 active = getattr(userobj, 'active', True)
263 active = getattr(userobj, 'active', True)
264 email = getattr(userobj, 'email', '')
264 email = getattr(userobj, 'email', '')
265 username = getattr(userobj, 'username', username)
265 username = getattr(userobj, 'username', username)
266 firstname = getattr(userobj, 'firstname', '')
266 firstname = getattr(userobj, 'firstname', '')
267 lastname = getattr(userobj, 'lastname', '')
267 lastname = getattr(userobj, 'lastname', '')
268 extern_type = getattr(userobj, 'extern_type', '')
268 extern_type = getattr(userobj, 'extern_type', '')
269
269
270 user_attrs = {
270 user_attrs = {
271 'username': username,
271 'username': username,
272 'firstname': crowd_user["first-name"] or firstname,
272 'firstname': crowd_user["first-name"] or firstname,
273 'lastname': crowd_user["last-name"] or lastname,
273 'lastname': crowd_user["last-name"] or lastname,
274 'groups': crowd_user["groups"],
274 'groups': crowd_user["groups"],
275 'user_group_sync': True,
275 'user_group_sync': True,
276 'email': crowd_user["email"] or email,
276 'email': crowd_user["email"] or email,
277 'admin': admin,
277 'admin': admin,
278 'active': active,
278 'active': active,
279 'active_from_extern': crowd_user.get('active'),
279 'active_from_extern': crowd_user.get('active'),
280 'extern_name': crowd_user["name"],
280 'extern_name': crowd_user["name"],
281 'extern_type': extern_type,
281 'extern_type': extern_type,
282 }
282 }
283
283
284 # set an admin if we're in admin_groups of crowd
284 # set an admin if we're in admin_groups of crowd
285 for group in settings["admin_groups"]:
285 for group in settings["admin_groups"]:
286 if group in user_attrs["groups"]:
286 if group in user_attrs["groups"]:
287 user_attrs["admin"] = True
287 user_attrs["admin"] = True
288 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
288 log.debug("Final crowd user object: \n%s", formatted_json(user_attrs))
289 log.info('user `%s` authenticated correctly', user_attrs['username'])
289 log.info('user `%s` authenticated correctly', user_attrs['username'])
290 return user_attrs
290 return user_attrs
291
291
292
292
293 def includeme(config):
293 def includeme(config):
294 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
294 plugin_id = 'egg:rhodecode-enterprise-ce#{}'.format(RhodeCodeAuthPlugin.uid)
295 plugin_factory(plugin_id).includeme(config)
295 plugin_factory(plugin_id).includeme(config)
General Comments 0
You need to be logged in to leave comments. Login now