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