##// END OF EJS Templates
authomatic: should not be a global object anymore
ergo -
Show More
@@ -1,288 +1,245 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 # App Enlight Enterprise Edition, including its added features, Support
18 # App Enlight Enterprise Edition, including its added features, Support
19 # services, and proprietary license terms, please see
19 # services, and proprietary license terms, please see
20 # https://rhodecode.com/licenses/
20 # https://rhodecode.com/licenses/
21
21
22 import datetime
22 import datetime
23 import logging
23 import logging
24 import pyelasticsearch
24 import pyelasticsearch
25 import redis
25 import redis
26 import os
26 import os
27 from pkg_resources import iter_entry_points
27 from pkg_resources import iter_entry_points
28
28
29 import appenlight.lib.jinja2_filters as jinja2_filters
29 import appenlight.lib.jinja2_filters as jinja2_filters
30 import appenlight.lib.encryption as encryption
30 import appenlight.lib.encryption as encryption
31
31
32 from authomatic.providers import oauth2, oauth1
33 from authomatic import Authomatic
34 from pyramid.config import PHASE3_CONFIG
32 from pyramid.config import PHASE3_CONFIG
35 from pyramid.authentication import AuthTktAuthenticationPolicy
33 from pyramid.authentication import AuthTktAuthenticationPolicy
36 from pyramid.authorization import ACLAuthorizationPolicy
34 from pyramid.authorization import ACLAuthorizationPolicy
37 from pyramid_mailer.mailer import Mailer
35 from pyramid_mailer.mailer import Mailer
38 from pyramid.renderers import JSON
36 from pyramid.renderers import JSON
39 from pyramid_redis_sessions import session_factory_from_settings
37 from pyramid_redis_sessions import session_factory_from_settings
40 from pyramid.settings import asbool, aslist
38 from pyramid.settings import asbool, aslist
41 from pyramid.security import AllPermissionsList
39 from pyramid.security import AllPermissionsList
42 from pyramid_authstack import AuthenticationStackPolicy
40 from pyramid_authstack import AuthenticationStackPolicy
43 from redlock import Redlock
41 from redlock import Redlock
44 from sqlalchemy import engine_from_config
42 from sqlalchemy import engine_from_config
45
43
46 from appenlight.celery import configure_celery
44 from appenlight.celery import configure_celery
47 from appenlight.lib.configurator import CythonCompatConfigurator
45 from appenlight.lib.configurator import CythonCompatConfigurator
48 from appenlight.lib import cache_regions
46 from appenlight.lib import cache_regions
49 from appenlight.lib.ext_json import json
47 from appenlight.lib.ext_json import json
50 from appenlight.security import groupfinder, AuthTokenAuthenticationPolicy
48 from appenlight.security import groupfinder, AuthTokenAuthenticationPolicy
51
49
52 json_renderer = JSON(serializer=json.dumps, indent=4)
50 json_renderer = JSON(serializer=json.dumps, indent=4)
53
51
54 log = logging.getLogger(__name__)
52 log = logging.getLogger(__name__)
55
53
56
54
57 def datetime_adapter(obj, request):
55 def datetime_adapter(obj, request):
58 return obj.isoformat()
56 return obj.isoformat()
59
57
60
58
61 def all_permissions_adapter(obj, request):
59 def all_permissions_adapter(obj, request):
62 return '__all_permissions__'
60 return '__all_permissions__'
63
61
64
62
65 json_renderer.add_adapter(datetime.datetime, datetime_adapter)
63 json_renderer.add_adapter(datetime.datetime, datetime_adapter)
66 json_renderer.add_adapter(AllPermissionsList, all_permissions_adapter)
64 json_renderer.add_adapter(AllPermissionsList, all_permissions_adapter)
67
65
68
66
69 def main(global_config, **settings):
67 def main(global_config, **settings):
70 """ This function returns a Pyramid WSGI application.
68 """ This function returns a Pyramid WSGI application.
71 """
69 """
72 auth_tkt_policy = AuthTktAuthenticationPolicy(
70 auth_tkt_policy = AuthTktAuthenticationPolicy(
73 settings['authtkt.secret'],
71 settings['authtkt.secret'],
74 hashalg='sha512',
72 hashalg='sha512',
75 callback=groupfinder,
73 callback=groupfinder,
76 max_age=2592000,
74 max_age=2592000,
77 secure=asbool(settings.get('authtkt.secure', 'false')))
75 secure=asbool(settings.get('authtkt.secure', 'false')))
78 auth_token_policy = AuthTokenAuthenticationPolicy(
76 auth_token_policy = AuthTokenAuthenticationPolicy(
79 callback=groupfinder
77 callback=groupfinder
80 )
78 )
81 authorization_policy = ACLAuthorizationPolicy()
79 authorization_policy = ACLAuthorizationPolicy()
82 authentication_policy = AuthenticationStackPolicy()
80 authentication_policy = AuthenticationStackPolicy()
83 authentication_policy.add_policy('auth_tkt', auth_tkt_policy)
81 authentication_policy.add_policy('auth_tkt', auth_tkt_policy)
84 authentication_policy.add_policy('auth_token', auth_token_policy)
82 authentication_policy.add_policy('auth_token', auth_token_policy)
85 # set crypto key
83 # set crypto key
86 encryption.ENCRYPTION_SECRET = settings.get('encryption_secret')
84 encryption.ENCRYPTION_SECRET = settings.get('encryption_secret')
87 # import this later so encyption key can be monkeypatched
85 # import this later so encyption key can be monkeypatched
88 from appenlight.models import DBSession, register_datastores
86 from appenlight.models import DBSession, register_datastores
89 # update config with cometd info
87 # update config with cometd info
90 settings['cometd_servers'] = {'server': settings['cometd.server'],
88 settings['cometd_servers'] = {'server': settings['cometd.server'],
91 'secret': settings['cometd.secret']}
89 'secret': settings['cometd.secret']}
92
90
93 # Create the Pyramid Configurator.
91 # Create the Pyramid Configurator.
94 settings['_mail_url'] = settings['mailing.app_url']
92 settings['_mail_url'] = settings['mailing.app_url']
95 config = CythonCompatConfigurator(
93 config = CythonCompatConfigurator(
96 settings=settings,
94 settings=settings,
97 authentication_policy=authentication_policy,
95 authentication_policy=authentication_policy,
98 authorization_policy=authorization_policy,
96 authorization_policy=authorization_policy,
99 root_factory='appenlight.security.RootFactory',
97 root_factory='appenlight.security.RootFactory',
100 default_permission='view')
98 default_permission='view')
101 config.set_default_csrf_options(require_csrf=True, header='X-XSRF-TOKEN')
99 config.set_default_csrf_options(require_csrf=True, header='X-XSRF-TOKEN')
102 config.add_view_deriver('appenlight.predicates.csrf_view',
100 config.add_view_deriver('appenlight.predicates.csrf_view',
103 name='csrf_view')
101 name='csrf_view')
104
102
105 # later, when config is available
103 # later, when config is available
106 dogpile_config = {'url': settings['redis.url'],
104 dogpile_config = {'url': settings['redis.url'],
107 "redis_expiration_time": 86400,
105 "redis_expiration_time": 86400,
108 "redis_distributed_lock": True}
106 "redis_distributed_lock": True}
109 cache_regions.regions = cache_regions.CacheRegions(dogpile_config)
107 cache_regions.regions = cache_regions.CacheRegions(dogpile_config)
110 config.registry.cache_regions = cache_regions.regions
108 config.registry.cache_regions = cache_regions.regions
111 engine = engine_from_config(settings, 'sqlalchemy.',
109 engine = engine_from_config(settings, 'sqlalchemy.',
112 json_serializer=json.dumps)
110 json_serializer=json.dumps)
113 DBSession.configure(bind=engine)
111 DBSession.configure(bind=engine)
114
112
115 # json rederer that serializes datetime
113 # json rederer that serializes datetime
116 config.add_renderer('json', json_renderer)
114 config.add_renderer('json', json_renderer)
117 config.set_request_property('appenlight.lib.request.es_conn', 'es_conn')
115 config.set_request_property('appenlight.lib.request.es_conn', 'es_conn')
118 config.set_request_property('appenlight.lib.request.get_user', 'user',
116 config.set_request_property('appenlight.lib.request.get_user', 'user',
119 reify=True)
117 reify=True)
120 config.set_request_property('appenlight.lib.request.get_csrf_token',
118 config.set_request_property('appenlight.lib.request.get_csrf_token',
121 'csrf_token', reify=True)
119 'csrf_token', reify=True)
122 config.set_request_property('appenlight.lib.request.safe_json_body',
120 config.set_request_property('appenlight.lib.request.safe_json_body',
123 'safe_json_body', reify=True)
121 'safe_json_body', reify=True)
124 config.set_request_property('appenlight.lib.request.unsafe_json_body',
122 config.set_request_property('appenlight.lib.request.unsafe_json_body',
125 'unsafe_json_body', reify=True)
123 'unsafe_json_body', reify=True)
126 config.add_request_method('appenlight.lib.request.add_flash_to_headers',
124 config.add_request_method('appenlight.lib.request.add_flash_to_headers',
127 'add_flash_to_headers')
125 'add_flash_to_headers')
126 config.add_request_method('appenlight.lib.request.get_authomatic',
127 'authomatic', reify=True)
128
128
129 config.include('pyramid_redis_sessions')
129 config.include('pyramid_redis_sessions')
130 config.include('pyramid_tm')
130 config.include('pyramid_tm')
131 config.include('pyramid_jinja2')
131 config.include('pyramid_jinja2')
132 config.include('appenlight_client.ext.pyramid_tween')
132 config.include('appenlight_client.ext.pyramid_tween')
133 config.include('ziggurat_foundations.ext.pyramid.sign_in')
133 config.include('ziggurat_foundations.ext.pyramid.sign_in')
134 es_server_list = aslist(settings['elasticsearch.nodes'])
134 es_server_list = aslist(settings['elasticsearch.nodes'])
135 redis_url = settings['redis.url']
135 redis_url = settings['redis.url']
136 log.warning('Elasticsearch server list: {}'.format(es_server_list))
136 log.warning('Elasticsearch server list: {}'.format(es_server_list))
137 log.warning('Redis server: {}'.format(redis_url))
137 log.warning('Redis server: {}'.format(redis_url))
138 config.registry.es_conn = pyelasticsearch.ElasticSearch(es_server_list)
138 config.registry.es_conn = pyelasticsearch.ElasticSearch(es_server_list)
139 config.registry.redis_conn = redis.StrictRedis.from_url(redis_url)
139 config.registry.redis_conn = redis.StrictRedis.from_url(redis_url)
140
140
141 config.registry.redis_lockmgr = Redlock([settings['redis.redlock.url'], ],
141 config.registry.redis_lockmgr = Redlock([settings['redis.redlock.url'], ],
142 retry_count=0, retry_delay=0)
142 retry_count=0, retry_delay=0)
143 # mailer
143 # mailer
144 config.registry.mailer = Mailer.from_settings(settings)
144 config.registry.mailer = Mailer.from_settings(settings)
145
145
146 # Configure sessions
146 # Configure sessions
147 session_factory = session_factory_from_settings(settings)
147 session_factory = session_factory_from_settings(settings)
148 config.set_session_factory(session_factory)
148 config.set_session_factory(session_factory)
149
149
150 # Configure renderers and event subscribers
150 # Configure renderers and event subscribers
151 config.add_jinja2_extension('jinja2.ext.loopcontrols')
151 config.add_jinja2_extension('jinja2.ext.loopcontrols')
152 config.add_jinja2_search_path('appenlight:templates')
152 config.add_jinja2_search_path('appenlight:templates')
153 # event subscribers
153 # event subscribers
154 config.add_subscriber("appenlight.subscribers.application_created",
154 config.add_subscriber("appenlight.subscribers.application_created",
155 "pyramid.events.ApplicationCreated")
155 "pyramid.events.ApplicationCreated")
156 config.add_subscriber("appenlight.subscribers.add_renderer_globals",
156 config.add_subscriber("appenlight.subscribers.add_renderer_globals",
157 "pyramid.events.BeforeRender")
157 "pyramid.events.BeforeRender")
158 config.add_subscriber('appenlight.subscribers.new_request',
158 config.add_subscriber('appenlight.subscribers.new_request',
159 'pyramid.events.NewRequest')
159 'pyramid.events.NewRequest')
160 config.add_view_predicate('context_type_class',
160 config.add_view_predicate('context_type_class',
161 'appenlight.predicates.contextTypeClass')
161 'appenlight.predicates.contextTypeClass')
162
162
163 register_datastores(es_conn=config.registry.es_conn,
163 register_datastores(es_conn=config.registry.es_conn,
164 redis_conn=config.registry.redis_conn,
164 redis_conn=config.registry.redis_conn,
165 redis_lockmgr=config.registry.redis_lockmgr)
165 redis_lockmgr=config.registry.redis_lockmgr)
166
166
167 # base stuff and scan
167 # base stuff and scan
168
168
169 # need to ensure webassets exists otherwise config.override_asset()
169 # need to ensure webassets exists otherwise config.override_asset()
170 # throws exception
170 # throws exception
171 if not os.path.exists(settings['webassets.dir']):
171 if not os.path.exists(settings['webassets.dir']):
172 os.mkdir(settings['webassets.dir'])
172 os.mkdir(settings['webassets.dir'])
173 config.add_static_view(path='appenlight:webassets',
173 config.add_static_view(path='appenlight:webassets',
174 name='static', cache_max_age=3600)
174 name='static', cache_max_age=3600)
175 config.override_asset(to_override='appenlight:webassets/',
175 config.override_asset(to_override='appenlight:webassets/',
176 override_with=settings['webassets.dir'])
176 override_with=settings['webassets.dir'])
177
177
178 config.include('appenlight.views')
178 config.include('appenlight.views')
179 config.include('appenlight.views.admin')
179 config.include('appenlight.views.admin')
180 config.scan(ignore=['appenlight.migrations',
180 config.scan(ignore=['appenlight.migrations',
181 'appenlight.scripts',
181 'appenlight.scripts',
182 'appenlight.tests'])
182 'appenlight.tests'])
183
183
184 # authomatic social auth
185 authomatic_conf = {
186 # callback http://yourapp.com/social_auth/twitter
187 'twitter': {
188 'class_': oauth1.Twitter,
189 'consumer_key': settings.get('authomatic.pr.twitter.key', 'X'),
190 'consumer_secret': settings.get('authomatic.pr.twitter.secret',
191 'X'),
192 },
193 # callback http://yourapp.com/social_auth/facebook
194 'facebook': {
195 'class_': oauth2.Facebook,
196 'consumer_key': settings.get('authomatic.pr.facebook.app_id', 'X'),
197 'consumer_secret': settings.get('authomatic.pr.facebook.secret',
198 'X'),
199 'scope': ['email'],
200 },
201 # callback http://yourapp.com/social_auth/google
202 'google': {
203 'class_': oauth2.Google,
204 'consumer_key': settings.get('authomatic.pr.google.key', 'X'),
205 'consumer_secret': settings.get(
206 'authomatic.pr.google.secret', 'X'),
207 'scope': ['profile', 'email'],
208 },
209 'github': {
210 'class_': oauth2.GitHub,
211 'consumer_key': settings.get('authomatic.pr.github.key', 'X'),
212 'consumer_secret': settings.get(
213 'authomatic.pr.github.secret', 'X'),
214 'scope': ['repo', 'public_repo', 'user:email'],
215 'access_headers': {'User-Agent': 'AppEnlight'},
216 },
217 'bitbucket': {
218 'class_': oauth1.Bitbucket,
219 'consumer_key': settings.get('authomatic.pr.bitbucket.key', 'X'),
220 'consumer_secret': settings.get(
221 'authomatic.pr.bitbucket.secret', 'X')
222 }
223 }
224 config.registry.authomatic = Authomatic(
225 config=authomatic_conf, secret=settings['authomatic.secret'])
226
227 # resource type information
184 # resource type information
228 config.registry.resource_types = ['resource', 'application']
185 config.registry.resource_types = ['resource', 'application']
229
186
230 # plugin information
187 # plugin information
231 config.registry.appenlight_plugins = {}
188 config.registry.appenlight_plugins = {}
232
189
233 def register_appenlight_plugin(config, plugin_name, plugin_config):
190 def register_appenlight_plugin(config, plugin_name, plugin_config):
234 def register():
191 def register():
235 log.warning('Registering plugin: {}'.format(plugin_name))
192 log.warning('Registering plugin: {}'.format(plugin_name))
236 if plugin_name not in config.registry.appenlight_plugins:
193 if plugin_name not in config.registry.appenlight_plugins:
237 config.registry.appenlight_plugins[plugin_name] = {
194 config.registry.appenlight_plugins[plugin_name] = {
238 'javascript': None,
195 'javascript': None,
239 'static': None,
196 'static': None,
240 'css': None,
197 'css': None,
241 'top_nav': None,
198 'top_nav': None,
242 'celery_tasks': None,
199 'celery_tasks': None,
243 'celery_beats': None,
200 'celery_beats': None,
244 'fulltext_indexer': None,
201 'fulltext_indexer': None,
245 'sqlalchemy_migrations': None,
202 'sqlalchemy_migrations': None,
246 'default_values_setter': None,
203 'default_values_setter': None,
247 'resource_types': [],
204 'resource_types': [],
248 'url_gen': None
205 'url_gen': None
249 }
206 }
250 config.registry.appenlight_plugins[plugin_name].update(
207 config.registry.appenlight_plugins[plugin_name].update(
251 plugin_config)
208 plugin_config)
252 # inform AE what kind of resource types we have available
209 # inform AE what kind of resource types we have available
253 # so we can avoid failing when a plugin is removed but data
210 # so we can avoid failing when a plugin is removed but data
254 # is still present in the db
211 # is still present in the db
255 if plugin_config.get('resource_types'):
212 if plugin_config.get('resource_types'):
256 config.registry.resource_types.extend(
213 config.registry.resource_types.extend(
257 plugin_config['resource_types'])
214 plugin_config['resource_types'])
258
215
259 config.action('appenlight_plugin={}'.format(plugin_name), register)
216 config.action('appenlight_plugin={}'.format(plugin_name), register)
260
217
261 config.add_directive('register_appenlight_plugin',
218 config.add_directive('register_appenlight_plugin',
262 register_appenlight_plugin)
219 register_appenlight_plugin)
263
220
264 for entry_point in iter_entry_points(group='appenlight.plugins'):
221 for entry_point in iter_entry_points(group='appenlight.plugins'):
265 plugin = entry_point.load()
222 plugin = entry_point.load()
266 plugin.includeme(config)
223 plugin.includeme(config)
267
224
268 # include other appenlight plugins explictly if needed
225 # include other appenlight plugins explictly if needed
269 includes = aslist(settings.get('appenlight.includes', []))
226 includes = aslist(settings.get('appenlight.includes', []))
270 for inc in includes:
227 for inc in includes:
271 config.include(inc)
228 config.include(inc)
272
229
273 # run this after everything registers in configurator
230 # run this after everything registers in configurator
274
231
275 def pre_commit():
232 def pre_commit():
276 jinja_env = config.get_jinja2_environment()
233 jinja_env = config.get_jinja2_environment()
277 jinja_env.filters['tojson'] = json.dumps
234 jinja_env.filters['tojson'] = json.dumps
278 jinja_env.filters['toJSONUnsafe'] = jinja2_filters.toJSONUnsafe
235 jinja_env.filters['toJSONUnsafe'] = jinja2_filters.toJSONUnsafe
279
236
280 config.action(None, pre_commit, order=PHASE3_CONFIG + 999)
237 config.action(None, pre_commit, order=PHASE3_CONFIG + 999)
281
238
282 def wrap_config_celery():
239 def wrap_config_celery():
283 configure_celery(config.registry)
240 configure_celery(config.registry)
284
241
285 config.action(None, wrap_config_celery, order=PHASE3_CONFIG + 999)
242 config.action(None, wrap_config_celery, order=PHASE3_CONFIG + 999)
286
243
287 app = config.make_wsgi_app()
244 app = config.make_wsgi_app()
288 return app
245 return app
@@ -1,89 +1,140 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 # App Enlight Enterprise Edition, including its added features, Support
18 # App Enlight Enterprise Edition, including its added features, Support
19 # services, and proprietary license terms, please see
19 # services, and proprietary license terms, please see
20 # https://rhodecode.com/licenses/
20 # https://rhodecode.com/licenses/
21
21
22 import appenlight.lib.helpers as helpers
23 import json
22 import json
23
24 from pyramid.security import unauthenticated_userid
24 from pyramid.security import unauthenticated_userid
25
26 import appenlight.lib.helpers as helpers
27
28 from authomatic.providers import oauth2, oauth1
29 from authomatic import Authomatic
25 from appenlight.models.user import User
30 from appenlight.models.user import User
26
31
27
32
28 class CSRFException(Exception):
33 class CSRFException(Exception):
29 pass
34 pass
30
35
31
36
32 class JSONException(Exception):
37 class JSONException(Exception):
33 pass
38 pass
34
39
35
40
36 def get_csrf_token(request):
41 def get_csrf_token(request):
37 return request.session.get_csrf_token()
42 return request.session.get_csrf_token()
38
43
39
44
40 def safe_json_body(request):
45 def safe_json_body(request):
41 """
46 """
42 Returns None if json body is missing or erroneous
47 Returns None if json body is missing or erroneous
43 """
48 """
44 try:
49 try:
45 return request.json_body
50 return request.json_body
46 except ValueError:
51 except ValueError:
47 return None
52 return None
48
53
49
54
50 def unsafe_json_body(request):
55 def unsafe_json_body(request):
51 """
56 """
52 Throws JSONException if json can't deserialize
57 Throws JSONException if json can't deserialize
53 """
58 """
54 try:
59 try:
55 return request.json_body
60 return request.json_body
56 except ValueError:
61 except ValueError:
57 raise JSONException('Incorrect JSON')
62 raise JSONException('Incorrect JSON')
58
63
59
64
60 def get_user(request):
65 def get_user(request):
61 if not request.path_info.startswith('/static'):
66 if not request.path_info.startswith('/static'):
62 user_id = unauthenticated_userid(request)
67 user_id = unauthenticated_userid(request)
63 try:
68 try:
64 user_id = int(user_id)
69 user_id = int(user_id)
65 except Exception:
70 except Exception:
66 return None
71 return None
67
72
68 if user_id:
73 if user_id:
69 user = User.by_id(user_id)
74 user = User.by_id(user_id)
70 if user:
75 if user:
71 request.environ['appenlight.username'] = '%d:%s' % (
76 request.environ['appenlight.username'] = '%d:%s' % (
72 user_id, user.user_name)
77 user_id, user.user_name)
73 return user
78 return user
74 else:
79 else:
75 return None
80 return None
76
81
77
82
78 def es_conn(request):
83 def es_conn(request):
79 return request.registry.es_conn
84 return request.registry.es_conn
80
85
81
86
82 def add_flash_to_headers(request, clear=True):
87 def add_flash_to_headers(request, clear=True):
83 """
88 """
84 Adds pending flash messages to response, if clear is true clears out the
89 Adds pending flash messages to response, if clear is true clears out the
85 flash queue
90 flash queue
86 """
91 """
87 flash_msgs = helpers.get_type_formatted_flash(request)
92 flash_msgs = helpers.get_type_formatted_flash(request)
88 request.response.headers['x-flash-messages'] = json.dumps(flash_msgs)
93 request.response.headers['x-flash-messages'] = json.dumps(flash_msgs)
89 helpers.clear_flash(request)
94 helpers.clear_flash(request)
95
96
97 def get_authomatic(request):
98 settings = request.registry.settings
99 # authomatic social auth
100 authomatic_conf = {
101 # callback http://yourapp.com/social_auth/twitter
102 'twitter': {
103 'class_': oauth1.Twitter,
104 'consumer_key': settings.get('authomatic.pr.twitter.key', ''),
105 'consumer_secret': settings.get('authomatic.pr.twitter.secret',
106 ''),
107 },
108 # callback http://yourapp.com/social_auth/facebook
109 'facebook': {
110 'class_': oauth2.Facebook,
111 'consumer_key': settings.get('authomatic.pr.facebook.app_id', 'X'),
112 'consumer_secret': settings.get('authomatic.pr.facebook.secret',
113 ''),
114 'scope': ['email'],
115 },
116 # callback http://yourapp.com/social_auth/google
117 'google': {
118 'class_': oauth2.Google,
119 'consumer_key': settings.get('authomatic.pr.google.key', ''),
120 'consumer_secret': settings.get(
121 'authomatic.pr.google.secret', ''),
122 'scope': ['profile', 'email'],
123 },
124 'github': {
125 'class_': oauth2.GitHub,
126 'consumer_key': settings.get('authomatic.pr.github.key', ''),
127 'consumer_secret': settings.get(
128 'authomatic.pr.github.secret', ''),
129 'scope': ['repo', 'public_repo', 'user:email'],
130 'access_headers': {'User-Agent': 'AppEnlight'},
131 },
132 'bitbucket': {
133 'class_': oauth1.Bitbucket,
134 'consumer_key': settings.get('authomatic.pr.bitbucket.key', ''),
135 'consumer_secret': settings.get(
136 'authomatic.pr.bitbucket.secret', '')
137 }
138 }
139 return Authomatic(
140 config=authomatic_conf, secret=settings['authomatic.secret'])
@@ -1,678 +1,678 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 # App Enlight Enterprise Edition, including its added features, Support
18 # App Enlight Enterprise Edition, including its added features, Support
19 # services, and proprietary license terms, please see
19 # services, and proprietary license terms, please see
20 # https://rhodecode.com/licenses/
20 # https://rhodecode.com/licenses/
21
21
22 import colander
22 import colander
23 import datetime
23 import datetime
24 import json
24 import json
25 import logging
25 import logging
26 import uuid
26 import uuid
27 import pyramid.security as security
27 import pyramid.security as security
28 import appenlight.lib.helpers as h
28 import appenlight.lib.helpers as h
29
29
30 from authomatic.adapters import WebObAdapter
30 from authomatic.adapters import WebObAdapter
31 from pyramid.view import view_config
31 from pyramid.view import view_config
32 from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity
32 from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity
33 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest
33 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest
34 from pyramid.security import NO_PERMISSION_REQUIRED
34 from pyramid.security import NO_PERMISSION_REQUIRED
35 from ziggurat_foundations.models.services.external_identity import \
35 from ziggurat_foundations.models.services.external_identity import \
36 ExternalIdentityService
36 ExternalIdentityService
37
37
38 from appenlight.lib import generate_random_string
38 from appenlight.lib import generate_random_string
39 from appenlight.lib.social import handle_social_data
39 from appenlight.lib.social import handle_social_data
40 from appenlight.lib.utils import cometd_request, add_cors_headers, \
40 from appenlight.lib.utils import cometd_request, add_cors_headers, \
41 permission_tuple_to_dict
41 permission_tuple_to_dict
42 from appenlight.models import DBSession
42 from appenlight.models import DBSession
43 from appenlight.models.alert_channels.email import EmailAlertChannel
43 from appenlight.models.alert_channels.email import EmailAlertChannel
44 from appenlight.models.alert_channel_action import AlertChannelAction
44 from appenlight.models.alert_channel_action import AlertChannelAction
45 from appenlight.models.services.alert_channel import AlertChannelService
45 from appenlight.models.services.alert_channel import AlertChannelService
46 from appenlight.models.services.alert_channel_action import \
46 from appenlight.models.services.alert_channel_action import \
47 AlertChannelActionService
47 AlertChannelActionService
48 from appenlight.models.auth_token import AuthToken
48 from appenlight.models.auth_token import AuthToken
49 from appenlight.models.report import REPORT_TYPE_MATRIX
49 from appenlight.models.report import REPORT_TYPE_MATRIX
50 from appenlight.models.user import User
50 from appenlight.models.user import User
51 from appenlight.models.services.user import UserService
51 from appenlight.models.services.user import UserService
52 from appenlight.subscribers import _
52 from appenlight.subscribers import _
53 from appenlight.validators import build_rule_schema
53 from appenlight.validators import build_rule_schema
54 from appenlight import forms
54 from appenlight import forms
55 from webob.multidict import MultiDict
55 from webob.multidict import MultiDict
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 @view_config(route_name='users_no_id', renderer='json',
60 @view_config(route_name='users_no_id', renderer='json',
61 request_method="GET", permission='root_administration')
61 request_method="GET", permission='root_administration')
62 def users_list(request):
62 def users_list(request):
63 """
63 """
64 Returns users list
64 Returns users list
65 """
65 """
66 props = ['user_name', 'id', 'first_name', 'last_name', 'email',
66 props = ['user_name', 'id', 'first_name', 'last_name', 'email',
67 'last_login_date', 'status']
67 'last_login_date', 'status']
68 users = UserService.all()
68 users = UserService.all()
69 users_dicts = []
69 users_dicts = []
70 for user in users:
70 for user in users:
71 u_dict = user.get_dict(include_keys=props)
71 u_dict = user.get_dict(include_keys=props)
72 u_dict['gravatar_url'] = user.gravatar_url(s=20)
72 u_dict['gravatar_url'] = user.gravatar_url(s=20)
73 users_dicts.append(u_dict)
73 users_dicts.append(u_dict)
74 return users_dicts
74 return users_dicts
75
75
76
76
77 @view_config(route_name='users_no_id', renderer='json',
77 @view_config(route_name='users_no_id', renderer='json',
78 request_method="POST", permission='root_administration')
78 request_method="POST", permission='root_administration')
79 def users_create(request):
79 def users_create(request):
80 """
80 """
81 Returns users list
81 Returns users list
82 """
82 """
83 form = forms.UserCreateForm(MultiDict(request.safe_json_body or {}),
83 form = forms.UserCreateForm(MultiDict(request.safe_json_body or {}),
84 csrf_context=request)
84 csrf_context=request)
85 if form.validate():
85 if form.validate():
86 log.info('registering user')
86 log.info('registering user')
87 user = User()
87 user = User()
88 # insert new user here
88 # insert new user here
89 DBSession.add(user)
89 DBSession.add(user)
90 form.populate_obj(user)
90 form.populate_obj(user)
91 user.regenerate_security_code()
91 user.regenerate_security_code()
92 user.set_password(user.user_password)
92 user.set_password(user.user_password)
93 user.status = 1 if form.status.data else 0
93 user.status = 1 if form.status.data else 0
94 request.session.flash(_('User created'))
94 request.session.flash(_('User created'))
95 DBSession.flush()
95 DBSession.flush()
96 return user.get_dict(exclude_keys=['security_code_date', 'notes',
96 return user.get_dict(exclude_keys=['security_code_date', 'notes',
97 'security_code', 'user_password'])
97 'security_code', 'user_password'])
98 else:
98 else:
99 return HTTPUnprocessableEntity(body=form.errors_json)
99 return HTTPUnprocessableEntity(body=form.errors_json)
100
100
101
101
102 @view_config(route_name='users', renderer='json',
102 @view_config(route_name='users', renderer='json',
103 request_method="GET", permission='root_administration')
103 request_method="GET", permission='root_administration')
104 @view_config(route_name='users', renderer='json',
104 @view_config(route_name='users', renderer='json',
105 request_method="PATCH", permission='root_administration')
105 request_method="PATCH", permission='root_administration')
106 def users_update(request):
106 def users_update(request):
107 """
107 """
108 Updates user object
108 Updates user object
109 """
109 """
110 user = User.by_id(request.matchdict.get('user_id'))
110 user = User.by_id(request.matchdict.get('user_id'))
111 if not user:
111 if not user:
112 return HTTPNotFound()
112 return HTTPNotFound()
113 post_data = request.safe_json_body or {}
113 post_data = request.safe_json_body or {}
114 if request.method == 'PATCH':
114 if request.method == 'PATCH':
115 form = forms.UserUpdateForm(MultiDict(post_data),
115 form = forms.UserUpdateForm(MultiDict(post_data),
116 csrf_context=request)
116 csrf_context=request)
117 if form.validate():
117 if form.validate():
118 form.populate_obj(user, ignore_none=True)
118 form.populate_obj(user, ignore_none=True)
119 if form.user_password.data:
119 if form.user_password.data:
120 user.set_password(user.user_password)
120 user.set_password(user.user_password)
121 if form.status.data:
121 if form.status.data:
122 user.status = 1
122 user.status = 1
123 else:
123 else:
124 user.status = 0
124 user.status = 0
125 else:
125 else:
126 return HTTPUnprocessableEntity(body=form.errors_json)
126 return HTTPUnprocessableEntity(body=form.errors_json)
127 return user.get_dict(exclude_keys=['security_code_date', 'notes',
127 return user.get_dict(exclude_keys=['security_code_date', 'notes',
128 'security_code', 'user_password'])
128 'security_code', 'user_password'])
129
129
130
130
131 @view_config(route_name='users_property',
131 @view_config(route_name='users_property',
132 match_param='key=resource_permissions',
132 match_param='key=resource_permissions',
133 renderer='json', permission='authenticated')
133 renderer='json', permission='authenticated')
134 def users_resource_permissions_list(request):
134 def users_resource_permissions_list(request):
135 """
135 """
136 Get list of permissions assigned to specific resources
136 Get list of permissions assigned to specific resources
137 """
137 """
138 user = User.by_id(request.matchdict.get('user_id'))
138 user = User.by_id(request.matchdict.get('user_id'))
139 if not user:
139 if not user:
140 return HTTPNotFound()
140 return HTTPNotFound()
141 return [permission_tuple_to_dict(perm) for perm in
141 return [permission_tuple_to_dict(perm) for perm in
142 user.resources_with_possible_perms()]
142 user.resources_with_possible_perms()]
143
143
144
144
145 @view_config(route_name='users', renderer='json',
145 @view_config(route_name='users', renderer='json',
146 request_method="DELETE", permission='root_administration')
146 request_method="DELETE", permission='root_administration')
147 def users_DELETE(request):
147 def users_DELETE(request):
148 """
148 """
149 Removes a user permanently from db - makes a check to see if after the
149 Removes a user permanently from db - makes a check to see if after the
150 operation there will be at least one admin left
150 operation there will be at least one admin left
151 """
151 """
152 msg = _('There needs to be at least one administrator in the system')
152 msg = _('There needs to be at least one administrator in the system')
153 user = User.by_id(request.matchdict.get('user_id'))
153 user = User.by_id(request.matchdict.get('user_id'))
154 if user:
154 if user:
155 users = User.users_for_perms(['root_administration']).all()
155 users = User.users_for_perms(['root_administration']).all()
156 if len(users) < 2 and user.id == users[0].id:
156 if len(users) < 2 and user.id == users[0].id:
157 request.session.flash(msg, 'warning')
157 request.session.flash(msg, 'warning')
158 else:
158 else:
159 DBSession.delete(user)
159 DBSession.delete(user)
160 request.session.flash(_('User removed'))
160 request.session.flash(_('User removed'))
161 return True
161 return True
162 request.response.status = 422
162 request.response.status = 422
163 return False
163 return False
164
164
165
165
166 @view_config(route_name='users_self', renderer='json',
166 @view_config(route_name='users_self', renderer='json',
167 request_method="GET", permission='authenticated')
167 request_method="GET", permission='authenticated')
168 @view_config(route_name='users_self', renderer='json',
168 @view_config(route_name='users_self', renderer='json',
169 request_method="PATCH", permission='authenticated')
169 request_method="PATCH", permission='authenticated')
170 def users_self(request):
170 def users_self(request):
171 """
171 """
172 Updates user personal information
172 Updates user personal information
173 """
173 """
174
174
175 if request.method == 'PATCH':
175 if request.method == 'PATCH':
176 form = forms.gen_user_profile_form()(
176 form = forms.gen_user_profile_form()(
177 MultiDict(request.unsafe_json_body),
177 MultiDict(request.unsafe_json_body),
178 csrf_context=request)
178 csrf_context=request)
179 if form.validate():
179 if form.validate():
180 form.populate_obj(request.user)
180 form.populate_obj(request.user)
181 request.session.flash(_('Your profile got updated.'))
181 request.session.flash(_('Your profile got updated.'))
182 else:
182 else:
183 return HTTPUnprocessableEntity(body=form.errors_json)
183 return HTTPUnprocessableEntity(body=form.errors_json)
184 return request.user.get_dict(exclude_keys=['security_code_date', 'notes',
184 return request.user.get_dict(exclude_keys=['security_code_date', 'notes',
185 'security_code',
185 'security_code',
186 'user_password'])
186 'user_password'])
187
187
188
188
189 @view_config(route_name='users_self_property',
189 @view_config(route_name='users_self_property',
190 match_param='key=external_identities', renderer='json',
190 match_param='key=external_identities', renderer='json',
191 request_method='GET', permission='authenticated')
191 request_method='GET', permission='authenticated')
192 def users_external_identies(request):
192 def users_external_identies(request):
193 user = request.user
193 user = request.user
194 identities = [{'provider': ident.provider_name,
194 identities = [{'provider': ident.provider_name,
195 'id': ident.external_user_name} for ident
195 'id': ident.external_user_name} for ident
196 in user.external_identities.all()]
196 in user.external_identities.all()]
197 return identities
197 return identities
198
198
199
199
200 @view_config(route_name='users_self_property',
200 @view_config(route_name='users_self_property',
201 match_param='key=external_identities', renderer='json',
201 match_param='key=external_identities', renderer='json',
202 request_method='DELETE', permission='authenticated')
202 request_method='DELETE', permission='authenticated')
203 def users_external_identies_DELETE(request):
203 def users_external_identies_DELETE(request):
204 """
204 """
205 Unbinds external identities(google,twitter etc.) from user account
205 Unbinds external identities(google,twitter etc.) from user account
206 """
206 """
207 user = request.user
207 user = request.user
208 for identity in user.external_identities.all():
208 for identity in user.external_identities.all():
209 log.info('found identity %s' % identity)
209 log.info('found identity %s' % identity)
210 if (identity.provider_name == request.params.get('provider') and
210 if (identity.provider_name == request.params.get('provider') and
211 identity.external_user_name == request.params.get('id')):
211 identity.external_user_name == request.params.get('id')):
212 log.info('remove identity %s' % identity)
212 log.info('remove identity %s' % identity)
213 DBSession.delete(identity)
213 DBSession.delete(identity)
214 return True
214 return True
215 return False
215 return False
216
216
217
217
218 @view_config(route_name='users_self_property',
218 @view_config(route_name='users_self_property',
219 match_param='key=password', renderer='json',
219 match_param='key=password', renderer='json',
220 request_method='PATCH', permission='authenticated')
220 request_method='PATCH', permission='authenticated')
221 def users_password(request):
221 def users_password(request):
222 """
222 """
223 Sets new password for user account
223 Sets new password for user account
224 """
224 """
225 user = request.user
225 user = request.user
226 form = forms.ChangePasswordForm(MultiDict(request.unsafe_json_body),
226 form = forms.ChangePasswordForm(MultiDict(request.unsafe_json_body),
227 csrf_context=request)
227 csrf_context=request)
228 form.old_password.user = user
228 form.old_password.user = user
229 if form.validate():
229 if form.validate():
230 user.regenerate_security_code()
230 user.regenerate_security_code()
231 user.set_password(form.new_password.data)
231 user.set_password(form.new_password.data)
232 msg = 'Your password got updated. ' \
232 msg = 'Your password got updated. ' \
233 'Next time log in with your new credentials.'
233 'Next time log in with your new credentials.'
234 request.session.flash(_(msg))
234 request.session.flash(_(msg))
235 return True
235 return True
236 else:
236 else:
237 return HTTPUnprocessableEntity(body=form.errors_json)
237 return HTTPUnprocessableEntity(body=form.errors_json)
238 return False
238 return False
239
239
240
240
241 @view_config(route_name='users_self_property', match_param='key=websocket',
241 @view_config(route_name='users_self_property', match_param='key=websocket',
242 renderer='json', permission='authenticated')
242 renderer='json', permission='authenticated')
243 def users_websocket(request):
243 def users_websocket(request):
244 """
244 """
245 Handle authorization of users trying to connect
245 Handle authorization of users trying to connect
246 """
246 """
247 # handle preflight request
247 # handle preflight request
248 user = request.user
248 user = request.user
249 if request.method == 'OPTIONS':
249 if request.method == 'OPTIONS':
250 res = request.response.body('OK')
250 res = request.response.body('OK')
251 add_cors_headers(res)
251 add_cors_headers(res)
252 return res
252 return res
253 applications = user.resources_with_perms(
253 applications = user.resources_with_perms(
254 ['view'], resource_types=['application'])
254 ['view'], resource_types=['application'])
255 channels = ['app_%s' % app.resource_id for app in applications]
255 channels = ['app_%s' % app.resource_id for app in applications]
256 payload = {"username": user.user_name,
256 payload = {"username": user.user_name,
257 "conn_id": str(uuid.uuid4()),
257 "conn_id": str(uuid.uuid4()),
258 "channels": channels
258 "channels": channels
259 }
259 }
260 settings = request.registry.settings
260 settings = request.registry.settings
261 response = cometd_request(
261 response = cometd_request(
262 settings['cometd.secret'], '/connect', payload,
262 settings['cometd.secret'], '/connect', payload,
263 servers=[request.registry.settings['cometd_servers']],
263 servers=[request.registry.settings['cometd_servers']],
264 throw_exceptions=True)
264 throw_exceptions=True)
265 return payload
265 return payload
266
266
267
267
268 @view_config(route_name='users_self_property', request_method="GET",
268 @view_config(route_name='users_self_property', request_method="GET",
269 match_param='key=alert_channels', renderer='json',
269 match_param='key=alert_channels', renderer='json',
270 permission='authenticated')
270 permission='authenticated')
271 def alert_channels(request):
271 def alert_channels(request):
272 """
272 """
273 Lists all available alert channels
273 Lists all available alert channels
274 """
274 """
275 user = request.user
275 user = request.user
276 return [c.get_dict(extended_info=True) for c in user.alert_channels]
276 return [c.get_dict(extended_info=True) for c in user.alert_channels]
277
277
278
278
279 @view_config(route_name='users_self_property', match_param='key=alert_actions',
279 @view_config(route_name='users_self_property', match_param='key=alert_actions',
280 request_method="GET", renderer='json', permission='authenticated')
280 request_method="GET", renderer='json', permission='authenticated')
281 def alert_actions(request):
281 def alert_actions(request):
282 """
282 """
283 Lists all available alert channels
283 Lists all available alert channels
284 """
284 """
285 user = request.user
285 user = request.user
286 return [r.get_dict(extended_info=True) for r in user.alert_actions]
286 return [r.get_dict(extended_info=True) for r in user.alert_actions]
287
287
288
288
289 @view_config(route_name='users_self_property', renderer='json',
289 @view_config(route_name='users_self_property', renderer='json',
290 match_param='key=alert_channels_rules', request_method='POST',
290 match_param='key=alert_channels_rules', request_method='POST',
291 permission='authenticated')
291 permission='authenticated')
292 def alert_channels_rule_POST(request):
292 def alert_channels_rule_POST(request):
293 """
293 """
294 Creates new notification rule for specific alert channel
294 Creates new notification rule for specific alert channel
295 """
295 """
296 user = request.user
296 user = request.user
297 alert_action = AlertChannelAction(owner_id=request.user.id,
297 alert_action = AlertChannelAction(owner_id=request.user.id,
298 type='report')
298 type='report')
299 DBSession.add(alert_action)
299 DBSession.add(alert_action)
300 DBSession.flush()
300 DBSession.flush()
301 return alert_action.get_dict()
301 return alert_action.get_dict()
302
302
303
303
304 @view_config(route_name='users_self_property', permission='authenticated',
304 @view_config(route_name='users_self_property', permission='authenticated',
305 match_param='key=alert_channels_rules',
305 match_param='key=alert_channels_rules',
306 renderer='json', request_method='DELETE')
306 renderer='json', request_method='DELETE')
307 def alert_channels_rule_DELETE(request):
307 def alert_channels_rule_DELETE(request):
308 """
308 """
309 Removes specific alert channel rule
309 Removes specific alert channel rule
310 """
310 """
311 user = request.user
311 user = request.user
312 rule_action = AlertChannelActionService.by_owner_id_and_pkey(
312 rule_action = AlertChannelActionService.by_owner_id_and_pkey(
313 user.id,
313 user.id,
314 request.GET.get('pkey'))
314 request.GET.get('pkey'))
315 if rule_action:
315 if rule_action:
316 DBSession.delete(rule_action)
316 DBSession.delete(rule_action)
317 return True
317 return True
318 return HTTPNotFound()
318 return HTTPNotFound()
319
319
320
320
321 @view_config(route_name='users_self_property', permission='authenticated',
321 @view_config(route_name='users_self_property', permission='authenticated',
322 match_param='key=alert_channels_rules',
322 match_param='key=alert_channels_rules',
323 renderer='json', request_method='PATCH')
323 renderer='json', request_method='PATCH')
324 def alert_channels_rule_PATCH(request):
324 def alert_channels_rule_PATCH(request):
325 """
325 """
326 Removes specific alert channel rule
326 Removes specific alert channel rule
327 """
327 """
328 user = request.user
328 user = request.user
329 json_body = request.unsafe_json_body
329 json_body = request.unsafe_json_body
330
330
331 schema = build_rule_schema(json_body['rule'], REPORT_TYPE_MATRIX)
331 schema = build_rule_schema(json_body['rule'], REPORT_TYPE_MATRIX)
332 try:
332 try:
333 schema.deserialize(json_body['rule'])
333 schema.deserialize(json_body['rule'])
334 except colander.Invalid as exc:
334 except colander.Invalid as exc:
335 return HTTPUnprocessableEntity(body=json.dumps(exc.asdict()))
335 return HTTPUnprocessableEntity(body=json.dumps(exc.asdict()))
336
336
337 rule_action = AlertChannelActionService.by_owner_id_and_pkey(
337 rule_action = AlertChannelActionService.by_owner_id_and_pkey(
338 user.id,
338 user.id,
339 request.GET.get('pkey'))
339 request.GET.get('pkey'))
340
340
341 if rule_action:
341 if rule_action:
342 rule_action.rule = json_body['rule']
342 rule_action.rule = json_body['rule']
343 rule_action.resource_id = json_body['resource_id']
343 rule_action.resource_id = json_body['resource_id']
344 rule_action.action = json_body['action']
344 rule_action.action = json_body['action']
345 return rule_action.get_dict()
345 return rule_action.get_dict()
346 return HTTPNotFound()
346 return HTTPNotFound()
347
347
348
348
349 @view_config(route_name='users_self_property', permission='authenticated',
349 @view_config(route_name='users_self_property', permission='authenticated',
350 match_param='key=alert_channels',
350 match_param='key=alert_channels',
351 renderer='json', request_method='PATCH')
351 renderer='json', request_method='PATCH')
352 def alert_channels_PATCH(request):
352 def alert_channels_PATCH(request):
353 user = request.user
353 user = request.user
354 channel_name = request.GET.get('channel_name')
354 channel_name = request.GET.get('channel_name')
355 channel_value = request.GET.get('channel_value')
355 channel_value = request.GET.get('channel_value')
356 # iterate over channels
356 # iterate over channels
357 channel = None
357 channel = None
358 for channel in user.alert_channels:
358 for channel in user.alert_channels:
359 if (channel.channel_name == channel_name and
359 if (channel.channel_name == channel_name and
360 channel.channel_value == channel_value):
360 channel.channel_value == channel_value):
361 break
361 break
362 if not channel:
362 if not channel:
363 return HTTPNotFound()
363 return HTTPNotFound()
364
364
365 allowed_keys = ['daily_digest', 'send_alerts']
365 allowed_keys = ['daily_digest', 'send_alerts']
366 for k, v in request.unsafe_json_body.items():
366 for k, v in request.unsafe_json_body.items():
367 if k in allowed_keys:
367 if k in allowed_keys:
368 setattr(channel, k, v)
368 setattr(channel, k, v)
369 else:
369 else:
370 return HTTPBadRequest()
370 return HTTPBadRequest()
371 return channel.get_dict()
371 return channel.get_dict()
372
372
373
373
374 @view_config(route_name='users_self_property', permission='authenticated',
374 @view_config(route_name='users_self_property', permission='authenticated',
375 match_param='key=alert_channels',
375 match_param='key=alert_channels',
376 request_method="POST", renderer='json')
376 request_method="POST", renderer='json')
377 def alert_channels_POST(request):
377 def alert_channels_POST(request):
378 """
378 """
379 Creates a new email alert channel for user, sends a validation email
379 Creates a new email alert channel for user, sends a validation email
380 """
380 """
381 user = request.user
381 user = request.user
382 form = forms.EmailChannelCreateForm(MultiDict(request.unsafe_json_body),
382 form = forms.EmailChannelCreateForm(MultiDict(request.unsafe_json_body),
383 csrf_context=request)
383 csrf_context=request)
384 if not form.validate():
384 if not form.validate():
385 return HTTPUnprocessableEntity(body=form.errors_json)
385 return HTTPUnprocessableEntity(body=form.errors_json)
386
386
387 email = form.email.data.strip()
387 email = form.email.data.strip()
388 channel = EmailAlertChannel()
388 channel = EmailAlertChannel()
389 channel.channel_name = 'email'
389 channel.channel_name = 'email'
390 channel.channel_value = email
390 channel.channel_value = email
391 security_code = generate_random_string(10)
391 security_code = generate_random_string(10)
392 channel.channel_json_conf = {'security_code': security_code}
392 channel.channel_json_conf = {'security_code': security_code}
393 user.alert_channels.append(channel)
393 user.alert_channels.append(channel)
394
394
395 email_vars = {'user': user,
395 email_vars = {'user': user,
396 'email': email,
396 'email': email,
397 'request': request,
397 'request': request,
398 'security_code': security_code,
398 'security_code': security_code,
399 'email_title': "App Enlight :: "
399 'email_title': "App Enlight :: "
400 "Please authorize your email"}
400 "Please authorize your email"}
401
401
402 UserService.send_email(request, recipients=[email],
402 UserService.send_email(request, recipients=[email],
403 variables=email_vars,
403 variables=email_vars,
404 template='/email_templates/authorize_email.jinja2')
404 template='/email_templates/authorize_email.jinja2')
405 request.session.flash(_('Your alert channel was '
405 request.session.flash(_('Your alert channel was '
406 'added to the system.'))
406 'added to the system.'))
407 request.session.flash(
407 request.session.flash(
408 _('You need to authorize your email channel, a message was '
408 _('You need to authorize your email channel, a message was '
409 'sent containing necessary information.'),
409 'sent containing necessary information.'),
410 'warning')
410 'warning')
411 DBSession.flush()
411 DBSession.flush()
412 channel.get_dict()
412 channel.get_dict()
413
413
414
414
415 @view_config(route_name='section_view',
415 @view_config(route_name='section_view',
416 match_param=['section=user_section',
416 match_param=['section=user_section',
417 'view=alert_channels_authorize'],
417 'view=alert_channels_authorize'],
418 renderer='string', permission='authenticated')
418 renderer='string', permission='authenticated')
419 def alert_channels_authorize(request):
419 def alert_channels_authorize(request):
420 """
420 """
421 Performs alert channel authorization based on auth code sent in email
421 Performs alert channel authorization based on auth code sent in email
422 """
422 """
423 user = request.user
423 user = request.user
424 for channel in user.alert_channels:
424 for channel in user.alert_channels:
425 security_code = request.params.get('security_code', '')
425 security_code = request.params.get('security_code', '')
426 if channel.channel_json_conf['security_code'] == security_code:
426 if channel.channel_json_conf['security_code'] == security_code:
427 channel.channel_validated = True
427 channel.channel_validated = True
428 request.session.flash(_('Your email was authorized.'))
428 request.session.flash(_('Your email was authorized.'))
429 return HTTPFound(location=request.route_url('/'))
429 return HTTPFound(location=request.route_url('/'))
430
430
431
431
432 @view_config(route_name='users_self_property', request_method="DELETE",
432 @view_config(route_name='users_self_property', request_method="DELETE",
433 match_param='key=alert_channels', renderer='json',
433 match_param='key=alert_channels', renderer='json',
434 permission='authenticated')
434 permission='authenticated')
435 def alert_channel_DELETE(request):
435 def alert_channel_DELETE(request):
436 """
436 """
437 Removes alert channel from users channel
437 Removes alert channel from users channel
438 """
438 """
439 user = request.user
439 user = request.user
440 channel = None
440 channel = None
441 for chan in user.alert_channels:
441 for chan in user.alert_channels:
442 if (chan.channel_name == request.params.get('channel_name') and
442 if (chan.channel_name == request.params.get('channel_name') and
443 chan.channel_value == request.params.get('channel_value')):
443 chan.channel_value == request.params.get('channel_value')):
444 channel = chan
444 channel = chan
445 break
445 break
446 if channel:
446 if channel:
447 user.alert_channels.remove(channel)
447 user.alert_channels.remove(channel)
448 request.session.flash(_('Your channel was removed.'))
448 request.session.flash(_('Your channel was removed.'))
449 return True
449 return True
450 return False
450 return False
451
451
452
452
453 @view_config(route_name='users_self_property', permission='authenticated',
453 @view_config(route_name='users_self_property', permission='authenticated',
454 match_param='key=alert_channels_actions_binds',
454 match_param='key=alert_channels_actions_binds',
455 renderer='json', request_method="POST")
455 renderer='json', request_method="POST")
456 def alert_channels_actions_binds_POST(request):
456 def alert_channels_actions_binds_POST(request):
457 """
457 """
458 Adds alert action to users channels
458 Adds alert action to users channels
459 """
459 """
460 user = request.user
460 user = request.user
461 json_body = request.unsafe_json_body
461 json_body = request.unsafe_json_body
462 channel = AlertChannelService.by_owner_id_and_pkey(
462 channel = AlertChannelService.by_owner_id_and_pkey(
463 user.id,
463 user.id,
464 json_body.get('channel_pkey'))
464 json_body.get('channel_pkey'))
465
465
466 rule_action = AlertChannelActionService.by_owner_id_and_pkey(
466 rule_action = AlertChannelActionService.by_owner_id_and_pkey(
467 user.id,
467 user.id,
468 json_body.get('action_pkey'))
468 json_body.get('action_pkey'))
469
469
470 if channel and rule_action:
470 if channel and rule_action:
471 if channel.pkey not in [c.pkey for c in rule_action.channels]:
471 if channel.pkey not in [c.pkey for c in rule_action.channels]:
472 rule_action.channels.append(channel)
472 rule_action.channels.append(channel)
473 return rule_action.get_dict(extended_info=True)
473 return rule_action.get_dict(extended_info=True)
474 return HTTPUnprocessableEntity()
474 return HTTPUnprocessableEntity()
475
475
476
476
477 @view_config(route_name='users_self_property', request_method="DELETE",
477 @view_config(route_name='users_self_property', request_method="DELETE",
478 match_param='key=alert_channels_actions_binds',
478 match_param='key=alert_channels_actions_binds',
479 renderer='json', permission='authenticated')
479 renderer='json', permission='authenticated')
480 def alert_channels_actions_binds_DELETE(request):
480 def alert_channels_actions_binds_DELETE(request):
481 """
481 """
482 Removes alert action from users channels
482 Removes alert action from users channels
483 """
483 """
484 user = request.user
484 user = request.user
485 channel = AlertChannelService.by_owner_id_and_pkey(
485 channel = AlertChannelService.by_owner_id_and_pkey(
486 user.id,
486 user.id,
487 request.GET.get('channel_pkey'))
487 request.GET.get('channel_pkey'))
488
488
489 rule_action = AlertChannelActionService.by_owner_id_and_pkey(
489 rule_action = AlertChannelActionService.by_owner_id_and_pkey(
490 user.id,
490 user.id,
491 request.GET.get('action_pkey'))
491 request.GET.get('action_pkey'))
492
492
493 if channel and rule_action:
493 if channel and rule_action:
494 if channel.pkey in [c.pkey for c in rule_action.channels]:
494 if channel.pkey in [c.pkey for c in rule_action.channels]:
495 rule_action.channels.remove(channel)
495 rule_action.channels.remove(channel)
496 return rule_action.get_dict(extended_info=True)
496 return rule_action.get_dict(extended_info=True)
497 return HTTPUnprocessableEntity()
497 return HTTPUnprocessableEntity()
498
498
499
499
500 @view_config(route_name='social_auth_abort',
500 @view_config(route_name='social_auth_abort',
501 renderer='string', permission=NO_PERMISSION_REQUIRED)
501 renderer='string', permission=NO_PERMISSION_REQUIRED)
502 def oauth_abort(request):
502 def oauth_abort(request):
503 """
503 """
504 Handles problems with authorization via velruse
504 Handles problems with authorization via velruse
505 """
505 """
506
506
507
507
508 @view_config(route_name='social_auth', permission=NO_PERMISSION_REQUIRED)
508 @view_config(route_name='social_auth', permission=NO_PERMISSION_REQUIRED)
509 def social_auth(request):
509 def social_auth(request):
510 # Get the internal provider name URL variable.
510 # Get the internal provider name URL variable.
511 provider_name = request.matchdict.get('provider')
511 provider_name = request.matchdict.get('provider')
512
512
513 # Start the login procedure.
513 # Start the login procedure.
514 adapter = WebObAdapter(request, request.response)
514 adapter = WebObAdapter(request, request.response)
515 result = request.registry.authomatic.login(adapter, provider_name)
515 result = request.authomatic.login(adapter, provider_name)
516 if result:
516 if result:
517 if result.error:
517 if result.error:
518 return handle_auth_error(request, result)
518 return handle_auth_error(request, result)
519 elif result.user:
519 elif result.user:
520 return handle_auth_success(request, result)
520 return handle_auth_success(request, result)
521 return request.response
521 return request.response
522
522
523
523
524 def handle_auth_error(request, result):
524 def handle_auth_error(request, result):
525 # Login procedure finished with an error.
525 # Login procedure finished with an error.
526 request.session.pop('zigg.social_auth', None)
526 request.session.pop('zigg.social_auth', None)
527 request.session.flash(_('Something went wrong when we tried to '
527 request.session.flash(_('Something went wrong when we tried to '
528 'authorize you via external provider. '
528 'authorize you via external provider. '
529 'Please try again.'), 'warning')
529 'Please try again.'), 'warning')
530
530
531 return HTTPFound(location=request.route_url('/'))
531 return HTTPFound(location=request.route_url('/'))
532
532
533
533
534 def handle_auth_success(request, result):
534 def handle_auth_success(request, result):
535 # Hooray, we have the user!
535 # Hooray, we have the user!
536 # OAuth 2.0 and OAuth 1.0a provide only limited user data on login,
536 # OAuth 2.0 and OAuth 1.0a provide only limited user data on login,
537 # We need to update the user to get more info.
537 # We need to update the user to get more info.
538 if result.user:
538 if result.user:
539 result.user.update()
539 result.user.update()
540
540
541 social_data = {
541 social_data = {
542 'user': {'data': result.user.data},
542 'user': {'data': result.user.data},
543 'credentials': result.user.credentials
543 'credentials': result.user.credentials
544 }
544 }
545 # normalize data
545 # normalize data
546 social_data['user']['id'] = result.user.id
546 social_data['user']['id'] = result.user.id
547 user_name = result.user.username or ''
547 user_name = result.user.username or ''
548 # use email name as username for google
548 # use email name as username for google
549 if (social_data['credentials'].provider_name == 'google' and
549 if (social_data['credentials'].provider_name == 'google' and
550 result.user.email):
550 result.user.email):
551 user_name = result.user.email
551 user_name = result.user.email
552 social_data['user']['user_name'] = user_name
552 social_data['user']['user_name'] = user_name
553 social_data['user']['email'] = result.user.email or ''
553 social_data['user']['email'] = result.user.email or ''
554
554
555 request.session['zigg.social_auth'] = social_data
555 request.session['zigg.social_auth'] = social_data
556 # user is logged so bind his external identity with account
556 # user is logged so bind his external identity with account
557 if request.user:
557 if request.user:
558 handle_social_data(request, request.user, social_data)
558 handle_social_data(request, request.user, social_data)
559 request.session.pop('zigg.social_auth', None)
559 request.session.pop('zigg.social_auth', None)
560 return HTTPFound(location=request.route_url('/'))
560 return HTTPFound(location=request.route_url('/'))
561 else:
561 else:
562 user = ExternalIdentityService.user_by_external_id_and_provider(
562 user = ExternalIdentityService.user_by_external_id_and_provider(
563 social_data['user']['id'],
563 social_data['user']['id'],
564 social_data['credentials'].provider_name
564 social_data['credentials'].provider_name
565 )
565 )
566 # fix legacy accounts with wrong google ID
566 # fix legacy accounts with wrong google ID
567 if not user and social_data['credentials'].provider_name == 'google':
567 if not user and social_data['credentials'].provider_name == 'google':
568 user = ExternalIdentityService.user_by_external_id_and_provider(
568 user = ExternalIdentityService.user_by_external_id_and_provider(
569 social_data['user']['email'],
569 social_data['user']['email'],
570 social_data['credentials'].provider_name)
570 social_data['credentials'].provider_name)
571
571
572 # user tokens are already found in our db
572 # user tokens are already found in our db
573 if user:
573 if user:
574 handle_social_data(request, user, social_data)
574 handle_social_data(request, user, social_data)
575 headers = security.remember(request, user.id)
575 headers = security.remember(request, user.id)
576 request.session.pop('zigg.social_auth', None)
576 request.session.pop('zigg.social_auth', None)
577 return HTTPFound(location=request.route_url('/'), headers=headers)
577 return HTTPFound(location=request.route_url('/'), headers=headers)
578 else:
578 else:
579 msg = 'You need to finish registration ' \
579 msg = 'You need to finish registration ' \
580 'process to bind your external identity to your account ' \
580 'process to bind your external identity to your account ' \
581 'or sign in to existing account'
581 'or sign in to existing account'
582 request.session.flash(msg)
582 request.session.flash(msg)
583 return HTTPFound(location=request.route_url('register'))
583 return HTTPFound(location=request.route_url('register'))
584
584
585
585
586 @view_config(route_name='section_view', permission='authenticated',
586 @view_config(route_name='section_view', permission='authenticated',
587 match_param=['section=users_section', 'view=search_users'],
587 match_param=['section=users_section', 'view=search_users'],
588 renderer='json')
588 renderer='json')
589 def search_users(request):
589 def search_users(request):
590 """
590 """
591 Returns a list of users for autocomplete
591 Returns a list of users for autocomplete
592 """
592 """
593 user = request.user
593 user = request.user
594 items_returned = []
594 items_returned = []
595 like_condition = request.params.get('user_name', '') + '%'
595 like_condition = request.params.get('user_name', '') + '%'
596 # first append used if email is passed
596 # first append used if email is passed
597 found_user = User.by_email(request.params.get('user_name', ''))
597 found_user = User.by_email(request.params.get('user_name', ''))
598 if found_user:
598 if found_user:
599 name = '{} {}'.format(found_user.first_name, found_user.last_name)
599 name = '{} {}'.format(found_user.first_name, found_user.last_name)
600 items_returned.append({'user': found_user.user_name, 'name': name})
600 items_returned.append({'user': found_user.user_name, 'name': name})
601 for found_user in User.user_names_like(like_condition).limit(20):
601 for found_user in User.user_names_like(like_condition).limit(20):
602 name = '{} {}'.format(found_user.first_name, found_user.last_name)
602 name = '{} {}'.format(found_user.first_name, found_user.last_name)
603 items_returned.append({'user': found_user.user_name, 'name': name})
603 items_returned.append({'user': found_user.user_name, 'name': name})
604 return items_returned
604 return items_returned
605
605
606
606
607 @view_config(route_name='users_self_property', match_param='key=auth_tokens',
607 @view_config(route_name='users_self_property', match_param='key=auth_tokens',
608 request_method="GET", renderer='json', permission='authenticated')
608 request_method="GET", renderer='json', permission='authenticated')
609 @view_config(route_name='users_property', match_param='key=auth_tokens',
609 @view_config(route_name='users_property', match_param='key=auth_tokens',
610 request_method="GET", renderer='json', permission='authenticated')
610 request_method="GET", renderer='json', permission='authenticated')
611 def auth_tokens_list(request):
611 def auth_tokens_list(request):
612 """
612 """
613 Lists all available alert channels
613 Lists all available alert channels
614 """
614 """
615 if request.matched_route.name == 'users_self_property':
615 if request.matched_route.name == 'users_self_property':
616 user = request.user
616 user = request.user
617 else:
617 else:
618 user = User.by_id(request.matchdict.get('user_id'))
618 user = User.by_id(request.matchdict.get('user_id'))
619 if not user:
619 if not user:
620 return HTTPNotFound()
620 return HTTPNotFound()
621 return [c.get_dict() for c in user.auth_tokens]
621 return [c.get_dict() for c in user.auth_tokens]
622
622
623
623
624 @view_config(route_name='users_self_property', match_param='key=auth_tokens',
624 @view_config(route_name='users_self_property', match_param='key=auth_tokens',
625 request_method="POST", renderer='json',
625 request_method="POST", renderer='json',
626 permission='authenticated')
626 permission='authenticated')
627 @view_config(route_name='users_property', match_param='key=auth_tokens',
627 @view_config(route_name='users_property', match_param='key=auth_tokens',
628 request_method="POST", renderer='json',
628 request_method="POST", renderer='json',
629 permission='authenticated')
629 permission='authenticated')
630 def auth_tokens_POST(request):
630 def auth_tokens_POST(request):
631 """
631 """
632 Lists all available alert channels
632 Lists all available alert channels
633 """
633 """
634 if request.matched_route.name == 'users_self_property':
634 if request.matched_route.name == 'users_self_property':
635 user = request.user
635 user = request.user
636 else:
636 else:
637 user = User.by_id(request.matchdict.get('user_id'))
637 user = User.by_id(request.matchdict.get('user_id'))
638 if not user:
638 if not user:
639 return HTTPNotFound()
639 return HTTPNotFound()
640
640
641 req_data = request.safe_json_body or {}
641 req_data = request.safe_json_body or {}
642 if not req_data.get('expires'):
642 if not req_data.get('expires'):
643 req_data.pop('expires', None)
643 req_data.pop('expires', None)
644 form = forms.AuthTokenCreateForm(MultiDict(req_data), csrf_context=request)
644 form = forms.AuthTokenCreateForm(MultiDict(req_data), csrf_context=request)
645 if not form.validate():
645 if not form.validate():
646 return HTTPUnprocessableEntity(body=form.errors_json)
646 return HTTPUnprocessableEntity(body=form.errors_json)
647 token = AuthToken()
647 token = AuthToken()
648 form.populate_obj(token)
648 form.populate_obj(token)
649 if token.expires:
649 if token.expires:
650 interval = h.time_deltas.get(token.expires)['delta']
650 interval = h.time_deltas.get(token.expires)['delta']
651 token.expires = datetime.datetime.utcnow() + interval
651 token.expires = datetime.datetime.utcnow() + interval
652 user.auth_tokens.append(token)
652 user.auth_tokens.append(token)
653 DBSession.flush()
653 DBSession.flush()
654 return token.get_dict()
654 return token.get_dict()
655
655
656
656
657 @view_config(route_name='users_self_property', match_param='key=auth_tokens',
657 @view_config(route_name='users_self_property', match_param='key=auth_tokens',
658 request_method="DELETE", renderer='json',
658 request_method="DELETE", renderer='json',
659 permission='authenticated')
659 permission='authenticated')
660 @view_config(route_name='users_property', match_param='key=auth_tokens',
660 @view_config(route_name='users_property', match_param='key=auth_tokens',
661 request_method="DELETE", renderer='json',
661 request_method="DELETE", renderer='json',
662 permission='authenticated')
662 permission='authenticated')
663 def auth_tokens_DELETE(request):
663 def auth_tokens_DELETE(request):
664 """
664 """
665 Lists all available alert channels
665 Lists all available alert channels
666 """
666 """
667 if request.matched_route.name == 'users_self_property':
667 if request.matched_route.name == 'users_self_property':
668 user = request.user
668 user = request.user
669 else:
669 else:
670 user = User.by_id(request.matchdict.get('user_id'))
670 user = User.by_id(request.matchdict.get('user_id'))
671 if not user:
671 if not user:
672 return HTTPNotFound()
672 return HTTPNotFound()
673
673
674 for token in user.auth_tokens:
674 for token in user.auth_tokens:
675 if token.token == request.params.get('token'):
675 if token.token == request.params.get('token'):
676 user.auth_tokens.remove(token)
676 user.auth_tokens.remove(token)
677 return True
677 return True
678 return False
678 return False
General Comments 0
You need to be logged in to leave comments. Login now