##// END OF EJS Templates
requirements: bump ziggurat_foundations to 0.8.3
ergo -
Show More
@@ -1,220 +1,220 b''
1 # Created by .ignore support plugin (hsz.mobi)
1 # Created by .ignore support plugin (hsz.mobi)
2 ### Node template
2 ### Node template
3 # Logs
3 # Logs
4 logs
4 logs
5 *.log
5 *.log
6 npm-debug.log*
6 npm-debug.log*
7 yarn-debug.log*
7 yarn-debug.log*
8 yarn-error.log*
8 yarn-error.log*
9
9
10 # Runtime data
10 # Runtime data
11 pids
11 pids
12 *.pid
12 *.pid
13 *.seed
13 *.seed
14 *.pid.lock
14 *.pid.lock
15
15
16 # Directory for instrumented libs generated by jscoverage/JSCover
16 # Directory for instrumented libs generated by jscoverage/JSCover
17 lib-cov
17 lib-cov
18
18
19 # Coverage directory used by tools like istanbul
19 # Coverage directory used by tools like istanbul
20 coverage
20 coverage
21
21
22 # nyc test coverage
22 # nyc test coverage
23 .nyc_output
23 .nyc_output
24
24
25 # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
25 # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
26 .grunt
26 .grunt
27
27
28 # Bower dependency directory (https://bower.io/)
28 # Bower dependency directory (https://bower.io/)
29 bower_components
29 bower_components
30
30
31 # node-waf configuration
31 # node-waf configuration
32 .lock-wscript
32 .lock-wscript
33
33
34 # Compiled binary addons (https://nodejs.org/api/addons.html)
34 # Compiled binary addons (https://nodejs.org/api/addons.html)
35 build/Release
35 build/Release
36
36
37 # Dependency directories
37 # Dependency directories
38 node_modules/
38 node_modules/
39 jspm_packages/
39 jspm_packages/
40
40
41 # Typescript v1 declaration files
41 # Typescript v1 declaration files
42 typings/
42 typings/
43
43
44 # Optional npm cache directory
44 # Optional npm cache directory
45 .npm
45 .npm
46
46
47 # Optional eslint cache
47 # Optional eslint cache
48 .eslintcache
48 .eslintcache
49
49
50 # Optional REPL history
50 # Optional REPL history
51 .node_repl_history
51 .node_repl_history
52
52
53 # Output of 'npm pack'
53 # Output of 'npm pack'
54 *.tgz
54 *.tgz
55
55
56 # Yarn Integrity file
56 # Yarn Integrity file
57 .yarn-integrity
57 .yarn-integrity
58
58
59 # dotenv environment variables file
59 # dotenv environment variables file
60 .env
60 .env
61
61
62 ### JetBrains template
62 ### JetBrains template
63 # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
63 # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
64 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
64 # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
65
65
66 # User-specific stuff:
66 # User-specific stuff:
67 .idea/**/workspace.xml
67 .idea/**/workspace.xml
68 .idea/**/tasks.xml
68 .idea/**/tasks.xml
69 .idea/dictionaries
69 .idea/dictionaries
70
70
71 # Sensitive or high-churn files:
71 # Sensitive or high-churn files:
72 .idea/**/dataSources/
72 .idea/**/dataSources/
73 .idea/**/dataSources.ids
73 .idea/**/dataSources.ids
74 .idea/**/dataSources.xml
74 .idea/**/dataSources.xml
75 .idea/**/dataSources.local.xml
75 .idea/**/dataSources.local.xml
76 .idea/**/sqlDataSources.xml
76 .idea/**/sqlDataSources.xml
77 .idea/**/dynamic.xml
77 .idea/**/dynamic.xml
78 .idea/**/uiDesigner.xml
78 .idea/**/uiDesigner.xml
79
79
80 # Gradle:
80 # Gradle:
81 .idea/**/gradle.xml
81 .idea/**/gradle.xml
82 .idea/**/libraries
82 .idea/**/libraries
83
83
84 # CMake
84 # CMake
85 cmake-build-debug/
85 cmake-build-debug/
86
86
87 # Mongo Explorer plugin:
87 # Mongo Explorer plugin:
88 .idea/**/mongoSettings.xml
88 .idea/**/mongoSettings.xml
89
89
90 ## File-based project format:
90 ## File-based project format:
91 *.iws
91 *.iws
92
92
93 ## Plugin-specific files:
93 ## Plugin-specific files:
94
94
95 # IntelliJ
95 # IntelliJ
96 out/
96 out/
97
97
98 # mpeltonen/sbt-idea plugin
98 # mpeltonen/sbt-idea plugin
99 .idea_modules/
99 .idea_modules/
100
100
101 # JIRA plugin
101 # JIRA plugin
102 atlassian-ide-plugin.xml
102 atlassian-ide-plugin.xml
103
103
104 # Cursive Clojure plugin
104 # Cursive Clojure plugin
105 .idea/replstate.xml
105 .idea/replstate.xml
106
106
107 # Crashlytics plugin (for Android Studio and IntelliJ)
107 # Crashlytics plugin (for Android Studio and IntelliJ)
108 com_crashlytics_export_strings.xml
108 com_crashlytics_export_strings.xml
109 crashlytics.properties
109 crashlytics.properties
110 crashlytics-build.properties
110 crashlytics-build.properties
111 fabric.properties
111 fabric.properties
112 ### Python template
112 ### Python template
113 # Byte-compiled / optimized / DLL files
113 # Byte-compiled / optimized / DLL files
114 __pycache__/
114 __pycache__/
115 *.py[cod]
115 *.py[cod]
116 *$py.class
116 *$py.class
117
117
118 # C extensions
118 # C extensions
119 *.so
119 *.so
120
120
121 # Distribution / packaging
121 # Distribution / packaging
122 .Python
122 .Python
123 build/
123 build/
124 develop-eggs/
124 develop-eggs/
125 dist/
125 dist/
126 downloads/
126 downloads/
127 eggs/
127 eggs/
128 .eggs/
128 .eggs/
129 lib/
129 /lib/
130 lib64/
130 /lib64/
131 parts/
131 parts/
132 sdist/
132 sdist/
133 var/
133 var/
134 wheels/
134 wheels/
135 *.egg-info/
135 *.egg-info/
136 .installed.cfg
136 .installed.cfg
137 *.egg
137 *.egg
138 MANIFEST
138 MANIFEST
139
139
140 # PyInstaller
140 # PyInstaller
141 # Usually these files are written by a python script from a template
141 # Usually these files are written by a python script from a template
142 # before PyInstaller builds the exe, so as to inject date/other infos into it.
142 # before PyInstaller builds the exe, so as to inject date/other infos into it.
143 *.manifest
143 *.manifest
144 *.spec
144 *.spec
145
145
146 # Installer logs
146 # Installer logs
147 pip-log.txt
147 pip-log.txt
148 pip-delete-this-directory.txt
148 pip-delete-this-directory.txt
149
149
150 # Unit test / coverage reports
150 # Unit test / coverage reports
151 htmlcov/
151 htmlcov/
152 .tox/
152 .tox/
153 .coverage
153 .coverage
154 .coverage.*
154 .coverage.*
155 .cache
155 .cache
156 nosetests.xml
156 nosetests.xml
157 coverage.xml
157 coverage.xml
158 *.cover
158 *.cover
159 .hypothesis/
159 .hypothesis/
160
160
161 # Translations
161 # Translations
162 *.mo
162 *.mo
163 *.pot
163 *.pot
164
164
165 # Django stuff:
165 # Django stuff:
166 local_settings.py
166 local_settings.py
167
167
168 # Flask stuff:
168 # Flask stuff:
169 instance/
169 instance/
170 .webassets-cache
170 .webassets-cache
171
171
172 # Scrapy stuff:
172 # Scrapy stuff:
173 .scrapy
173 .scrapy
174
174
175 # Sphinx documentation
175 # Sphinx documentation
176 docs/_build/
176 docs/_build/
177
177
178 # PyBuilder
178 # PyBuilder
179 target/
179 target/
180
180
181 # Jupyter Notebook
181 # Jupyter Notebook
182 .ipynb_checkpoints
182 .ipynb_checkpoints
183
183
184 # pyenv
184 # pyenv
185 .python-version
185 .python-version
186
186
187 # celery beat schedule file
187 # celery beat schedule file
188 celerybeat-schedule
188 celerybeat-schedule
189
189
190 # SageMath parsed files
190 # SageMath parsed files
191 *.sage.py
191 *.sage.py
192
192
193 # Environments
193 # Environments
194 .venv
194 .venv
195 env/
195 env/
196 venv/
196 venv/
197 ENV/
197 ENV/
198 env.bak/
198 env.bak/
199 venv.bak/
199 venv.bak/
200
200
201 # Spyder project settings
201 # Spyder project settings
202 .spyderproject
202 .spyderproject
203 .spyproject
203 .spyproject
204
204
205 # Rope project settings
205 # Rope project settings
206 .ropeproject
206 .ropeproject
207
207
208 # mkdocs documentation
208 # mkdocs documentation
209 /site
209 /site
210
210
211 # mypy
211 # mypy
212 .mypy_cache/
212 .mypy_cache/
213 ### Example user template template
213 ### Example user template template
214 ### Example user template
214 ### Example user template
215
215
216 # IntelliJ project files
216 # IntelliJ project files
217 .idea
217 .idea
218 *.iml
218 *.iml
219 out
219 out
220 gen
220 gen
@@ -1,49 +1,49 b''
1 repoze.sendmail==4.1
1 repoze.sendmail==4.1
2 pyramid==1.7.3
2 pyramid==1.7.3
3 pyramid_tm==0.12
3 pyramid_tm==0.12
4 pyramid_debugtoolbar
4 pyramid_debugtoolbar
5 pyramid_authstack==1.0.1
5 pyramid_authstack==1.0.1
6 SQLAlchemy==1.0.12
6 SQLAlchemy==1.0.12
7 alembic==1.0.8
7 alembic==1.0.8
8 webhelpers2==2.0
8 webhelpers2==2.0
9 transaction==1.4.3
9 transaction==1.4.3
10 zope.sqlalchemy==0.7.6
10 zope.sqlalchemy==0.7.6
11 pyramid_mailer==0.14.1
11 pyramid_mailer==0.14.1
12 redis==2.10.5
12 redis==2.10.5
13 redlock-py==1.0.8
13 redlock-py==1.0.8
14 pyramid_jinja2==2.6.2
14 pyramid_jinja2==2.6.2
15 psycopg2==2.7.7
15 psycopg2==2.7.7
16 wtforms==2.1
16 wtforms==2.1
17 celery==3.1.23
17 celery==3.1.23
18 formencode==1.3.0
18 formencode==1.3.0
19 psutil==2.1.2
19 psutil==2.1.2
20 ziggurat_foundations==0.6.8
20 ziggurat_foundations==0.8.3
21 bcrypt==3.1.6
21 bcrypt==3.1.6
22 appenlight_client
22 appenlight_client
23 markdown==2.5
23 markdown==2.5
24 colander==1.7
24 colander==1.7
25 defusedxml==0.5.0
25 defusedxml==0.5.0
26 dogpile.cache==0.5.7
26 dogpile.cache==0.5.7
27 pyramid_redis_sessions==1.0.1
27 pyramid_redis_sessions==1.0.1
28 simplejson==3.8.2
28 simplejson==3.8.2
29 waitress==1.0
29 waitress==1.0
30 gunicorn==19.9.0
30 gunicorn==19.9.0
31 requests==2.20.0
31 requests==2.20.0
32 requests_oauthlib==0.6.1
32 requests_oauthlib==0.6.1
33 gevent==1.1.1
33 gevent==1.1.1
34 gevent-websocket==0.9.5
34 gevent-websocket==0.9.5
35 pygments==2.1.3
35 pygments==2.1.3
36 lxml==4.3.2
36 lxml==4.3.2
37 paginate==0.5.4
37 paginate==0.5.4
38 paginate-sqlalchemy==0.2.0
38 paginate-sqlalchemy==0.2.0
39 pyelasticsearch==1.4
39 pyelasticsearch==1.4
40 six==1.9.0
40 six==1.9.0
41 mock==1.0.1
41 mock==1.0.1
42 itsdangerous==1.1.0
42 itsdangerous==1.1.0
43 camplight==0.9.6
43 camplight==0.9.6
44 jira==1.0.7
44 jira==1.0.7
45 python-dateutil==2.5.3
45 python-dateutil==2.5.3
46 authomatic==0.1.0.post1
46 authomatic==0.1.0.post1
47 cryptography==2.6.1
47 cryptography==2.6.1
48 webassets==0.11.1
48 webassets==0.11.1
49
49
@@ -1,660 +1,662 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import bisect
17 import bisect
18 import collections
18 import collections
19 import math
19 import math
20 from datetime import datetime, timedelta
20 from datetime import datetime, timedelta
21
21
22 import sqlalchemy as sa
22 import sqlalchemy as sa
23 import pyelasticsearch
23 import pyelasticsearch
24
24
25 from celery.utils.log import get_task_logger
25 from celery.utils.log import get_task_logger
26 from zope.sqlalchemy import mark_changed
26 from zope.sqlalchemy import mark_changed
27 from pyramid.threadlocal import get_current_request, get_current_registry
27 from pyramid.threadlocal import get_current_request, get_current_registry
28 from ziggurat_foundations.models.services.resource import ResourceService
29
28 from appenlight.celery import celery
30 from appenlight.celery import celery
29 from appenlight.models.report_group import ReportGroup
31 from appenlight.models.report_group import ReportGroup
30 from appenlight.models import DBSession, Datastores
32 from appenlight.models import DBSession, Datastores
31 from appenlight.models.report import Report
33 from appenlight.models.report import Report
32 from appenlight.models.log import Log
34 from appenlight.models.log import Log
33 from appenlight.models.metric import Metric
35 from appenlight.models.metric import Metric
34 from appenlight.models.event import Event
36 from appenlight.models.event import Event
35
37
36 from appenlight.models.services.application import ApplicationService
38 from appenlight.models.services.application import ApplicationService
37 from appenlight.models.services.event import EventService
39 from appenlight.models.services.event import EventService
38 from appenlight.models.services.log import LogService
40 from appenlight.models.services.log import LogService
39 from appenlight.models.services.report import ReportService
41 from appenlight.models.services.report import ReportService
40 from appenlight.models.services.report_group import ReportGroupService
42 from appenlight.models.services.report_group import ReportGroupService
41 from appenlight.models.services.user import UserService
43 from appenlight.models.services.user import UserService
42 from appenlight.models.tag import Tag
44 from appenlight.models.tag import Tag
43 from appenlight.lib import print_traceback
45 from appenlight.lib import print_traceback
44 from appenlight.lib.utils import parse_proto, in_batches
46 from appenlight.lib.utils import parse_proto, in_batches
45 from appenlight.lib.ext_json import json
47 from appenlight.lib.ext_json import json
46 from appenlight.lib.redis_keys import REDIS_KEYS
48 from appenlight.lib.redis_keys import REDIS_KEYS
47 from appenlight.lib.enums import ReportType
49 from appenlight.lib.enums import ReportType
48
50
49 log = get_task_logger(__name__)
51 log = get_task_logger(__name__)
50
52
51 sample_boundries = list(range(100, 1000, 100)) + \
53 sample_boundries = list(range(100, 1000, 100)) + \
52 list(range(1000, 10000, 1000)) + \
54 list(range(1000, 10000, 1000)) + \
53 list(range(10000, 100000, 5000))
55 list(range(10000, 100000, 5000))
54
56
55
57
56 def pick_sample(total_occurences, report_type=None):
58 def pick_sample(total_occurences, report_type=None):
57 every = 1.0
59 every = 1.0
58 position = bisect.bisect_left(sample_boundries, total_occurences)
60 position = bisect.bisect_left(sample_boundries, total_occurences)
59 if position > 0:
61 if position > 0:
60 if report_type == ReportType.not_found:
62 if report_type == ReportType.not_found:
61 divide = 10.0
63 divide = 10.0
62 else:
64 else:
63 divide = 100.0
65 divide = 100.0
64 every = sample_boundries[position - 1] / divide
66 every = sample_boundries[position - 1] / divide
65 return total_occurences % every == 0
67 return total_occurences % every == 0
66
68
67
69
68 @celery.task(queue="default", default_retry_delay=1, max_retries=2)
70 @celery.task(queue="default", default_retry_delay=1, max_retries=2)
69 def test_exception_task():
71 def test_exception_task():
70 log.error('test celery log', extra={'location': 'celery'})
72 log.error('test celery log', extra={'location': 'celery'})
71 log.warning('test celery log', extra={'location': 'celery'})
73 log.warning('test celery log', extra={'location': 'celery'})
72 raise Exception('Celery exception test')
74 raise Exception('Celery exception test')
73
75
74
76
75 @celery.task(queue="default", default_retry_delay=1, max_retries=2)
77 @celery.task(queue="default", default_retry_delay=1, max_retries=2)
76 def test_retry_exception_task():
78 def test_retry_exception_task():
77 try:
79 try:
78 import time
80 import time
79
81
80 time.sleep(1.3)
82 time.sleep(1.3)
81 log.error('test retry celery log', extra={'location': 'celery'})
83 log.error('test retry celery log', extra={'location': 'celery'})
82 log.warning('test retry celery log', extra={'location': 'celery'})
84 log.warning('test retry celery log', extra={'location': 'celery'})
83 raise Exception('Celery exception test')
85 raise Exception('Celery exception test')
84 except Exception as exc:
86 except Exception as exc:
85 test_retry_exception_task.retry(exc=exc)
87 test_retry_exception_task.retry(exc=exc)
86
88
87
89
88 @celery.task(queue="reports", default_retry_delay=600, max_retries=144)
90 @celery.task(queue="reports", default_retry_delay=600, max_retries=144)
89 def add_reports(resource_id, request_params, dataset, **kwargs):
91 def add_reports(resource_id, request_params, dataset, **kwargs):
90 proto_version = parse_proto(request_params.get('protocol_version', ''))
92 proto_version = parse_proto(request_params.get('protocol_version', ''))
91 current_time = datetime.utcnow().replace(second=0, microsecond=0)
93 current_time = datetime.utcnow().replace(second=0, microsecond=0)
92 try:
94 try:
93 # we will store solr docs here for single insert
95 # we will store solr docs here for single insert
94 es_report_docs = {}
96 es_report_docs = {}
95 es_report_group_docs = {}
97 es_report_group_docs = {}
96 resource = ApplicationService.by_id(resource_id)
98 resource = ApplicationService.by_id(resource_id)
97
99
98 tags = []
100 tags = []
99 es_slow_calls_docs = {}
101 es_slow_calls_docs = {}
100 es_reports_stats_rows = {}
102 es_reports_stats_rows = {}
101 for report_data in dataset:
103 for report_data in dataset:
102 # build report details for later
104 # build report details for later
103 added_details = 0
105 added_details = 0
104 report = Report()
106 report = Report()
105 report.set_data(report_data, resource, proto_version)
107 report.set_data(report_data, resource, proto_version)
106 report._skip_ft_index = True
108 report._skip_ft_index = True
107
109
108 # find latest group in this months partition
110 # find latest group in this months partition
109 report_group = ReportGroupService.by_hash_and_resource(
111 report_group = ReportGroupService.by_hash_and_resource(
110 report.resource_id,
112 report.resource_id,
111 report.grouping_hash,
113 report.grouping_hash,
112 since_when=datetime.utcnow().date().replace(day=1)
114 since_when=datetime.utcnow().date().replace(day=1)
113 )
115 )
114 occurences = report_data.get('occurences', 1)
116 occurences = report_data.get('occurences', 1)
115 if not report_group:
117 if not report_group:
116 # total reports will be +1 moment later
118 # total reports will be +1 moment later
117 report_group = ReportGroup(grouping_hash=report.grouping_hash,
119 report_group = ReportGroup(grouping_hash=report.grouping_hash,
118 occurences=0, total_reports=0,
120 occurences=0, total_reports=0,
119 last_report=0,
121 last_report=0,
120 priority=report.priority,
122 priority=report.priority,
121 error=report.error,
123 error=report.error,
122 first_timestamp=report.start_time)
124 first_timestamp=report.start_time)
123 report_group._skip_ft_index = True
125 report_group._skip_ft_index = True
124 report_group.report_type = report.report_type
126 report_group.report_type = report.report_type
125 report.report_group_time = report_group.first_timestamp
127 report.report_group_time = report_group.first_timestamp
126 add_sample = pick_sample(report_group.occurences,
128 add_sample = pick_sample(report_group.occurences,
127 report_type=report_group.report_type)
129 report_type=report_group.report_type)
128 if add_sample:
130 if add_sample:
129 resource.report_groups.append(report_group)
131 resource.report_groups.append(report_group)
130 report_group.reports.append(report)
132 report_group.reports.append(report)
131 added_details += 1
133 added_details += 1
132 DBSession.flush()
134 DBSession.flush()
133 if report.partition_id not in es_report_docs:
135 if report.partition_id not in es_report_docs:
134 es_report_docs[report.partition_id] = []
136 es_report_docs[report.partition_id] = []
135 es_report_docs[report.partition_id].append(report.es_doc())
137 es_report_docs[report.partition_id].append(report.es_doc())
136 tags.extend(list(report.tags.items()))
138 tags.extend(list(report.tags.items()))
137 slow_calls = report.add_slow_calls(report_data, report_group)
139 slow_calls = report.add_slow_calls(report_data, report_group)
138 DBSession.flush()
140 DBSession.flush()
139 for s_call in slow_calls:
141 for s_call in slow_calls:
140 if s_call.partition_id not in es_slow_calls_docs:
142 if s_call.partition_id not in es_slow_calls_docs:
141 es_slow_calls_docs[s_call.partition_id] = []
143 es_slow_calls_docs[s_call.partition_id] = []
142 es_slow_calls_docs[s_call.partition_id].append(
144 es_slow_calls_docs[s_call.partition_id].append(
143 s_call.es_doc())
145 s_call.es_doc())
144 # try generating new stat rows if needed
146 # try generating new stat rows if needed
145 else:
147 else:
146 # required for postprocessing to not fail later
148 # required for postprocessing to not fail later
147 report.report_group = report_group
149 report.report_group = report_group
148
150
149 stat_row = ReportService.generate_stat_rows(
151 stat_row = ReportService.generate_stat_rows(
150 report, resource, report_group)
152 report, resource, report_group)
151 if stat_row.partition_id not in es_reports_stats_rows:
153 if stat_row.partition_id not in es_reports_stats_rows:
152 es_reports_stats_rows[stat_row.partition_id] = []
154 es_reports_stats_rows[stat_row.partition_id] = []
153 es_reports_stats_rows[stat_row.partition_id].append(
155 es_reports_stats_rows[stat_row.partition_id].append(
154 stat_row.es_doc())
156 stat_row.es_doc())
155
157
156 # see if we should mark 10th occurence of report
158 # see if we should mark 10th occurence of report
157 last_occurences_10 = int(math.floor(report_group.occurences / 10))
159 last_occurences_10 = int(math.floor(report_group.occurences / 10))
158 curr_occurences_10 = int(math.floor(
160 curr_occurences_10 = int(math.floor(
159 (report_group.occurences + report.occurences) / 10))
161 (report_group.occurences + report.occurences) / 10))
160 last_occurences_100 = int(
162 last_occurences_100 = int(
161 math.floor(report_group.occurences / 100))
163 math.floor(report_group.occurences / 100))
162 curr_occurences_100 = int(math.floor(
164 curr_occurences_100 = int(math.floor(
163 (report_group.occurences + report.occurences) / 100))
165 (report_group.occurences + report.occurences) / 100))
164 notify_occurences_10 = last_occurences_10 != curr_occurences_10
166 notify_occurences_10 = last_occurences_10 != curr_occurences_10
165 notify_occurences_100 = last_occurences_100 != curr_occurences_100
167 notify_occurences_100 = last_occurences_100 != curr_occurences_100
166 report_group.occurences = ReportGroup.occurences + occurences
168 report_group.occurences = ReportGroup.occurences + occurences
167 report_group.last_timestamp = report.start_time
169 report_group.last_timestamp = report.start_time
168 report_group.summed_duration = ReportGroup.summed_duration + report.duration
170 report_group.summed_duration = ReportGroup.summed_duration + report.duration
169 summed_duration = ReportGroup.summed_duration + report.duration
171 summed_duration = ReportGroup.summed_duration + report.duration
170 summed_occurences = ReportGroup.occurences + occurences
172 summed_occurences = ReportGroup.occurences + occurences
171 report_group.average_duration = summed_duration / summed_occurences
173 report_group.average_duration = summed_duration / summed_occurences
172 report_group.run_postprocessing(report)
174 report_group.run_postprocessing(report)
173 if added_details:
175 if added_details:
174 report_group.total_reports = ReportGroup.total_reports + 1
176 report_group.total_reports = ReportGroup.total_reports + 1
175 report_group.last_report = report.id
177 report_group.last_report = report.id
176 report_group.set_notification_info(notify_10=notify_occurences_10,
178 report_group.set_notification_info(notify_10=notify_occurences_10,
177 notify_100=notify_occurences_100)
179 notify_100=notify_occurences_100)
178 DBSession.flush()
180 DBSession.flush()
179 report_group.get_report().notify_channel(report_group)
181 report_group.get_report().notify_channel(report_group)
180 if report_group.partition_id not in es_report_group_docs:
182 if report_group.partition_id not in es_report_group_docs:
181 es_report_group_docs[report_group.partition_id] = []
183 es_report_group_docs[report_group.partition_id] = []
182 es_report_group_docs[report_group.partition_id].append(
184 es_report_group_docs[report_group.partition_id].append(
183 report_group.es_doc())
185 report_group.es_doc())
184
186
185 action = 'REPORT'
187 action = 'REPORT'
186 log_msg = '%s: %s %s, client: %s, proto: %s' % (
188 log_msg = '%s: %s %s, client: %s, proto: %s' % (
187 action,
189 action,
188 report_data.get('http_status', 'unknown'),
190 report_data.get('http_status', 'unknown'),
189 str(resource),
191 str(resource),
190 report_data.get('client'),
192 report_data.get('client'),
191 proto_version)
193 proto_version)
192 log.info(log_msg)
194 log.info(log_msg)
193 total_reports = len(dataset)
195 total_reports = len(dataset)
194 redis_pipeline = Datastores.redis.pipeline(transaction=False)
196 redis_pipeline = Datastores.redis.pipeline(transaction=False)
195 key = REDIS_KEYS['counters']['reports_per_minute'].format(current_time)
197 key = REDIS_KEYS['counters']['reports_per_minute'].format(current_time)
196 redis_pipeline.incr(key, total_reports)
198 redis_pipeline.incr(key, total_reports)
197 redis_pipeline.expire(key, 3600 * 24)
199 redis_pipeline.expire(key, 3600 * 24)
198 key = REDIS_KEYS['counters']['events_per_minute_per_user'].format(
200 key = REDIS_KEYS['counters']['events_per_minute_per_user'].format(
199 resource.owner_user_id, current_time)
201 resource.owner_user_id, current_time)
200 redis_pipeline.incr(key, total_reports)
202 redis_pipeline.incr(key, total_reports)
201 redis_pipeline.expire(key, 3600)
203 redis_pipeline.expire(key, 3600)
202 key = REDIS_KEYS['counters']['reports_per_hour_per_app'].format(
204 key = REDIS_KEYS['counters']['reports_per_hour_per_app'].format(
203 resource_id, current_time.replace(minute=0))
205 resource_id, current_time.replace(minute=0))
204 redis_pipeline.incr(key, total_reports)
206 redis_pipeline.incr(key, total_reports)
205 redis_pipeline.expire(key, 3600 * 24 * 7)
207 redis_pipeline.expire(key, 3600 * 24 * 7)
206 redis_pipeline.sadd(
208 redis_pipeline.sadd(
207 REDIS_KEYS['apps_that_got_new_data_per_hour'].format(
209 REDIS_KEYS['apps_that_got_new_data_per_hour'].format(
208 current_time.replace(minute=0)), resource_id)
210 current_time.replace(minute=0)), resource_id)
209 redis_pipeline.execute()
211 redis_pipeline.execute()
210
212
211 add_reports_es(es_report_group_docs, es_report_docs)
213 add_reports_es(es_report_group_docs, es_report_docs)
212 add_reports_slow_calls_es(es_slow_calls_docs)
214 add_reports_slow_calls_es(es_slow_calls_docs)
213 add_reports_stats_rows_es(es_reports_stats_rows)
215 add_reports_stats_rows_es(es_reports_stats_rows)
214 return True
216 return True
215 except Exception as exc:
217 except Exception as exc:
216 print_traceback(log)
218 print_traceback(log)
217 add_reports.retry(exc=exc)
219 add_reports.retry(exc=exc)
218
220
219
221
220 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
222 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
221 def add_reports_es(report_group_docs, report_docs):
223 def add_reports_es(report_group_docs, report_docs):
222 for k, v in report_group_docs.items():
224 for k, v in report_group_docs.items():
223 Datastores.es.bulk_index(k, 'report_group', v, id_field="_id")
225 Datastores.es.bulk_index(k, 'report_group', v, id_field="_id")
224 for k, v in report_docs.items():
226 for k, v in report_docs.items():
225 Datastores.es.bulk_index(k, 'report', v, id_field="_id",
227 Datastores.es.bulk_index(k, 'report', v, id_field="_id",
226 parent_field='_parent')
228 parent_field='_parent')
227
229
228
230
229 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
231 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
230 def add_reports_slow_calls_es(es_docs):
232 def add_reports_slow_calls_es(es_docs):
231 for k, v in es_docs.items():
233 for k, v in es_docs.items():
232 Datastores.es.bulk_index(k, 'log', v)
234 Datastores.es.bulk_index(k, 'log', v)
233
235
234
236
235 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
237 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
236 def add_reports_stats_rows_es(es_docs):
238 def add_reports_stats_rows_es(es_docs):
237 for k, v in es_docs.items():
239 for k, v in es_docs.items():
238 Datastores.es.bulk_index(k, 'log', v)
240 Datastores.es.bulk_index(k, 'log', v)
239
241
240
242
241 @celery.task(queue="logs", default_retry_delay=600, max_retries=144)
243 @celery.task(queue="logs", default_retry_delay=600, max_retries=144)
242 def add_logs(resource_id, request_params, dataset, **kwargs):
244 def add_logs(resource_id, request_params, dataset, **kwargs):
243 proto_version = request_params.get('protocol_version')
245 proto_version = request_params.get('protocol_version')
244 current_time = datetime.utcnow().replace(second=0, microsecond=0)
246 current_time = datetime.utcnow().replace(second=0, microsecond=0)
245
247
246 try:
248 try:
247 es_docs = collections.defaultdict(list)
249 es_docs = collections.defaultdict(list)
248 resource = ApplicationService.by_id_cached()(resource_id)
250 resource = ApplicationService.by_id_cached()(resource_id)
249 resource = DBSession.merge(resource, load=False)
251 resource = DBSession.merge(resource, load=False)
250 ns_pairs = []
252 ns_pairs = []
251 for entry in dataset:
253 for entry in dataset:
252 # gather pk and ns so we can remove older versions of row later
254 # gather pk and ns so we can remove older versions of row later
253 if entry['primary_key'] is not None:
255 if entry['primary_key'] is not None:
254 ns_pairs.append({"pk": entry['primary_key'],
256 ns_pairs.append({"pk": entry['primary_key'],
255 "ns": entry['namespace']})
257 "ns": entry['namespace']})
256 log_entry = Log()
258 log_entry = Log()
257 log_entry.set_data(entry, resource=resource)
259 log_entry.set_data(entry, resource=resource)
258 log_entry._skip_ft_index = True
260 log_entry._skip_ft_index = True
259 resource.logs.append(log_entry)
261 resource.logs.append(log_entry)
260 DBSession.flush()
262 DBSession.flush()
261 # insert non pk rows first
263 # insert non pk rows first
262 if entry['primary_key'] is None:
264 if entry['primary_key'] is None:
263 es_docs[log_entry.partition_id].append(log_entry.es_doc())
265 es_docs[log_entry.partition_id].append(log_entry.es_doc())
264
266
265 # 2nd pass to delete all log entries from db foe same pk/ns pair
267 # 2nd pass to delete all log entries from db foe same pk/ns pair
266 if ns_pairs:
268 if ns_pairs:
267 ids_to_delete = []
269 ids_to_delete = []
268 es_docs = collections.defaultdict(list)
270 es_docs = collections.defaultdict(list)
269 es_docs_to_delete = collections.defaultdict(list)
271 es_docs_to_delete = collections.defaultdict(list)
270 found_pkey_logs = LogService.query_by_primary_key_and_namespace(
272 found_pkey_logs = LogService.query_by_primary_key_and_namespace(
271 list_of_pairs=ns_pairs)
273 list_of_pairs=ns_pairs)
272 log_dict = {}
274 log_dict = {}
273 for log_entry in found_pkey_logs:
275 for log_entry in found_pkey_logs:
274 log_key = (log_entry.primary_key, log_entry.namespace)
276 log_key = (log_entry.primary_key, log_entry.namespace)
275 if log_key not in log_dict:
277 if log_key not in log_dict:
276 log_dict[log_key] = []
278 log_dict[log_key] = []
277 log_dict[log_key].append(log_entry)
279 log_dict[log_key].append(log_entry)
278
280
279 for ns, entry_list in log_dict.items():
281 for ns, entry_list in log_dict.items():
280 entry_list = sorted(entry_list, key=lambda x: x.timestamp)
282 entry_list = sorted(entry_list, key=lambda x: x.timestamp)
281 # newest row needs to be indexed in es
283 # newest row needs to be indexed in es
282 log_entry = entry_list[-1]
284 log_entry = entry_list[-1]
283 # delete everything from pg and ES, leave the last row in pg
285 # delete everything from pg and ES, leave the last row in pg
284 for e in entry_list[:-1]:
286 for e in entry_list[:-1]:
285 ids_to_delete.append(e.log_id)
287 ids_to_delete.append(e.log_id)
286 es_docs_to_delete[e.partition_id].append(e.delete_hash)
288 es_docs_to_delete[e.partition_id].append(e.delete_hash)
287
289
288 es_docs_to_delete[log_entry.partition_id].append(
290 es_docs_to_delete[log_entry.partition_id].append(
289 log_entry.delete_hash)
291 log_entry.delete_hash)
290
292
291 es_docs[log_entry.partition_id].append(log_entry.es_doc())
293 es_docs[log_entry.partition_id].append(log_entry.es_doc())
292
294
293 if ids_to_delete:
295 if ids_to_delete:
294 query = DBSession.query(Log).filter(
296 query = DBSession.query(Log).filter(
295 Log.log_id.in_(ids_to_delete))
297 Log.log_id.in_(ids_to_delete))
296 query.delete(synchronize_session=False)
298 query.delete(synchronize_session=False)
297 if es_docs_to_delete:
299 if es_docs_to_delete:
298 # batch this to avoid problems with default ES bulk limits
300 # batch this to avoid problems with default ES bulk limits
299 for es_index in es_docs_to_delete.keys():
301 for es_index in es_docs_to_delete.keys():
300 for batch in in_batches(es_docs_to_delete[es_index], 20):
302 for batch in in_batches(es_docs_to_delete[es_index], 20):
301 query = {'terms': {'delete_hash': batch}}
303 query = {'terms': {'delete_hash': batch}}
302
304
303 try:
305 try:
304 Datastores.es.delete_by_query(
306 Datastores.es.delete_by_query(
305 es_index, 'log', query)
307 es_index, 'log', query)
306 except pyelasticsearch.ElasticHttpNotFoundError as exc:
308 except pyelasticsearch.ElasticHttpNotFoundError as exc:
307 msg = 'skipping index {}'.format(es_index)
309 msg = 'skipping index {}'.format(es_index)
308 log.info(msg)
310 log.info(msg)
309
311
310 total_logs = len(dataset)
312 total_logs = len(dataset)
311
313
312 log_msg = 'LOG_NEW: %s, entries: %s, proto:%s' % (
314 log_msg = 'LOG_NEW: %s, entries: %s, proto:%s' % (
313 str(resource),
315 str(resource),
314 total_logs,
316 total_logs,
315 proto_version)
317 proto_version)
316 log.info(log_msg)
318 log.info(log_msg)
317 # mark_changed(session)
319 # mark_changed(session)
318 redis_pipeline = Datastores.redis.pipeline(transaction=False)
320 redis_pipeline = Datastores.redis.pipeline(transaction=False)
319 key = REDIS_KEYS['counters']['logs_per_minute'].format(current_time)
321 key = REDIS_KEYS['counters']['logs_per_minute'].format(current_time)
320 redis_pipeline.incr(key, total_logs)
322 redis_pipeline.incr(key, total_logs)
321 redis_pipeline.expire(key, 3600 * 24)
323 redis_pipeline.expire(key, 3600 * 24)
322 key = REDIS_KEYS['counters']['events_per_minute_per_user'].format(
324 key = REDIS_KEYS['counters']['events_per_minute_per_user'].format(
323 resource.owner_user_id, current_time)
325 resource.owner_user_id, current_time)
324 redis_pipeline.incr(key, total_logs)
326 redis_pipeline.incr(key, total_logs)
325 redis_pipeline.expire(key, 3600)
327 redis_pipeline.expire(key, 3600)
326 key = REDIS_KEYS['counters']['logs_per_hour_per_app'].format(
328 key = REDIS_KEYS['counters']['logs_per_hour_per_app'].format(
327 resource_id, current_time.replace(minute=0))
329 resource_id, current_time.replace(minute=0))
328 redis_pipeline.incr(key, total_logs)
330 redis_pipeline.incr(key, total_logs)
329 redis_pipeline.expire(key, 3600 * 24 * 7)
331 redis_pipeline.expire(key, 3600 * 24 * 7)
330 redis_pipeline.sadd(
332 redis_pipeline.sadd(
331 REDIS_KEYS['apps_that_got_new_data_per_hour'].format(
333 REDIS_KEYS['apps_that_got_new_data_per_hour'].format(
332 current_time.replace(minute=0)), resource_id)
334 current_time.replace(minute=0)), resource_id)
333 redis_pipeline.execute()
335 redis_pipeline.execute()
334 add_logs_es(es_docs)
336 add_logs_es(es_docs)
335 return True
337 return True
336 except Exception as exc:
338 except Exception as exc:
337 print_traceback(log)
339 print_traceback(log)
338 add_logs.retry(exc=exc)
340 add_logs.retry(exc=exc)
339
341
340
342
341 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
343 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
342 def add_logs_es(es_docs):
344 def add_logs_es(es_docs):
343 for k, v in es_docs.items():
345 for k, v in es_docs.items():
344 Datastores.es.bulk_index(k, 'log', v)
346 Datastores.es.bulk_index(k, 'log', v)
345
347
346
348
347 @celery.task(queue="metrics", default_retry_delay=600, max_retries=144)
349 @celery.task(queue="metrics", default_retry_delay=600, max_retries=144)
348 def add_metrics(resource_id, request_params, dataset, proto_version):
350 def add_metrics(resource_id, request_params, dataset, proto_version):
349 current_time = datetime.utcnow().replace(second=0, microsecond=0)
351 current_time = datetime.utcnow().replace(second=0, microsecond=0)
350 try:
352 try:
351 resource = ApplicationService.by_id_cached()(resource_id)
353 resource = ApplicationService.by_id_cached()(resource_id)
352 resource = DBSession.merge(resource, load=False)
354 resource = DBSession.merge(resource, load=False)
353 es_docs = []
355 es_docs = []
354 rows = []
356 rows = []
355 for metric in dataset:
357 for metric in dataset:
356 tags = dict(metric['tags'])
358 tags = dict(metric['tags'])
357 server_n = tags.get('server_name', metric['server_name']).lower()
359 server_n = tags.get('server_name', metric['server_name']).lower()
358 tags['server_name'] = server_n or 'unknown'
360 tags['server_name'] = server_n or 'unknown'
359 new_metric = Metric(
361 new_metric = Metric(
360 timestamp=metric['timestamp'],
362 timestamp=metric['timestamp'],
361 resource_id=resource.resource_id,
363 resource_id=resource.resource_id,
362 namespace=metric['namespace'],
364 namespace=metric['namespace'],
363 tags=tags)
365 tags=tags)
364 rows.append(new_metric)
366 rows.append(new_metric)
365 es_docs.append(new_metric.es_doc())
367 es_docs.append(new_metric.es_doc())
366 session = DBSession()
368 session = DBSession()
367 session.bulk_save_objects(rows)
369 session.bulk_save_objects(rows)
368 session.flush()
370 session.flush()
369
371
370 action = 'METRICS'
372 action = 'METRICS'
371 metrics_msg = '%s: %s, metrics: %s, proto:%s' % (
373 metrics_msg = '%s: %s, metrics: %s, proto:%s' % (
372 action,
374 action,
373 str(resource),
375 str(resource),
374 len(dataset),
376 len(dataset),
375 proto_version
377 proto_version
376 )
378 )
377 log.info(metrics_msg)
379 log.info(metrics_msg)
378
380
379 mark_changed(session)
381 mark_changed(session)
380 redis_pipeline = Datastores.redis.pipeline(transaction=False)
382 redis_pipeline = Datastores.redis.pipeline(transaction=False)
381 key = REDIS_KEYS['counters']['metrics_per_minute'].format(current_time)
383 key = REDIS_KEYS['counters']['metrics_per_minute'].format(current_time)
382 redis_pipeline.incr(key, len(rows))
384 redis_pipeline.incr(key, len(rows))
383 redis_pipeline.expire(key, 3600 * 24)
385 redis_pipeline.expire(key, 3600 * 24)
384 key = REDIS_KEYS['counters']['events_per_minute_per_user'].format(
386 key = REDIS_KEYS['counters']['events_per_minute_per_user'].format(
385 resource.owner_user_id, current_time)
387 resource.owner_user_id, current_time)
386 redis_pipeline.incr(key, len(rows))
388 redis_pipeline.incr(key, len(rows))
387 redis_pipeline.expire(key, 3600)
389 redis_pipeline.expire(key, 3600)
388 key = REDIS_KEYS['counters']['metrics_per_hour_per_app'].format(
390 key = REDIS_KEYS['counters']['metrics_per_hour_per_app'].format(
389 resource_id, current_time.replace(minute=0))
391 resource_id, current_time.replace(minute=0))
390 redis_pipeline.incr(key, len(rows))
392 redis_pipeline.incr(key, len(rows))
391 redis_pipeline.expire(key, 3600 * 24 * 7)
393 redis_pipeline.expire(key, 3600 * 24 * 7)
392 redis_pipeline.sadd(
394 redis_pipeline.sadd(
393 REDIS_KEYS['apps_that_got_new_data_per_hour'].format(
395 REDIS_KEYS['apps_that_got_new_data_per_hour'].format(
394 current_time.replace(minute=0)), resource_id)
396 current_time.replace(minute=0)), resource_id)
395 redis_pipeline.execute()
397 redis_pipeline.execute()
396 add_metrics_es(es_docs)
398 add_metrics_es(es_docs)
397 return True
399 return True
398 except Exception as exc:
400 except Exception as exc:
399 print_traceback(log)
401 print_traceback(log)
400 add_metrics.retry(exc=exc)
402 add_metrics.retry(exc=exc)
401
403
402
404
403 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
405 @celery.task(queue="es", default_retry_delay=600, max_retries=144)
404 def add_metrics_es(es_docs):
406 def add_metrics_es(es_docs):
405 for doc in es_docs:
407 for doc in es_docs:
406 partition = 'rcae_m_%s' % doc['timestamp'].strftime('%Y_%m_%d')
408 partition = 'rcae_m_%s' % doc['timestamp'].strftime('%Y_%m_%d')
407 Datastores.es.index(partition, 'log', doc)
409 Datastores.es.index(partition, 'log', doc)
408
410
409
411
410 @celery.task(queue="default", default_retry_delay=5, max_retries=2)
412 @celery.task(queue="default", default_retry_delay=5, max_retries=2)
411 def check_user_report_notifications(resource_id):
413 def check_user_report_notifications(resource_id):
412 since_when = datetime.utcnow()
414 since_when = datetime.utcnow()
413 try:
415 try:
414 request = get_current_request()
416 request = get_current_request()
415 application = ApplicationService.by_id(resource_id)
417 application = ApplicationService.by_id(resource_id)
416 if not application:
418 if not application:
417 return
419 return
418 error_key = REDIS_KEYS['reports_to_notify_per_type_per_app'].format(
420 error_key = REDIS_KEYS['reports_to_notify_per_type_per_app'].format(
419 ReportType.error, resource_id)
421 ReportType.error, resource_id)
420 slow_key = REDIS_KEYS['reports_to_notify_per_type_per_app'].format(
422 slow_key = REDIS_KEYS['reports_to_notify_per_type_per_app'].format(
421 ReportType.slow, resource_id)
423 ReportType.slow, resource_id)
422 error_group_ids = Datastores.redis.smembers(error_key)
424 error_group_ids = Datastores.redis.smembers(error_key)
423 slow_group_ids = Datastores.redis.smembers(slow_key)
425 slow_group_ids = Datastores.redis.smembers(slow_key)
424 Datastores.redis.delete(error_key)
426 Datastores.redis.delete(error_key)
425 Datastores.redis.delete(slow_key)
427 Datastores.redis.delete(slow_key)
426 err_gids = [int(g_id) for g_id in error_group_ids]
428 err_gids = [int(g_id) for g_id in error_group_ids]
427 slow_gids = [int(g_id) for g_id in list(slow_group_ids)]
429 slow_gids = [int(g_id) for g_id in list(slow_group_ids)]
428 group_ids = err_gids + slow_gids
430 group_ids = err_gids + slow_gids
429 occurence_dict = {}
431 occurence_dict = {}
430 for g_id in group_ids:
432 for g_id in group_ids:
431 key = REDIS_KEYS['counters']['report_group_occurences'].format(
433 key = REDIS_KEYS['counters']['report_group_occurences'].format(
432 g_id)
434 g_id)
433 val = Datastores.redis.get(key)
435 val = Datastores.redis.get(key)
434 Datastores.redis.delete(key)
436 Datastores.redis.delete(key)
435 if val:
437 if val:
436 occurence_dict[g_id] = int(val)
438 occurence_dict[g_id] = int(val)
437 else:
439 else:
438 occurence_dict[g_id] = 1
440 occurence_dict[g_id] = 1
439 report_groups = ReportGroupService.by_ids(group_ids)
441 report_groups = ReportGroupService.by_ids(group_ids)
440 report_groups.options(sa.orm.joinedload(ReportGroup.last_report_ref))
442 report_groups.options(sa.orm.joinedload(ReportGroup.last_report_ref))
441
443
442 ApplicationService.check_for_groups_alert(
444 ApplicationService.check_for_groups_alert(
443 application, 'alert', report_groups=report_groups,
445 application, 'alert', report_groups=report_groups,
444 occurence_dict=occurence_dict)
446 occurence_dict=occurence_dict)
445 users = set([p.user for p in application.users_for_perm('view')])
447 users = set([p.user for p in ResourceService.users_for_perm(application, 'view')])
446 report_groups = report_groups.all()
448 report_groups = report_groups.all()
447 for user in users:
449 for user in users:
448 UserService.report_notify(user, request, application,
450 UserService.report_notify(user, request, application,
449 report_groups=report_groups,
451 report_groups=report_groups,
450 occurence_dict=occurence_dict)
452 occurence_dict=occurence_dict)
451 for group in report_groups:
453 for group in report_groups:
452 # marks report_groups as notified
454 # marks report_groups as notified
453 if not group.notified:
455 if not group.notified:
454 group.notified = True
456 group.notified = True
455 except Exception as exc:
457 except Exception as exc:
456 print_traceback(log)
458 print_traceback(log)
457 raise
459 raise
458
460
459
461
460 @celery.task(queue="default", default_retry_delay=5, max_retries=2)
462 @celery.task(queue="default", default_retry_delay=5, max_retries=2)
461 def check_alerts(resource_id):
463 def check_alerts(resource_id):
462 since_when = datetime.utcnow()
464 since_when = datetime.utcnow()
463 try:
465 try:
464 request = get_current_request()
466 request = get_current_request()
465 application = ApplicationService.by_id(resource_id)
467 application = ApplicationService.by_id(resource_id)
466 if not application:
468 if not application:
467 return
469 return
468 error_key = REDIS_KEYS[
470 error_key = REDIS_KEYS[
469 'reports_to_notify_per_type_per_app_alerting'].format(
471 'reports_to_notify_per_type_per_app_alerting'].format(
470 ReportType.error, resource_id)
472 ReportType.error, resource_id)
471 slow_key = REDIS_KEYS[
473 slow_key = REDIS_KEYS[
472 'reports_to_notify_per_type_per_app_alerting'].format(
474 'reports_to_notify_per_type_per_app_alerting'].format(
473 ReportType.slow, resource_id)
475 ReportType.slow, resource_id)
474 error_group_ids = Datastores.redis.smembers(error_key)
476 error_group_ids = Datastores.redis.smembers(error_key)
475 slow_group_ids = Datastores.redis.smembers(slow_key)
477 slow_group_ids = Datastores.redis.smembers(slow_key)
476 Datastores.redis.delete(error_key)
478 Datastores.redis.delete(error_key)
477 Datastores.redis.delete(slow_key)
479 Datastores.redis.delete(slow_key)
478 err_gids = [int(g_id) for g_id in error_group_ids]
480 err_gids = [int(g_id) for g_id in error_group_ids]
479 slow_gids = [int(g_id) for g_id in list(slow_group_ids)]
481 slow_gids = [int(g_id) for g_id in list(slow_group_ids)]
480 group_ids = err_gids + slow_gids
482 group_ids = err_gids + slow_gids
481 occurence_dict = {}
483 occurence_dict = {}
482 for g_id in group_ids:
484 for g_id in group_ids:
483 key = REDIS_KEYS['counters'][
485 key = REDIS_KEYS['counters'][
484 'report_group_occurences_alerting'].format(
486 'report_group_occurences_alerting'].format(
485 g_id)
487 g_id)
486 val = Datastores.redis.get(key)
488 val = Datastores.redis.get(key)
487 Datastores.redis.delete(key)
489 Datastores.redis.delete(key)
488 if val:
490 if val:
489 occurence_dict[g_id] = int(val)
491 occurence_dict[g_id] = int(val)
490 else:
492 else:
491 occurence_dict[g_id] = 1
493 occurence_dict[g_id] = 1
492 report_groups = ReportGroupService.by_ids(group_ids)
494 report_groups = ReportGroupService.by_ids(group_ids)
493 report_groups.options(sa.orm.joinedload(ReportGroup.last_report_ref))
495 report_groups.options(sa.orm.joinedload(ReportGroup.last_report_ref))
494
496
495 ApplicationService.check_for_groups_alert(
497 ApplicationService.check_for_groups_alert(
496 application, 'alert', report_groups=report_groups,
498 application, 'alert', report_groups=report_groups,
497 occurence_dict=occurence_dict, since_when=since_when)
499 occurence_dict=occurence_dict, since_when=since_when)
498 except Exception as exc:
500 except Exception as exc:
499 print_traceback(log)
501 print_traceback(log)
500 raise
502 raise
501
503
502
504
503 @celery.task(queue="default", default_retry_delay=1, max_retries=2)
505 @celery.task(queue="default", default_retry_delay=1, max_retries=2)
504 def close_alerts():
506 def close_alerts():
505 log.warning('Checking alerts')
507 log.warning('Checking alerts')
506 since_when = datetime.utcnow()
508 since_when = datetime.utcnow()
507 try:
509 try:
508 event_types = [Event.types['error_report_alert'],
510 event_types = [Event.types['error_report_alert'],
509 Event.types['slow_report_alert'], ]
511 Event.types['slow_report_alert'], ]
510 statuses = [Event.statuses['active']]
512 statuses = [Event.statuses['active']]
511 # get events older than 5 min
513 # get events older than 5 min
512 events = EventService.by_type_and_status(
514 events = EventService.by_type_and_status(
513 event_types,
515 event_types,
514 statuses,
516 statuses,
515 older_than=(since_when - timedelta(minutes=5)))
517 older_than=(since_when - timedelta(minutes=5)))
516 for event in events:
518 for event in events:
517 # see if we can close them
519 # see if we can close them
518 event.validate_or_close(
520 event.validate_or_close(
519 since_when=(since_when - timedelta(minutes=1)))
521 since_when=(since_when - timedelta(minutes=1)))
520 except Exception as exc:
522 except Exception as exc:
521 print_traceback(log)
523 print_traceback(log)
522 raise
524 raise
523
525
524
526
525 @celery.task(queue="default", default_retry_delay=600, max_retries=144)
527 @celery.task(queue="default", default_retry_delay=600, max_retries=144)
526 def update_tag_counter(tag_name, tag_value, count):
528 def update_tag_counter(tag_name, tag_value, count):
527 try:
529 try:
528 query = DBSession.query(Tag).filter(Tag.name == tag_name).filter(
530 query = DBSession.query(Tag).filter(Tag.name == tag_name).filter(
529 sa.cast(Tag.value, sa.types.TEXT) == sa.cast(json.dumps(tag_value),
531 sa.cast(Tag.value, sa.types.TEXT) == sa.cast(json.dumps(tag_value),
530 sa.types.TEXT))
532 sa.types.TEXT))
531 query.update({'times_seen': Tag.times_seen + count,
533 query.update({'times_seen': Tag.times_seen + count,
532 'last_timestamp': datetime.utcnow()},
534 'last_timestamp': datetime.utcnow()},
533 synchronize_session=False)
535 synchronize_session=False)
534 session = DBSession()
536 session = DBSession()
535 mark_changed(session)
537 mark_changed(session)
536 return True
538 return True
537 except Exception as exc:
539 except Exception as exc:
538 print_traceback(log)
540 print_traceback(log)
539 update_tag_counter.retry(exc=exc)
541 update_tag_counter.retry(exc=exc)
540
542
541
543
542 @celery.task(queue="default")
544 @celery.task(queue="default")
543 def update_tag_counters():
545 def update_tag_counters():
544 """
546 """
545 Sets task to update counters for application tags
547 Sets task to update counters for application tags
546 """
548 """
547 tags = Datastores.redis.lrange(REDIS_KEYS['seen_tag_list'], 0, -1)
549 tags = Datastores.redis.lrange(REDIS_KEYS['seen_tag_list'], 0, -1)
548 Datastores.redis.delete(REDIS_KEYS['seen_tag_list'])
550 Datastores.redis.delete(REDIS_KEYS['seen_tag_list'])
549 c = collections.Counter(tags)
551 c = collections.Counter(tags)
550 for t_json, count in c.items():
552 for t_json, count in c.items():
551 tag_info = json.loads(t_json)
553 tag_info = json.loads(t_json)
552 update_tag_counter.delay(tag_info[0], tag_info[1], count)
554 update_tag_counter.delay(tag_info[0], tag_info[1], count)
553
555
554
556
555 @celery.task(queue="default")
557 @celery.task(queue="default")
556 def daily_digest():
558 def daily_digest():
557 """
559 """
558 Sends daily digest with top 50 error reports
560 Sends daily digest with top 50 error reports
559 """
561 """
560 request = get_current_request()
562 request = get_current_request()
561 apps = Datastores.redis.smembers(REDIS_KEYS['apps_that_had_reports'])
563 apps = Datastores.redis.smembers(REDIS_KEYS['apps_that_had_reports'])
562 Datastores.redis.delete(REDIS_KEYS['apps_that_had_reports'])
564 Datastores.redis.delete(REDIS_KEYS['apps_that_had_reports'])
563 since_when = datetime.utcnow() - timedelta(hours=8)
565 since_when = datetime.utcnow() - timedelta(hours=8)
564 log.warning('Generating daily digests')
566 log.warning('Generating daily digests')
565 for resource_id in apps:
567 for resource_id in apps:
566 resource_id = resource_id.decode('utf8')
568 resource_id = resource_id.decode('utf8')
567 end_date = datetime.utcnow().replace(microsecond=0, second=0)
569 end_date = datetime.utcnow().replace(microsecond=0, second=0)
568 filter_settings = {'resource': [resource_id],
570 filter_settings = {'resource': [resource_id],
569 'tags': [{'name': 'type',
571 'tags': [{'name': 'type',
570 'value': ['error'], 'op': None}],
572 'value': ['error'], 'op': None}],
571 'type': 'error', 'start_date': since_when,
573 'type': 'error', 'start_date': since_when,
572 'end_date': end_date}
574 'end_date': end_date}
573
575
574 reports = ReportGroupService.get_trending(
576 reports = ReportGroupService.get_trending(
575 request, filter_settings=filter_settings, limit=50)
577 request, filter_settings=filter_settings, limit=50)
576
578
577 application = ApplicationService.by_id(resource_id)
579 application = ApplicationService.by_id(resource_id)
578 if application:
580 if application:
579 users = set([p.user for p in application.users_for_perm('view')])
581 users = set([p.user for p in ResourceService.users_for_perm(application, 'view')])
580 for user in users:
582 for user in users:
581 user.send_digest(request, application, reports=reports,
583 user.send_digest(request, application, reports=reports,
582 since_when=since_when)
584 since_when=since_when)
583
585
584
586
585 @celery.task(queue="default")
587 @celery.task(queue="default")
586 def notifications_reports():
588 def notifications_reports():
587 """
589 """
588 Loop that checks redis for info and then issues new tasks to celery to
590 Loop that checks redis for info and then issues new tasks to celery to
589 issue notifications
591 issue notifications
590 """
592 """
591 apps = Datastores.redis.smembers(REDIS_KEYS['apps_that_had_reports'])
593 apps = Datastores.redis.smembers(REDIS_KEYS['apps_that_had_reports'])
592 Datastores.redis.delete(REDIS_KEYS['apps_that_had_reports'])
594 Datastores.redis.delete(REDIS_KEYS['apps_that_had_reports'])
593 for app in apps:
595 for app in apps:
594 log.warning('Notify for app: %s' % app)
596 log.warning('Notify for app: %s' % app)
595 check_user_report_notifications.delay(app.decode('utf8'))
597 check_user_report_notifications.delay(app.decode('utf8'))
596
598
597 @celery.task(queue="default")
599 @celery.task(queue="default")
598 def alerting_reports():
600 def alerting_reports():
599 """
601 """
600 Loop that checks redis for info and then issues new tasks to celery to
602 Loop that checks redis for info and then issues new tasks to celery to
601 perform the following:
603 perform the following:
602 - which applications should have new alerts opened
604 - which applications should have new alerts opened
603 """
605 """
604
606
605 apps = Datastores.redis.smembers(REDIS_KEYS['apps_that_had_reports_alerting'])
607 apps = Datastores.redis.smembers(REDIS_KEYS['apps_that_had_reports_alerting'])
606 Datastores.redis.delete(REDIS_KEYS['apps_that_had_reports_alerting'])
608 Datastores.redis.delete(REDIS_KEYS['apps_that_had_reports_alerting'])
607 for app in apps:
609 for app in apps:
608 log.warning('Notify for app: %s' % app)
610 log.warning('Notify for app: %s' % app)
609 check_alerts.delay(app.decode('utf8'))
611 check_alerts.delay(app.decode('utf8'))
610
612
611
613
612 @celery.task(queue="default", soft_time_limit=3600 * 4,
614 @celery.task(queue="default", soft_time_limit=3600 * 4,
613 hard_time_limit=3600 * 4, max_retries=144)
615 hard_time_limit=3600 * 4, max_retries=144)
614 def logs_cleanup(resource_id, filter_settings):
616 def logs_cleanup(resource_id, filter_settings):
615 request = get_current_request()
617 request = get_current_request()
616 request.tm.begin()
618 request.tm.begin()
617 es_query = {
619 es_query = {
618 "_source": False,
620 "_source": False,
619 "size": 5000,
621 "size": 5000,
620 "query": {
622 "query": {
621 "filtered": {
623 "filtered": {
622 "filter": {
624 "filter": {
623 "and": [{"term": {"resource_id": resource_id}}]
625 "and": [{"term": {"resource_id": resource_id}}]
624 }
626 }
625 }
627 }
626 }
628 }
627 }
629 }
628
630
629 query = DBSession.query(Log).filter(Log.resource_id == resource_id)
631 query = DBSession.query(Log).filter(Log.resource_id == resource_id)
630 if filter_settings['namespace']:
632 if filter_settings['namespace']:
631 query = query.filter(Log.namespace == filter_settings['namespace'][0])
633 query = query.filter(Log.namespace == filter_settings['namespace'][0])
632 es_query['query']['filtered']['filter']['and'].append(
634 es_query['query']['filtered']['filter']['and'].append(
633 {"term": {"namespace": filter_settings['namespace'][0]}}
635 {"term": {"namespace": filter_settings['namespace'][0]}}
634 )
636 )
635 query.delete(synchronize_session=False)
637 query.delete(synchronize_session=False)
636 request.tm.commit()
638 request.tm.commit()
637 result = request.es_conn.search(es_query, index='rcae_l_*',
639 result = request.es_conn.search(es_query, index='rcae_l_*',
638 doc_type='log', es_scroll='1m',
640 doc_type='log', es_scroll='1m',
639 es_search_type='scan')
641 es_search_type='scan')
640 scroll_id = result['_scroll_id']
642 scroll_id = result['_scroll_id']
641 while True:
643 while True:
642 log.warning('log_cleanup, app:{} ns:{} batch'.format(
644 log.warning('log_cleanup, app:{} ns:{} batch'.format(
643 resource_id,
645 resource_id,
644 filter_settings['namespace']
646 filter_settings['namespace']
645 ))
647 ))
646 es_docs_to_delete = []
648 es_docs_to_delete = []
647 result = request.es_conn.send_request(
649 result = request.es_conn.send_request(
648 'POST', ['_search', 'scroll'],
650 'POST', ['_search', 'scroll'],
649 body=scroll_id, query_params={"scroll": '1m'})
651 body=scroll_id, query_params={"scroll": '1m'})
650 scroll_id = result['_scroll_id']
652 scroll_id = result['_scroll_id']
651 if not result['hits']['hits']:
653 if not result['hits']['hits']:
652 break
654 break
653 for doc in result['hits']['hits']:
655 for doc in result['hits']['hits']:
654 es_docs_to_delete.append({"id": doc['_id'],
656 es_docs_to_delete.append({"id": doc['_id'],
655 "index": doc['_index']})
657 "index": doc['_index']})
656
658
657 for batch in in_batches(es_docs_to_delete, 10):
659 for batch in in_batches(es_docs_to_delete, 10):
658 Datastores.es.bulk([Datastores.es.delete_op(doc_type='log',
660 Datastores.es.bulk([Datastores.es.delete_op(doc_type='log',
659 **to_del)
661 **to_del)
660 for to_del in batch])
662 for to_del in batch])
@@ -1,896 +1,896 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import wtforms
17 import wtforms
18 import formencode
18 import formencode
19 import re
19 import re
20 import pyramid.threadlocal
20 import pyramid.threadlocal
21 import datetime
21 import datetime
22 import appenlight.lib.helpers as h
22 import appenlight.lib.helpers as h
23
23
24 from appenlight.models.user import User
24 from ziggurat_foundations.models.services.user import UserService
25 from appenlight.models.group import Group
25 from ziggurat_foundations.models.services.group import GroupService
26 from appenlight.models import DBSession
26 from appenlight.models import DBSession
27 from appenlight.models.alert_channel import AlertChannel
27 from appenlight.models.alert_channel import AlertChannel
28 from appenlight.models.integrations import IntegrationException
28 from appenlight.models.integrations import IntegrationException
29 from appenlight.models.integrations.campfire import CampfireIntegration
29 from appenlight.models.integrations.campfire import CampfireIntegration
30 from appenlight.models.integrations.bitbucket import BitbucketIntegration
30 from appenlight.models.integrations.bitbucket import BitbucketIntegration
31 from appenlight.models.integrations.github import GithubIntegration
31 from appenlight.models.integrations.github import GithubIntegration
32 from appenlight.models.integrations.flowdock import FlowdockIntegration
32 from appenlight.models.integrations.flowdock import FlowdockIntegration
33 from appenlight.models.integrations.hipchat import HipchatIntegration
33 from appenlight.models.integrations.hipchat import HipchatIntegration
34 from appenlight.models.integrations.jira import JiraClient
34 from appenlight.models.integrations.jira import JiraClient
35 from appenlight.models.integrations.slack import SlackIntegration
35 from appenlight.models.integrations.slack import SlackIntegration
36 from appenlight.lib.ext_json import json
36 from appenlight.lib.ext_json import json
37 from wtforms.ext.csrf.form import SecureForm
37 from wtforms.ext.csrf.form import SecureForm
38 from wtforms.compat import iteritems
38 from wtforms.compat import iteritems
39 from collections import defaultdict
39 from collections import defaultdict
40
40
41 _ = str
41 _ = str
42
42
43 strip_filter = lambda x: x.strip() if x else None
43 strip_filter = lambda x: x.strip() if x else None
44 uppercase_filter = lambda x: x.upper() if x else None
44 uppercase_filter = lambda x: x.upper() if x else None
45
45
46 FALSE_VALUES = ('false', '', False, None)
46 FALSE_VALUES = ('false', '', False, None)
47
47
48
48
49 class CSRFException(Exception):
49 class CSRFException(Exception):
50 pass
50 pass
51
51
52
52
53 class ReactorForm(SecureForm):
53 class ReactorForm(SecureForm):
54 def __init__(self, formdata=None, obj=None, prefix='', csrf_context=None,
54 def __init__(self, formdata=None, obj=None, prefix='', csrf_context=None,
55 **kwargs):
55 **kwargs):
56 super(ReactorForm, self).__init__(formdata=formdata, obj=obj,
56 super(ReactorForm, self).__init__(formdata=formdata, obj=obj,
57 prefix=prefix,
57 prefix=prefix,
58 csrf_context=csrf_context, **kwargs)
58 csrf_context=csrf_context, **kwargs)
59 self._csrf_context = csrf_context
59 self._csrf_context = csrf_context
60
60
61 def generate_csrf_token(self, csrf_context):
61 def generate_csrf_token(self, csrf_context):
62 return csrf_context.session.get_csrf_token()
62 return csrf_context.session.get_csrf_token()
63
63
64 def validate_csrf_token(self, field):
64 def validate_csrf_token(self, field):
65 request = self._csrf_context or pyramid.threadlocal.get_current_request()
65 request = self._csrf_context or pyramid.threadlocal.get_current_request()
66 is_from_auth_token = 'auth:auth_token' in request.effective_principals
66 is_from_auth_token = 'auth:auth_token' in request.effective_principals
67 if is_from_auth_token:
67 if is_from_auth_token:
68 return True
68 return True
69
69
70 if field.data != field.current_token:
70 if field.data != field.current_token:
71 # try to save the day by using token from angular
71 # try to save the day by using token from angular
72 if request.headers.get('X-XSRF-TOKEN') != field.current_token:
72 if request.headers.get('X-XSRF-TOKEN') != field.current_token:
73 raise CSRFException('Invalid CSRF token')
73 raise CSRFException('Invalid CSRF token')
74
74
75 @property
75 @property
76 def errors_dict(self):
76 def errors_dict(self):
77 r_dict = defaultdict(list)
77 r_dict = defaultdict(list)
78 for k, errors in self.errors.items():
78 for k, errors in self.errors.items():
79 r_dict[k].extend([str(e) for e in errors])
79 r_dict[k].extend([str(e) for e in errors])
80 return r_dict
80 return r_dict
81
81
82 @property
82 @property
83 def errors_json(self):
83 def errors_json(self):
84 return json.dumps(self.errors_dict)
84 return json.dumps(self.errors_dict)
85
85
86 def populate_obj(self, obj, ignore_none=False):
86 def populate_obj(self, obj, ignore_none=False):
87 """
87 """
88 Populates the attributes of the passed `obj` with data from the form's
88 Populates the attributes of the passed `obj` with data from the form's
89 fields.
89 fields.
90
90
91 :note: This is a destructive operation; Any attribute with the same name
91 :note: This is a destructive operation; Any attribute with the same name
92 as a field will be overridden. Use with caution.
92 as a field will be overridden. Use with caution.
93 """
93 """
94 if ignore_none:
94 if ignore_none:
95 for name, field in iteritems(self._fields):
95 for name, field in iteritems(self._fields):
96 if field.data is not None:
96 if field.data is not None:
97 field.populate_obj(obj, name)
97 field.populate_obj(obj, name)
98 else:
98 else:
99 for name, field in iteritems(self._fields):
99 for name, field in iteritems(self._fields):
100 field.populate_obj(obj, name)
100 field.populate_obj(obj, name)
101
101
102 css_classes = {}
102 css_classes = {}
103 ignore_labels = {}
103 ignore_labels = {}
104
104
105
105
106 class SignInForm(ReactorForm):
106 class SignInForm(ReactorForm):
107 came_from = wtforms.HiddenField()
107 came_from = wtforms.HiddenField()
108 sign_in_user_name = wtforms.StringField(_('User Name'))
108 sign_in_user_name = wtforms.StringField(_('User Name'))
109 sign_in_user_password = wtforms.PasswordField(_('Password'))
109 sign_in_user_password = wtforms.PasswordField(_('Password'))
110
110
111 ignore_labels = ['submit']
111 ignore_labels = ['submit']
112 css_classes = {'submit': 'btn btn-primary'}
112 css_classes = {'submit': 'btn btn-primary'}
113
113
114 html_attrs = {'sign_in_user_name': {'placeholder': 'Your login'},
114 html_attrs = {'sign_in_user_name': {'placeholder': 'Your login'},
115 'sign_in_user_password': {
115 'sign_in_user_password': {
116 'placeholder': 'Your password'}}
116 'placeholder': 'Your password'}}
117
117
118
118
119 from wtforms.widgets import html_params, HTMLString
119 from wtforms.widgets import html_params, HTMLString
120
120
121
121
122 def select_multi_checkbox(field, ul_class='set', **kwargs):
122 def select_multi_checkbox(field, ul_class='set', **kwargs):
123 """Render a multi-checkbox widget"""
123 """Render a multi-checkbox widget"""
124 kwargs.setdefault('type', 'checkbox')
124 kwargs.setdefault('type', 'checkbox')
125 field_id = kwargs.pop('id', field.id)
125 field_id = kwargs.pop('id', field.id)
126 html = ['<ul %s>' % html_params(id=field_id, class_=ul_class)]
126 html = ['<ul %s>' % html_params(id=field_id, class_=ul_class)]
127 for value, label, checked in field.iter_choices():
127 for value, label, checked in field.iter_choices():
128 choice_id = '%s-%s' % (field_id, value)
128 choice_id = '%s-%s' % (field_id, value)
129 options = dict(kwargs, name=field.name, value=value, id=choice_id)
129 options = dict(kwargs, name=field.name, value=value, id=choice_id)
130 if checked:
130 if checked:
131 options['checked'] = 'checked'
131 options['checked'] = 'checked'
132 html.append('<li><input %s /> ' % html_params(**options))
132 html.append('<li><input %s /> ' % html_params(**options))
133 html.append('<label for="%s">%s</label></li>' % (choice_id, label))
133 html.append('<label for="%s">%s</label></li>' % (choice_id, label))
134 html.append('</ul>')
134 html.append('</ul>')
135 return HTMLString(''.join(html))
135 return HTMLString(''.join(html))
136
136
137
137
138 def button_widget(field, button_cls='ButtonField btn btn-default', **kwargs):
138 def button_widget(field, button_cls='ButtonField btn btn-default', **kwargs):
139 """Render a button widget"""
139 """Render a button widget"""
140 kwargs.setdefault('type', 'button')
140 kwargs.setdefault('type', 'button')
141 field_id = kwargs.pop('id', field.id)
141 field_id = kwargs.pop('id', field.id)
142 kwargs.setdefault('value', field.label.text)
142 kwargs.setdefault('value', field.label.text)
143 html = ['<button %s>%s</button>' % (html_params(id=field_id,
143 html = ['<button %s>%s</button>' % (html_params(id=field_id,
144 class_=button_cls),
144 class_=button_cls),
145 kwargs['value'],)]
145 kwargs['value'],)]
146 return HTMLString(''.join(html))
146 return HTMLString(''.join(html))
147
147
148
148
149 def clean_whitespace(value):
149 def clean_whitespace(value):
150 if value:
150 if value:
151 return value.strip()
151 return value.strip()
152 return value
152 return value
153
153
154
154
155 def found_username_validator(form, field):
155 def found_username_validator(form, field):
156 user = User.by_user_name(field.data)
156 user = UserService.by_user_name(field.data)
157 # sets user to recover in email validator
157 # sets user to recover in email validator
158 form.field_user = user
158 form.field_user = user
159 if not user:
159 if not user:
160 raise wtforms.ValidationError('This username does not exist')
160 raise wtforms.ValidationError('This username does not exist')
161
161
162
162
163 def found_username_email_validator(form, field):
163 def found_username_email_validator(form, field):
164 user = User.by_email(field.data)
164 user = UserService.by_email(field.data)
165 if not user:
165 if not user:
166 raise wtforms.ValidationError('Email is incorrect')
166 raise wtforms.ValidationError('Email is incorrect')
167
167
168
168
169 def unique_username_validator(form, field):
169 def unique_username_validator(form, field):
170 user = User.by_user_name(field.data)
170 user = UserService.by_user_name(field.data)
171 if user:
171 if user:
172 raise wtforms.ValidationError('This username already exists in system')
172 raise wtforms.ValidationError('This username already exists in system')
173
173
174
174
175 def unique_groupname_validator(form, field):
175 def unique_groupname_validator(form, field):
176 group = Group.by_group_name(field.data)
176 group = GroupService.by_group_name(field.data)
177 mod_group = getattr(form, '_modified_group', None)
177 mod_group = getattr(form, '_modified_group', None)
178 if group and (not mod_group or mod_group.id != group.id):
178 if group and (not mod_group or mod_group.id != group.id):
179 raise wtforms.ValidationError(
179 raise wtforms.ValidationError(
180 'This group name already exists in system')
180 'This group name already exists in system')
181
181
182
182
183 def unique_email_validator(form, field):
183 def unique_email_validator(form, field):
184 user = User.by_email(field.data)
184 user = UserService.by_email(field.data)
185 if user:
185 if user:
186 raise wtforms.ValidationError('This email already exists in system')
186 raise wtforms.ValidationError('This email already exists in system')
187
187
188
188
189 def email_validator(form, field):
189 def email_validator(form, field):
190 validator = formencode.validators.Email()
190 validator = formencode.validators.Email()
191 try:
191 try:
192 validator.to_python(field.data)
192 validator.to_python(field.data)
193 except formencode.Invalid as e:
193 except formencode.Invalid as e:
194 raise wtforms.ValidationError(e)
194 raise wtforms.ValidationError(e)
195
195
196
196
197 def unique_alert_email_validator(form, field):
197 def unique_alert_email_validator(form, field):
198 q = DBSession.query(AlertChannel)
198 q = DBSession.query(AlertChannel)
199 q = q.filter(AlertChannel.channel_name == 'email')
199 q = q.filter(AlertChannel.channel_name == 'email')
200 q = q.filter(AlertChannel.channel_value == field.data)
200 q = q.filter(AlertChannel.channel_value == field.data)
201 email = q.first()
201 email = q.first()
202 if email:
202 if email:
203 raise wtforms.ValidationError(
203 raise wtforms.ValidationError(
204 'This email already exists in alert system')
204 'This email already exists in alert system')
205
205
206
206
207 def blocked_email_validator(form, field):
207 def blocked_email_validator(form, field):
208 blocked_emails = [
208 blocked_emails = [
209 'goood-mail.org',
209 'goood-mail.org',
210 'shoeonlineblog.com',
210 'shoeonlineblog.com',
211 'louboutinemart.com',
211 'louboutinemart.com',
212 'guccibagshere.com',
212 'guccibagshere.com',
213 'nikeshoesoutletforsale.com'
213 'nikeshoesoutletforsale.com'
214 ]
214 ]
215 data = field.data or ''
215 data = field.data or ''
216 domain = data.split('@')[-1]
216 domain = data.split('@')[-1]
217 if domain in blocked_emails:
217 if domain in blocked_emails:
218 raise wtforms.ValidationError('Don\'t spam')
218 raise wtforms.ValidationError('Don\'t spam')
219
219
220
220
221 def old_password_validator(form, field):
221 def old_password_validator(form, field):
222 if not field.user.check_password(field.data or ''):
222 if not UserService.check_password(field.user, field.data or ''):
223 raise wtforms.ValidationError('You need to enter correct password')
223 raise wtforms.ValidationError('You need to enter correct password')
224
224
225
225
226 class UserRegisterForm(ReactorForm):
226 class UserRegisterForm(ReactorForm):
227 user_name = wtforms.StringField(
227 user_name = wtforms.StringField(
228 _('User Name'),
228 _('User Name'),
229 filters=[strip_filter],
229 filters=[strip_filter],
230 validators=[
230 validators=[
231 wtforms.validators.Length(min=2, max=30),
231 wtforms.validators.Length(min=2, max=30),
232 wtforms.validators.Regexp(
232 wtforms.validators.Regexp(
233 re.compile(r'^[\.\w-]+$', re.UNICODE),
233 re.compile(r'^[\.\w-]+$', re.UNICODE),
234 message="Invalid characters used"),
234 message="Invalid characters used"),
235 unique_username_validator,
235 unique_username_validator,
236 wtforms.validators.DataRequired()
236 wtforms.validators.DataRequired()
237 ])
237 ])
238
238
239 user_password = wtforms.PasswordField(_('User Password'),
239 user_password = wtforms.PasswordField(_('User Password'),
240 filters=[strip_filter],
240 filters=[strip_filter],
241 validators=[
241 validators=[
242 wtforms.validators.Length(min=4),
242 wtforms.validators.Length(min=4),
243 wtforms.validators.DataRequired()
243 wtforms.validators.DataRequired()
244 ])
244 ])
245
245
246 email = wtforms.StringField(_('Email Address'),
246 email = wtforms.StringField(_('Email Address'),
247 filters=[strip_filter],
247 filters=[strip_filter],
248 validators=[email_validator,
248 validators=[email_validator,
249 unique_email_validator,
249 unique_email_validator,
250 blocked_email_validator,
250 blocked_email_validator,
251 wtforms.validators.DataRequired()])
251 wtforms.validators.DataRequired()])
252 first_name = wtforms.HiddenField(_('First Name'))
252 first_name = wtforms.HiddenField(_('First Name'))
253 last_name = wtforms.HiddenField(_('Last Name'))
253 last_name = wtforms.HiddenField(_('Last Name'))
254
254
255 ignore_labels = ['submit']
255 ignore_labels = ['submit']
256 css_classes = {'submit': 'btn btn-primary'}
256 css_classes = {'submit': 'btn btn-primary'}
257
257
258 html_attrs = {'user_name': {'placeholder': 'Your login'},
258 html_attrs = {'user_name': {'placeholder': 'Your login'},
259 'user_password': {'placeholder': 'Your password'},
259 'user_password': {'placeholder': 'Your password'},
260 'email': {'placeholder': 'Your email'}}
260 'email': {'placeholder': 'Your email'}}
261
261
262
262
263 class UserCreateForm(UserRegisterForm):
263 class UserCreateForm(UserRegisterForm):
264 status = wtforms.BooleanField('User status',
264 status = wtforms.BooleanField('User status',
265 false_values=FALSE_VALUES)
265 false_values=FALSE_VALUES)
266
266
267
267
268 class UserUpdateForm(UserCreateForm):
268 class UserUpdateForm(UserCreateForm):
269 user_name = None
269 user_name = None
270 user_password = wtforms.PasswordField(_('User Password'),
270 user_password = wtforms.PasswordField(_('User Password'),
271 filters=[strip_filter],
271 filters=[strip_filter],
272 validators=[
272 validators=[
273 wtforms.validators.Length(min=4),
273 wtforms.validators.Length(min=4),
274 wtforms.validators.Optional()
274 wtforms.validators.Optional()
275 ])
275 ])
276 email = wtforms.StringField(_('Email Address'),
276 email = wtforms.StringField(_('Email Address'),
277 filters=[strip_filter],
277 filters=[strip_filter],
278 validators=[email_validator,
278 validators=[email_validator,
279 wtforms.validators.DataRequired()])
279 wtforms.validators.DataRequired()])
280
280
281
281
282 class LostPasswordForm(ReactorForm):
282 class LostPasswordForm(ReactorForm):
283 email = wtforms.StringField(_('Email Address'),
283 email = wtforms.StringField(_('Email Address'),
284 filters=[strip_filter],
284 filters=[strip_filter],
285 validators=[email_validator,
285 validators=[email_validator,
286 found_username_email_validator,
286 found_username_email_validator,
287 wtforms.validators.DataRequired()])
287 wtforms.validators.DataRequired()])
288
288
289 submit = wtforms.SubmitField(_('Reset password'))
289 submit = wtforms.SubmitField(_('Reset password'))
290 ignore_labels = ['submit']
290 ignore_labels = ['submit']
291 css_classes = {'submit': 'btn btn-primary'}
291 css_classes = {'submit': 'btn btn-primary'}
292
292
293
293
294 class ChangePasswordForm(ReactorForm):
294 class ChangePasswordForm(ReactorForm):
295 old_password = wtforms.PasswordField(
295 old_password = wtforms.PasswordField(
296 'Old Password',
296 'Old Password',
297 filters=[strip_filter],
297 filters=[strip_filter],
298 validators=[old_password_validator,
298 validators=[old_password_validator,
299 wtforms.validators.DataRequired()])
299 wtforms.validators.DataRequired()])
300
300
301 new_password = wtforms.PasswordField(
301 new_password = wtforms.PasswordField(
302 'New Password',
302 'New Password',
303 filters=[strip_filter],
303 filters=[strip_filter],
304 validators=[wtforms.validators.Length(min=4),
304 validators=[wtforms.validators.Length(min=4),
305 wtforms.validators.DataRequired()])
305 wtforms.validators.DataRequired()])
306 new_password_confirm = wtforms.PasswordField(
306 new_password_confirm = wtforms.PasswordField(
307 'Confirm Password',
307 'Confirm Password',
308 filters=[strip_filter],
308 filters=[strip_filter],
309 validators=[wtforms.validators.EqualTo('new_password'),
309 validators=[wtforms.validators.EqualTo('new_password'),
310 wtforms.validators.DataRequired()])
310 wtforms.validators.DataRequired()])
311 submit = wtforms.SubmitField('Change Password')
311 submit = wtforms.SubmitField('Change Password')
312 ignore_labels = ['submit']
312 ignore_labels = ['submit']
313 css_classes = {'submit': 'btn btn-primary'}
313 css_classes = {'submit': 'btn btn-primary'}
314
314
315
315
316 class CheckPasswordForm(ReactorForm):
316 class CheckPasswordForm(ReactorForm):
317 password = wtforms.PasswordField(
317 password = wtforms.PasswordField(
318 'Password',
318 'Password',
319 filters=[strip_filter],
319 filters=[strip_filter],
320 validators=[old_password_validator,
320 validators=[old_password_validator,
321 wtforms.validators.DataRequired()])
321 wtforms.validators.DataRequired()])
322
322
323
323
324 class NewPasswordForm(ReactorForm):
324 class NewPasswordForm(ReactorForm):
325 new_password = wtforms.PasswordField(
325 new_password = wtforms.PasswordField(
326 'New Password',
326 'New Password',
327 filters=[strip_filter],
327 filters=[strip_filter],
328 validators=[wtforms.validators.Length(min=4),
328 validators=[wtforms.validators.Length(min=4),
329 wtforms.validators.DataRequired()])
329 wtforms.validators.DataRequired()])
330 new_password_confirm = wtforms.PasswordField(
330 new_password_confirm = wtforms.PasswordField(
331 'Confirm Password',
331 'Confirm Password',
332 filters=[strip_filter],
332 filters=[strip_filter],
333 validators=[wtforms.validators.EqualTo('new_password'),
333 validators=[wtforms.validators.EqualTo('new_password'),
334 wtforms.validators.DataRequired()])
334 wtforms.validators.DataRequired()])
335 submit = wtforms.SubmitField('Set Password')
335 submit = wtforms.SubmitField('Set Password')
336 ignore_labels = ['submit']
336 ignore_labels = ['submit']
337 css_classes = {'submit': 'btn btn-primary'}
337 css_classes = {'submit': 'btn btn-primary'}
338
338
339
339
340 class CORSTextAreaField(wtforms.StringField):
340 class CORSTextAreaField(wtforms.StringField):
341 """
341 """
342 This field represents an HTML ``<textarea>`` and can be used to take
342 This field represents an HTML ``<textarea>`` and can be used to take
343 multi-line input.
343 multi-line input.
344 """
344 """
345 widget = wtforms.widgets.TextArea()
345 widget = wtforms.widgets.TextArea()
346
346
347 def process_formdata(self, valuelist):
347 def process_formdata(self, valuelist):
348 self.data = []
348 self.data = []
349 if valuelist:
349 if valuelist:
350 data = [x.strip() for x in valuelist[0].split('\n')]
350 data = [x.strip() for x in valuelist[0].split('\n')]
351 for d in data:
351 for d in data:
352 if not d:
352 if not d:
353 continue
353 continue
354 if d.startswith('www.'):
354 if d.startswith('www.'):
355 d = d[4:]
355 d = d[4:]
356 if data:
356 if data:
357 self.data.append(d)
357 self.data.append(d)
358 else:
358 else:
359 self.data = []
359 self.data = []
360 self.data = '\n'.join(self.data)
360 self.data = '\n'.join(self.data)
361
361
362
362
363 class ApplicationCreateForm(ReactorForm):
363 class ApplicationCreateForm(ReactorForm):
364 resource_name = wtforms.StringField(
364 resource_name = wtforms.StringField(
365 _('Application name'),
365 _('Application name'),
366 filters=[strip_filter],
366 filters=[strip_filter],
367 validators=[wtforms.validators.Length(min=1),
367 validators=[wtforms.validators.Length(min=1),
368 wtforms.validators.DataRequired()])
368 wtforms.validators.DataRequired()])
369
369
370 domains = CORSTextAreaField(
370 domains = CORSTextAreaField(
371 _('Domain names for CORS headers '),
371 _('Domain names for CORS headers '),
372 validators=[wtforms.validators.Length(min=1),
372 validators=[wtforms.validators.Length(min=1),
373 wtforms.validators.Optional()],
373 wtforms.validators.Optional()],
374 description='Required for Javascript error '
374 description='Required for Javascript error '
375 'tracking (one line one domain, skip http:// part)')
375 'tracking (one line one domain, skip http:// part)')
376
376
377 submit = wtforms.SubmitField(_('Create Application'))
377 submit = wtforms.SubmitField(_('Create Application'))
378
378
379 ignore_labels = ['submit']
379 ignore_labels = ['submit']
380 css_classes = {'submit': 'btn btn-primary'}
380 css_classes = {'submit': 'btn btn-primary'}
381 html_attrs = {'resource_name': {'placeholder': 'Application Name'},
381 html_attrs = {'resource_name': {'placeholder': 'Application Name'},
382 'uptime_url': {'placeholder': 'http://somedomain.com'}}
382 'uptime_url': {'placeholder': 'http://somedomain.com'}}
383
383
384
384
385 class ApplicationUpdateForm(ApplicationCreateForm):
385 class ApplicationUpdateForm(ApplicationCreateForm):
386 default_grouping = wtforms.SelectField(
386 default_grouping = wtforms.SelectField(
387 _('Default grouping for errors'),
387 _('Default grouping for errors'),
388 choices=[('url_type', 'Error Type + location',),
388 choices=[('url_type', 'Error Type + location',),
389 ('url_traceback', 'Traceback + location',),
389 ('url_traceback', 'Traceback + location',),
390 ('traceback_server', 'Traceback + Server',)],
390 ('traceback_server', 'Traceback + Server',)],
391 default='url_traceback')
391 default='url_traceback')
392
392
393 error_report_threshold = wtforms.IntegerField(
393 error_report_threshold = wtforms.IntegerField(
394 _('Alert on error reports'),
394 _('Alert on error reports'),
395 validators=[
395 validators=[
396 wtforms.validators.NumberRange(min=1),
396 wtforms.validators.NumberRange(min=1),
397 wtforms.validators.DataRequired()
397 wtforms.validators.DataRequired()
398 ],
398 ],
399 description='Application requires to send at least this amount of '
399 description='Application requires to send at least this amount of '
400 'error reports per minute to open alert'
400 'error reports per minute to open alert'
401 )
401 )
402
402
403 slow_report_threshold = wtforms.IntegerField(
403 slow_report_threshold = wtforms.IntegerField(
404 _('Alert on slow reports'),
404 _('Alert on slow reports'),
405 validators=[wtforms.validators.NumberRange(min=1),
405 validators=[wtforms.validators.NumberRange(min=1),
406 wtforms.validators.DataRequired()],
406 wtforms.validators.DataRequired()],
407 description='Application requires to send at least this amount of '
407 description='Application requires to send at least this amount of '
408 'slow reports per minute to open alert')
408 'slow reports per minute to open alert')
409
409
410 allow_permanent_storage = wtforms.BooleanField(
410 allow_permanent_storage = wtforms.BooleanField(
411 _('Permanent logs'),
411 _('Permanent logs'),
412 false_values=FALSE_VALUES,
412 false_values=FALSE_VALUES,
413 description=_(
413 description=_(
414 'Allow permanent storage of logs in separate DB partitions'))
414 'Allow permanent storage of logs in separate DB partitions'))
415
415
416 submit = wtforms.SubmitField(_('Create Application'))
416 submit = wtforms.SubmitField(_('Create Application'))
417
417
418
418
419 class UserSearchSchemaForm(ReactorForm):
419 class UserSearchSchemaForm(ReactorForm):
420 user_name = wtforms.StringField('User Name',
420 user_name = wtforms.StringField('User Name',
421 filters=[strip_filter], )
421 filters=[strip_filter], )
422
422
423 submit = wtforms.SubmitField(_('Search User'))
423 submit = wtforms.SubmitField(_('Search User'))
424 ignore_labels = ['submit']
424 ignore_labels = ['submit']
425 css_classes = {'submit': 'btn btn-primary'}
425 css_classes = {'submit': 'btn btn-primary'}
426
426
427 '<li class="user_exists"><span></span></li>'
427 '<li class="user_exists"><span></span></li>'
428
428
429
429
430 class YesNoForm(ReactorForm):
430 class YesNoForm(ReactorForm):
431 no = wtforms.SubmitField('No', default='')
431 no = wtforms.SubmitField('No', default='')
432 yes = wtforms.SubmitField('Yes', default='')
432 yes = wtforms.SubmitField('Yes', default='')
433 ignore_labels = ['submit']
433 ignore_labels = ['submit']
434 css_classes = {'submit': 'btn btn-primary'}
434 css_classes = {'submit': 'btn btn-primary'}
435
435
436
436
437 status_codes = [('', 'All',), ('500', '500',), ('404', '404',)]
437 status_codes = [('', 'All',), ('500', '500',), ('404', '404',)]
438
438
439 priorities = [('', 'All',)]
439 priorities = [('', 'All',)]
440 for i in range(1, 11):
440 for i in range(1, 11):
441 priorities.append((str(i), str(i),))
441 priorities.append((str(i), str(i),))
442
442
443 report_status_choices = [('', 'All',),
443 report_status_choices = [('', 'All',),
444 ('never_reviewed', 'Never revieved',),
444 ('never_reviewed', 'Never revieved',),
445 ('reviewed', 'Revieved',),
445 ('reviewed', 'Revieved',),
446 ('public', 'Public',),
446 ('public', 'Public',),
447 ('fixed', 'Fixed',), ]
447 ('fixed', 'Fixed',), ]
448
448
449
449
450 class ReportBrowserForm(ReactorForm):
450 class ReportBrowserForm(ReactorForm):
451 applications = wtforms.SelectMultipleField('Applications',
451 applications = wtforms.SelectMultipleField('Applications',
452 widget=select_multi_checkbox)
452 widget=select_multi_checkbox)
453 http_status = wtforms.SelectField('HTTP Status', choices=status_codes)
453 http_status = wtforms.SelectField('HTTP Status', choices=status_codes)
454 priority = wtforms.SelectField('Priority', choices=priorities, default='')
454 priority = wtforms.SelectField('Priority', choices=priorities, default='')
455 start_date = wtforms.DateField('Start Date')
455 start_date = wtforms.DateField('Start Date')
456 end_date = wtforms.DateField('End Date')
456 end_date = wtforms.DateField('End Date')
457 error = wtforms.StringField('Error')
457 error = wtforms.StringField('Error')
458 url_path = wtforms.StringField('URL Path')
458 url_path = wtforms.StringField('URL Path')
459 url_domain = wtforms.StringField('URL Domain')
459 url_domain = wtforms.StringField('URL Domain')
460 report_status = wtforms.SelectField('Report status',
460 report_status = wtforms.SelectField('Report status',
461 choices=report_status_choices,
461 choices=report_status_choices,
462 default='')
462 default='')
463 submit = wtforms.SubmitField('<span class="glyphicon glyphicon-search">'
463 submit = wtforms.SubmitField('<span class="glyphicon glyphicon-search">'
464 '</span> Filter results',
464 '</span> Filter results',
465 widget=button_widget)
465 widget=button_widget)
466
466
467 ignore_labels = ['submit']
467 ignore_labels = ['submit']
468 css_classes = {'submit': 'btn btn-primary'}
468 css_classes = {'submit': 'btn btn-primary'}
469
469
470
470
471 slow_report_status_choices = [('', 'All',),
471 slow_report_status_choices = [('', 'All',),
472 ('never_reviewed', 'Never revieved',),
472 ('never_reviewed', 'Never revieved',),
473 ('reviewed', 'Revieved',),
473 ('reviewed', 'Revieved',),
474 ('public', 'Public',), ]
474 ('public', 'Public',), ]
475
475
476
476
477 class BulkOperationForm(ReactorForm):
477 class BulkOperationForm(ReactorForm):
478 applications = wtforms.SelectField('Applications')
478 applications = wtforms.SelectField('Applications')
479 start_date = wtforms.DateField(
479 start_date = wtforms.DateField(
480 'Start Date',
480 'Start Date',
481 default=lambda: datetime.datetime.utcnow() - datetime.timedelta(
481 default=lambda: datetime.datetime.utcnow() - datetime.timedelta(
482 days=90))
482 days=90))
483 end_date = wtforms.DateField('End Date')
483 end_date = wtforms.DateField('End Date')
484 confirm = wtforms.BooleanField(
484 confirm = wtforms.BooleanField(
485 'Confirm operation',
485 'Confirm operation',
486 validators=[wtforms.validators.DataRequired()])
486 validators=[wtforms.validators.DataRequired()])
487
487
488
488
489 class LogBrowserForm(ReactorForm):
489 class LogBrowserForm(ReactorForm):
490 applications = wtforms.SelectMultipleField('Applications',
490 applications = wtforms.SelectMultipleField('Applications',
491 widget=select_multi_checkbox)
491 widget=select_multi_checkbox)
492 start_date = wtforms.DateField('Start Date')
492 start_date = wtforms.DateField('Start Date')
493 log_level = wtforms.StringField('Log level')
493 log_level = wtforms.StringField('Log level')
494 message = wtforms.StringField('Message')
494 message = wtforms.StringField('Message')
495 namespace = wtforms.StringField('Namespace')
495 namespace = wtforms.StringField('Namespace')
496 submit = wtforms.SubmitField(
496 submit = wtforms.SubmitField(
497 '<span class="glyphicon glyphicon-search"></span> Filter results',
497 '<span class="glyphicon glyphicon-search"></span> Filter results',
498 widget=button_widget)
498 widget=button_widget)
499 ignore_labels = ['submit']
499 ignore_labels = ['submit']
500 css_classes = {'submit': 'btn btn-primary'}
500 css_classes = {'submit': 'btn btn-primary'}
501
501
502
502
503 class CommentForm(ReactorForm):
503 class CommentForm(ReactorForm):
504 body = wtforms.TextAreaField('Comment', validators=[
504 body = wtforms.TextAreaField('Comment', validators=[
505 wtforms.validators.Length(min=1),
505 wtforms.validators.Length(min=1),
506 wtforms.validators.DataRequired()
506 wtforms.validators.DataRequired()
507 ])
507 ])
508 submit = wtforms.SubmitField('Comment', )
508 submit = wtforms.SubmitField('Comment', )
509 ignore_labels = ['submit']
509 ignore_labels = ['submit']
510 css_classes = {'submit': 'btn btn-primary'}
510 css_classes = {'submit': 'btn btn-primary'}
511
511
512
512
513 class EmailChannelCreateForm(ReactorForm):
513 class EmailChannelCreateForm(ReactorForm):
514 email = wtforms.StringField(_('Email Address'),
514 email = wtforms.StringField(_('Email Address'),
515 filters=[strip_filter],
515 filters=[strip_filter],
516 validators=[email_validator,
516 validators=[email_validator,
517 unique_alert_email_validator,
517 unique_alert_email_validator,
518 wtforms.validators.DataRequired()])
518 wtforms.validators.DataRequired()])
519 submit = wtforms.SubmitField('Add email channel', )
519 submit = wtforms.SubmitField('Add email channel', )
520 ignore_labels = ['submit']
520 ignore_labels = ['submit']
521 css_classes = {'submit': 'btn btn-primary'}
521 css_classes = {'submit': 'btn btn-primary'}
522
522
523
523
524 def gen_user_profile_form():
524 def gen_user_profile_form():
525 class UserProfileForm(ReactorForm):
525 class UserProfileForm(ReactorForm):
526 email = wtforms.StringField(
526 email = wtforms.StringField(
527 _('Email Address'),
527 _('Email Address'),
528 validators=[email_validator, wtforms.validators.DataRequired()])
528 validators=[email_validator, wtforms.validators.DataRequired()])
529 first_name = wtforms.StringField(_('First Name'))
529 first_name = wtforms.StringField(_('First Name'))
530 last_name = wtforms.StringField(_('Last Name'))
530 last_name = wtforms.StringField(_('Last Name'))
531 company_name = wtforms.StringField(_('Company Name'))
531 company_name = wtforms.StringField(_('Company Name'))
532 company_address = wtforms.TextAreaField(_('Company Address'))
532 company_address = wtforms.TextAreaField(_('Company Address'))
533 zip_code = wtforms.StringField(_('ZIP code'))
533 zip_code = wtforms.StringField(_('ZIP code'))
534 city = wtforms.StringField(_('City'))
534 city = wtforms.StringField(_('City'))
535 notifications = wtforms.BooleanField('Account notifications',
535 notifications = wtforms.BooleanField('Account notifications',
536 false_values=FALSE_VALUES)
536 false_values=FALSE_VALUES)
537 submit = wtforms.SubmitField(_('Update Account'))
537 submit = wtforms.SubmitField(_('Update Account'))
538 ignore_labels = ['submit']
538 ignore_labels = ['submit']
539 css_classes = {'submit': 'btn btn-primary'}
539 css_classes = {'submit': 'btn btn-primary'}
540
540
541 return UserProfileForm
541 return UserProfileForm
542
542
543
543
544 class PurgeAppForm(ReactorForm):
544 class PurgeAppForm(ReactorForm):
545 resource_id = wtforms.HiddenField(
545 resource_id = wtforms.HiddenField(
546 'App Id',
546 'App Id',
547 validators=[wtforms.validators.DataRequired()])
547 validators=[wtforms.validators.DataRequired()])
548 days = wtforms.IntegerField(
548 days = wtforms.IntegerField(
549 'Days',
549 'Days',
550 validators=[wtforms.validators.DataRequired()])
550 validators=[wtforms.validators.DataRequired()])
551 password = wtforms.PasswordField(
551 password = wtforms.PasswordField(
552 'Admin Password',
552 'Admin Password',
553 validators=[old_password_validator, wtforms.validators.DataRequired()])
553 validators=[old_password_validator, wtforms.validators.DataRequired()])
554 submit = wtforms.SubmitField(_('Purge Data'))
554 submit = wtforms.SubmitField(_('Purge Data'))
555 ignore_labels = ['submit']
555 ignore_labels = ['submit']
556 css_classes = {'submit': 'btn btn-primary'}
556 css_classes = {'submit': 'btn btn-primary'}
557
557
558
558
559 class IntegrationRepoForm(ReactorForm):
559 class IntegrationRepoForm(ReactorForm):
560 host_name = wtforms.StringField("Service Host", default='')
560 host_name = wtforms.StringField("Service Host", default='')
561 user_name = wtforms.StringField(
561 user_name = wtforms.StringField(
562 "User Name",
562 "User Name",
563 filters=[strip_filter],
563 filters=[strip_filter],
564 validators=[wtforms.validators.DataRequired(),
564 validators=[wtforms.validators.DataRequired(),
565 wtforms.validators.Length(min=1)])
565 wtforms.validators.Length(min=1)])
566 repo_name = wtforms.StringField(
566 repo_name = wtforms.StringField(
567 "Repo Name",
567 "Repo Name",
568 filters=[strip_filter],
568 filters=[strip_filter],
569 validators=[wtforms.validators.DataRequired(),
569 validators=[wtforms.validators.DataRequired(),
570 wtforms.validators.Length(min=1)])
570 wtforms.validators.Length(min=1)])
571
571
572
572
573 class IntegrationBitbucketForm(IntegrationRepoForm):
573 class IntegrationBitbucketForm(IntegrationRepoForm):
574 host_name = wtforms.StringField("Service Host",
574 host_name = wtforms.StringField("Service Host",
575 default='https://bitbucket.org')
575 default='https://bitbucket.org')
576
576
577 def validate_user_name(self, field):
577 def validate_user_name(self, field):
578 try:
578 try:
579 request = pyramid.threadlocal.get_current_request()
579 request = pyramid.threadlocal.get_current_request()
580 client = BitbucketIntegration.create_client(
580 client = BitbucketIntegration.create_client(
581 request,
581 request,
582 self.user_name.data,
582 self.user_name.data,
583 self.repo_name.data)
583 self.repo_name.data)
584 client.get_assignees()
584 client.get_assignees()
585 except IntegrationException as e:
585 except IntegrationException as e:
586 raise wtforms.validators.ValidationError(str(e))
586 raise wtforms.validators.ValidationError(str(e))
587
587
588
588
589 class IntegrationGithubForm(IntegrationRepoForm):
589 class IntegrationGithubForm(IntegrationRepoForm):
590 host_name = wtforms.StringField("Service Host",
590 host_name = wtforms.StringField("Service Host",
591 default='https://github.com')
591 default='https://github.com')
592
592
593 def validate_user_name(self, field):
593 def validate_user_name(self, field):
594 try:
594 try:
595 request = pyramid.threadlocal.get_current_request()
595 request = pyramid.threadlocal.get_current_request()
596 client = GithubIntegration.create_client(
596 client = GithubIntegration.create_client(
597 request,
597 request,
598 self.user_name.data,
598 self.user_name.data,
599 self.repo_name.data)
599 self.repo_name.data)
600 client.get_assignees()
600 client.get_assignees()
601 except IntegrationException as e:
601 except IntegrationException as e:
602 raise wtforms.validators.ValidationError(str(e))
602 raise wtforms.validators.ValidationError(str(e))
603 raise wtforms.validators.ValidationError(str(e))
603 raise wtforms.validators.ValidationError(str(e))
604
604
605
605
606 def filter_rooms(data):
606 def filter_rooms(data):
607 if data is not None:
607 if data is not None:
608 rooms = data.split(',')
608 rooms = data.split(',')
609 return ','.join([r.strip() for r in rooms])
609 return ','.join([r.strip() for r in rooms])
610
610
611
611
612 class IntegrationCampfireForm(ReactorForm):
612 class IntegrationCampfireForm(ReactorForm):
613 account = wtforms.StringField(
613 account = wtforms.StringField(
614 'Account',
614 'Account',
615 filters=[strip_filter],
615 filters=[strip_filter],
616 validators=[wtforms.validators.DataRequired()])
616 validators=[wtforms.validators.DataRequired()])
617 api_token = wtforms.StringField(
617 api_token = wtforms.StringField(
618 'Api Token',
618 'Api Token',
619 filters=[strip_filter],
619 filters=[strip_filter],
620 validators=[wtforms.validators.DataRequired()])
620 validators=[wtforms.validators.DataRequired()])
621 rooms = wtforms.StringField('Room ID list', filters=[filter_rooms])
621 rooms = wtforms.StringField('Room ID list', filters=[filter_rooms])
622
622
623 def validate_api_token(self, field):
623 def validate_api_token(self, field):
624 try:
624 try:
625 client = CampfireIntegration.create_client(self.api_token.data,
625 client = CampfireIntegration.create_client(self.api_token.data,
626 self.account.data)
626 self.account.data)
627 client.get_account()
627 client.get_account()
628 except IntegrationException as e:
628 except IntegrationException as e:
629 raise wtforms.validators.ValidationError(str(e))
629 raise wtforms.validators.ValidationError(str(e))
630
630
631 def validate_rooms(self, field):
631 def validate_rooms(self, field):
632 if not field.data:
632 if not field.data:
633 return
633 return
634 client = CampfireIntegration.create_client(self.api_token.data,
634 client = CampfireIntegration.create_client(self.api_token.data,
635 self.account.data)
635 self.account.data)
636
636
637 try:
637 try:
638 room_list = [r['id'] for r in client.get_rooms()]
638 room_list = [r['id'] for r in client.get_rooms()]
639 except IntegrationException as e:
639 except IntegrationException as e:
640 raise wtforms.validators.ValidationError(str(e))
640 raise wtforms.validators.ValidationError(str(e))
641
641
642 rooms = field.data.split(',')
642 rooms = field.data.split(',')
643 if len(rooms) > 3:
643 if len(rooms) > 3:
644 msg = 'You can use up to 3 room ids'
644 msg = 'You can use up to 3 room ids'
645 raise wtforms.validators.ValidationError(msg)
645 raise wtforms.validators.ValidationError(msg)
646 if rooms:
646 if rooms:
647 for room_id in rooms:
647 for room_id in rooms:
648 if int(room_id) not in room_list:
648 if int(room_id) not in room_list:
649 msg = "Room %s doesn't exist"
649 msg = "Room %s doesn't exist"
650 raise wtforms.validators.ValidationError(msg % room_id)
650 raise wtforms.validators.ValidationError(msg % room_id)
651 if not room_id.strip().isdigit():
651 if not room_id.strip().isdigit():
652 msg = 'You must use only integers for room ids'
652 msg = 'You must use only integers for room ids'
653 raise wtforms.validators.ValidationError(msg)
653 raise wtforms.validators.ValidationError(msg)
654
654
655 submit = wtforms.SubmitField(_('Connect to Campfire'))
655 submit = wtforms.SubmitField(_('Connect to Campfire'))
656 ignore_labels = ['submit']
656 ignore_labels = ['submit']
657 css_classes = {'submit': 'btn btn-primary'}
657 css_classes = {'submit': 'btn btn-primary'}
658
658
659
659
660 def filter_rooms(data):
660 def filter_rooms(data):
661 if data is not None:
661 if data is not None:
662 rooms = data.split(',')
662 rooms = data.split(',')
663 return ','.join([r.strip() for r in rooms])
663 return ','.join([r.strip() for r in rooms])
664
664
665
665
666 class IntegrationHipchatForm(ReactorForm):
666 class IntegrationHipchatForm(ReactorForm):
667 api_token = wtforms.StringField(
667 api_token = wtforms.StringField(
668 'Api Token',
668 'Api Token',
669 filters=[strip_filter],
669 filters=[strip_filter],
670 validators=[wtforms.validators.DataRequired()])
670 validators=[wtforms.validators.DataRequired()])
671 rooms = wtforms.StringField(
671 rooms = wtforms.StringField(
672 'Room ID list',
672 'Room ID list',
673 filters=[filter_rooms],
673 filters=[filter_rooms],
674 validators=[wtforms.validators.DataRequired()])
674 validators=[wtforms.validators.DataRequired()])
675
675
676 def validate_rooms(self, field):
676 def validate_rooms(self, field):
677 if not field.data:
677 if not field.data:
678 return
678 return
679 client = HipchatIntegration.create_client(self.api_token.data)
679 client = HipchatIntegration.create_client(self.api_token.data)
680 rooms = field.data.split(',')
680 rooms = field.data.split(',')
681 if len(rooms) > 3:
681 if len(rooms) > 3:
682 msg = 'You can use up to 3 room ids'
682 msg = 'You can use up to 3 room ids'
683 raise wtforms.validators.ValidationError(msg)
683 raise wtforms.validators.ValidationError(msg)
684 if rooms:
684 if rooms:
685 for room_id in rooms:
685 for room_id in rooms:
686 if not room_id.strip().isdigit():
686 if not room_id.strip().isdigit():
687 msg = 'You must use only integers for room ids'
687 msg = 'You must use only integers for room ids'
688 raise wtforms.validators.ValidationError(msg)
688 raise wtforms.validators.ValidationError(msg)
689 try:
689 try:
690 client.send({
690 client.send({
691 "message_format": 'text',
691 "message_format": 'text',
692 "message": "testing for room existence",
692 "message": "testing for room existence",
693 "from": "AppEnlight",
693 "from": "AppEnlight",
694 "room_id": room_id,
694 "room_id": room_id,
695 "color": "green"
695 "color": "green"
696 })
696 })
697 except IntegrationException as exc:
697 except IntegrationException as exc:
698 msg = 'Room id: %s exception: %s'
698 msg = 'Room id: %s exception: %s'
699 raise wtforms.validators.ValidationError(msg % (room_id,
699 raise wtforms.validators.ValidationError(msg % (room_id,
700 exc))
700 exc))
701
701
702
702
703 class IntegrationFlowdockForm(ReactorForm):
703 class IntegrationFlowdockForm(ReactorForm):
704 api_token = wtforms.StringField('API Token',
704 api_token = wtforms.StringField('API Token',
705 filters=[strip_filter],
705 filters=[strip_filter],
706 validators=[
706 validators=[
707 wtforms.validators.DataRequired()
707 wtforms.validators.DataRequired()
708 ], )
708 ], )
709
709
710 def validate_api_token(self, field):
710 def validate_api_token(self, field):
711 try:
711 try:
712 client = FlowdockIntegration.create_client(self.api_token.data)
712 client = FlowdockIntegration.create_client(self.api_token.data)
713 registry = pyramid.threadlocal.get_current_registry()
713 registry = pyramid.threadlocal.get_current_registry()
714 payload = {
714 payload = {
715 "source": registry.settings['mailing.from_name'],
715 "source": registry.settings['mailing.from_name'],
716 "from_address": registry.settings['mailing.from_email'],
716 "from_address": registry.settings['mailing.from_email'],
717 "subject": "Integration test",
717 "subject": "Integration test",
718 "content": "If you can see this it was successful",
718 "content": "If you can see this it was successful",
719 "tags": ["appenlight"],
719 "tags": ["appenlight"],
720 "link": registry.settings['mailing.app_url']
720 "link": registry.settings['mailing.app_url']
721 }
721 }
722 client.send_to_inbox(payload)
722 client.send_to_inbox(payload)
723 except IntegrationException as e:
723 except IntegrationException as e:
724 raise wtforms.validators.ValidationError(str(e))
724 raise wtforms.validators.ValidationError(str(e))
725
725
726
726
727 class IntegrationSlackForm(ReactorForm):
727 class IntegrationSlackForm(ReactorForm):
728 webhook_url = wtforms.StringField(
728 webhook_url = wtforms.StringField(
729 'Reports webhook',
729 'Reports webhook',
730 filters=[strip_filter],
730 filters=[strip_filter],
731 validators=[wtforms.validators.DataRequired()])
731 validators=[wtforms.validators.DataRequired()])
732
732
733 def validate_webhook_url(self, field):
733 def validate_webhook_url(self, field):
734 registry = pyramid.threadlocal.get_current_registry()
734 registry = pyramid.threadlocal.get_current_registry()
735 client = SlackIntegration.create_client(field.data)
735 client = SlackIntegration.create_client(field.data)
736 link = "<%s|%s>" % (registry.settings['mailing.app_url'],
736 link = "<%s|%s>" % (registry.settings['mailing.app_url'],
737 registry.settings['mailing.from_name'])
737 registry.settings['mailing.from_name'])
738 test_data = {
738 test_data = {
739 "username": "AppEnlight",
739 "username": "AppEnlight",
740 "icon_emoji": ":fire:",
740 "icon_emoji": ":fire:",
741 "attachments": [
741 "attachments": [
742 {"fallback": "Testing integration channel: %s" % link,
742 {"fallback": "Testing integration channel: %s" % link,
743 "pretext": "Testing integration channel: %s" % link,
743 "pretext": "Testing integration channel: %s" % link,
744 "color": "good",
744 "color": "good",
745 "fields": [
745 "fields": [
746 {
746 {
747 "title": "Status",
747 "title": "Status",
748 "value": "Integration is working fine",
748 "value": "Integration is working fine",
749 "short": False
749 "short": False
750 }
750 }
751 ]}
751 ]}
752 ]
752 ]
753 }
753 }
754 try:
754 try:
755 client.make_request(data=test_data)
755 client.make_request(data=test_data)
756 except IntegrationException as exc:
756 except IntegrationException as exc:
757 raise wtforms.validators.ValidationError(str(exc))
757 raise wtforms.validators.ValidationError(str(exc))
758
758
759
759
760 class IntegrationWebhooksForm(ReactorForm):
760 class IntegrationWebhooksForm(ReactorForm):
761 reports_webhook = wtforms.StringField(
761 reports_webhook = wtforms.StringField(
762 'Reports webhook',
762 'Reports webhook',
763 filters=[strip_filter],
763 filters=[strip_filter],
764 validators=[wtforms.validators.DataRequired()])
764 validators=[wtforms.validators.DataRequired()])
765 alerts_webhook = wtforms.StringField(
765 alerts_webhook = wtforms.StringField(
766 'Alerts webhook',
766 'Alerts webhook',
767 filters=[strip_filter],
767 filters=[strip_filter],
768 validators=[wtforms.validators.DataRequired()])
768 validators=[wtforms.validators.DataRequired()])
769 submit = wtforms.SubmitField(_('Setup webhooks'))
769 submit = wtforms.SubmitField(_('Setup webhooks'))
770 ignore_labels = ['submit']
770 ignore_labels = ['submit']
771 css_classes = {'submit': 'btn btn-primary'}
771 css_classes = {'submit': 'btn btn-primary'}
772
772
773
773
774 class IntegrationJiraForm(ReactorForm):
774 class IntegrationJiraForm(ReactorForm):
775 host_name = wtforms.StringField(
775 host_name = wtforms.StringField(
776 'Server URL',
776 'Server URL',
777 filters=[strip_filter],
777 filters=[strip_filter],
778 validators=[wtforms.validators.DataRequired()])
778 validators=[wtforms.validators.DataRequired()])
779 user_name = wtforms.StringField(
779 user_name = wtforms.StringField(
780 'Username',
780 'Username',
781 filters=[strip_filter],
781 filters=[strip_filter],
782 validators=[wtforms.validators.DataRequired()])
782 validators=[wtforms.validators.DataRequired()])
783 password = wtforms.PasswordField(
783 password = wtforms.PasswordField(
784 'Password',
784 'Password',
785 filters=[strip_filter],
785 filters=[strip_filter],
786 validators=[wtforms.validators.DataRequired()])
786 validators=[wtforms.validators.DataRequired()])
787 project = wtforms.StringField(
787 project = wtforms.StringField(
788 'Project key',
788 'Project key',
789 filters=[uppercase_filter, strip_filter],
789 filters=[uppercase_filter, strip_filter],
790 validators=[wtforms.validators.DataRequired()])
790 validators=[wtforms.validators.DataRequired()])
791
791
792 def validate_project(self, field):
792 def validate_project(self, field):
793 if not field.data:
793 if not field.data:
794 return
794 return
795 try:
795 try:
796 client = JiraClient(self.user_name.data,
796 client = JiraClient(self.user_name.data,
797 self.password.data,
797 self.password.data,
798 self.host_name.data,
798 self.host_name.data,
799 self.project.data)
799 self.project.data)
800 except Exception as exc:
800 except Exception as exc:
801 raise wtforms.validators.ValidationError(str(exc))
801 raise wtforms.validators.ValidationError(str(exc))
802
802
803 room_list = [r.key.upper() for r in client.get_projects()]
803 room_list = [r.key.upper() for r in client.get_projects()]
804 if field.data.upper() not in room_list:
804 if field.data.upper() not in room_list:
805 msg = "Project %s doesn\t exist in your Jira Instance"
805 msg = "Project %s doesn\t exist in your Jira Instance"
806 raise wtforms.validators.ValidationError(msg % field.data)
806 raise wtforms.validators.ValidationError(msg % field.data)
807
807
808
808
809 def get_deletion_form(resource):
809 def get_deletion_form(resource):
810 class F(ReactorForm):
810 class F(ReactorForm):
811 application_name = wtforms.StringField(
811 application_name = wtforms.StringField(
812 'Application Name',
812 'Application Name',
813 filters=[strip_filter],
813 filters=[strip_filter],
814 validators=[wtforms.validators.AnyOf([resource.resource_name])])
814 validators=[wtforms.validators.AnyOf([resource.resource_name])])
815 resource_id = wtforms.HiddenField(default=resource.resource_id)
815 resource_id = wtforms.HiddenField(default=resource.resource_id)
816 submit = wtforms.SubmitField(_('Delete my application'))
816 submit = wtforms.SubmitField(_('Delete my application'))
817 ignore_labels = ['submit']
817 ignore_labels = ['submit']
818 css_classes = {'submit': 'btn btn-danger'}
818 css_classes = {'submit': 'btn btn-danger'}
819
819
820 return F
820 return F
821
821
822
822
823 class ChangeApplicationOwnerForm(ReactorForm):
823 class ChangeApplicationOwnerForm(ReactorForm):
824 password = wtforms.PasswordField(
824 password = wtforms.PasswordField(
825 'Password',
825 'Password',
826 filters=[strip_filter],
826 filters=[strip_filter],
827 validators=[old_password_validator,
827 validators=[old_password_validator,
828 wtforms.validators.DataRequired()])
828 wtforms.validators.DataRequired()])
829
829
830 user_name = wtforms.StringField(
830 user_name = wtforms.StringField(
831 'New owners username',
831 'New owners username',
832 filters=[strip_filter],
832 filters=[strip_filter],
833 validators=[found_username_validator,
833 validators=[found_username_validator,
834 wtforms.validators.DataRequired()])
834 wtforms.validators.DataRequired()])
835 submit = wtforms.SubmitField(_('Transfer ownership of application'))
835 submit = wtforms.SubmitField(_('Transfer ownership of application'))
836 ignore_labels = ['submit']
836 ignore_labels = ['submit']
837 css_classes = {'submit': 'btn btn-danger'}
837 css_classes = {'submit': 'btn btn-danger'}
838
838
839
839
840 def default_filename():
840 def default_filename():
841 return 'Invoice %s' % datetime.datetime.utcnow().strftime('%Y/%m')
841 return 'Invoice %s' % datetime.datetime.utcnow().strftime('%Y/%m')
842
842
843
843
844 class FileUploadForm(ReactorForm):
844 class FileUploadForm(ReactorForm):
845 title = wtforms.StringField('File Title',
845 title = wtforms.StringField('File Title',
846 default=default_filename,
846 default=default_filename,
847 validators=[wtforms.validators.DataRequired()])
847 validators=[wtforms.validators.DataRequired()])
848 file = wtforms.FileField('File')
848 file = wtforms.FileField('File')
849
849
850 def validate_file(self, field):
850 def validate_file(self, field):
851 if not hasattr(field.data, 'file'):
851 if not hasattr(field.data, 'file'):
852 raise wtforms.ValidationError('File is missing')
852 raise wtforms.ValidationError('File is missing')
853
853
854 submit = wtforms.SubmitField(_('Upload'))
854 submit = wtforms.SubmitField(_('Upload'))
855
855
856
856
857 def get_partition_deletion_form(es_indices, pg_indices):
857 def get_partition_deletion_form(es_indices, pg_indices):
858 class F(ReactorForm):
858 class F(ReactorForm):
859 es_index = wtforms.SelectMultipleField('Elasticsearch',
859 es_index = wtforms.SelectMultipleField('Elasticsearch',
860 choices=[(ix, '') for ix in
860 choices=[(ix, '') for ix in
861 es_indices])
861 es_indices])
862 pg_index = wtforms.SelectMultipleField('pg',
862 pg_index = wtforms.SelectMultipleField('pg',
863 choices=[(ix, '') for ix in
863 choices=[(ix, '') for ix in
864 pg_indices])
864 pg_indices])
865 confirm = wtforms.TextField('Confirm',
865 confirm = wtforms.TextField('Confirm',
866 filters=[uppercase_filter, strip_filter],
866 filters=[uppercase_filter, strip_filter],
867 validators=[
867 validators=[
868 wtforms.validators.AnyOf(['CONFIRM']),
868 wtforms.validators.AnyOf(['CONFIRM']),
869 wtforms.validators.DataRequired()])
869 wtforms.validators.DataRequired()])
870 ignore_labels = ['submit']
870 ignore_labels = ['submit']
871 css_classes = {'submit': 'btn btn-danger'}
871 css_classes = {'submit': 'btn btn-danger'}
872
872
873 return F
873 return F
874
874
875
875
876 class GroupCreateForm(ReactorForm):
876 class GroupCreateForm(ReactorForm):
877 group_name = wtforms.StringField(
877 group_name = wtforms.StringField(
878 _('Group Name'),
878 _('Group Name'),
879 filters=[strip_filter],
879 filters=[strip_filter],
880 validators=[
880 validators=[
881 wtforms.validators.Length(min=2, max=50),
881 wtforms.validators.Length(min=2, max=50),
882 unique_groupname_validator,
882 unique_groupname_validator,
883 wtforms.validators.DataRequired()
883 wtforms.validators.DataRequired()
884 ])
884 ])
885 description = wtforms.StringField(_('Group description'))
885 description = wtforms.StringField(_('Group description'))
886
886
887
887
888 time_choices = [(k, v['label'],) for k, v in h.time_deltas.items()]
888 time_choices = [(k, v['label'],) for k, v in h.time_deltas.items()]
889
889
890
890
891 class AuthTokenCreateForm(ReactorForm):
891 class AuthTokenCreateForm(ReactorForm):
892 description = wtforms.StringField(_('Token description'))
892 description = wtforms.StringField(_('Token description'))
893 expires = wtforms.SelectField('Expires',
893 expires = wtforms.SelectField('Expires',
894 coerce=lambda x: x,
894 coerce=lambda x: x,
895 choices=time_choices,
895 choices=time_choices,
896 validators=[wtforms.validators.Optional()])
896 validators=[wtforms.validators.Optional()])
@@ -1,135 +1,135 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import json
17 import json
18
18
19 from pyramid.security import unauthenticated_userid
19 from pyramid.security import unauthenticated_userid
20
20
21 import appenlight.lib.helpers as helpers
21 import appenlight.lib.helpers as helpers
22
22
23 from authomatic.providers import oauth2, oauth1
23 from authomatic.providers import oauth2, oauth1
24 from authomatic import Authomatic
24 from authomatic import Authomatic
25 from appenlight.models.user import User
25 from ziggurat_foundations.models.services.user import UserService
26
26
27
27
28 class CSRFException(Exception):
28 class CSRFException(Exception):
29 pass
29 pass
30
30
31
31
32 class JSONException(Exception):
32 class JSONException(Exception):
33 pass
33 pass
34
34
35
35
36 def get_csrf_token(request):
36 def get_csrf_token(request):
37 return request.session.get_csrf_token()
37 return request.session.get_csrf_token()
38
38
39
39
40 def safe_json_body(request):
40 def safe_json_body(request):
41 """
41 """
42 Returns None if json body is missing or erroneous
42 Returns None if json body is missing or erroneous
43 """
43 """
44 try:
44 try:
45 return request.json_body
45 return request.json_body
46 except ValueError:
46 except ValueError:
47 return None
47 return None
48
48
49
49
50 def unsafe_json_body(request):
50 def unsafe_json_body(request):
51 """
51 """
52 Throws JSONException if json can't deserialize
52 Throws JSONException if json can't deserialize
53 """
53 """
54 try:
54 try:
55 return request.json_body
55 return request.json_body
56 except ValueError:
56 except ValueError:
57 raise JSONException('Incorrect JSON')
57 raise JSONException('Incorrect JSON')
58
58
59
59
60 def get_user(request):
60 def get_user(request):
61 if not request.path_info.startswith('/static'):
61 if not request.path_info.startswith('/static'):
62 user_id = unauthenticated_userid(request)
62 user_id = unauthenticated_userid(request)
63 try:
63 try:
64 user_id = int(user_id)
64 user_id = int(user_id)
65 except Exception:
65 except Exception:
66 return None
66 return None
67
67
68 if user_id:
68 if user_id:
69 user = User.by_id(user_id)
69 user = UserService.by_id(user_id)
70 if user:
70 if user:
71 request.environ['appenlight.username'] = '%d:%s' % (
71 request.environ['appenlight.username'] = '%d:%s' % (
72 user_id, user.user_name)
72 user_id, user.user_name)
73 return user
73 return user
74 else:
74 else:
75 return None
75 return None
76
76
77
77
78 def es_conn(request):
78 def es_conn(request):
79 return request.registry.es_conn
79 return request.registry.es_conn
80
80
81
81
82 def add_flash_to_headers(request, clear=True):
82 def add_flash_to_headers(request, clear=True):
83 """
83 """
84 Adds pending flash messages to response, if clear is true clears out the
84 Adds pending flash messages to response, if clear is true clears out the
85 flash queue
85 flash queue
86 """
86 """
87 flash_msgs = helpers.get_type_formatted_flash(request)
87 flash_msgs = helpers.get_type_formatted_flash(request)
88 request.response.headers['x-flash-messages'] = json.dumps(flash_msgs)
88 request.response.headers['x-flash-messages'] = json.dumps(flash_msgs)
89 helpers.clear_flash(request)
89 helpers.clear_flash(request)
90
90
91
91
92 def get_authomatic(request):
92 def get_authomatic(request):
93 settings = request.registry.settings
93 settings = request.registry.settings
94 # authomatic social auth
94 # authomatic social auth
95 authomatic_conf = {
95 authomatic_conf = {
96 # callback http://yourapp.com/social_auth/twitter
96 # callback http://yourapp.com/social_auth/twitter
97 'twitter': {
97 'twitter': {
98 'class_': oauth1.Twitter,
98 'class_': oauth1.Twitter,
99 'consumer_key': settings.get('authomatic.pr.twitter.key', ''),
99 'consumer_key': settings.get('authomatic.pr.twitter.key', ''),
100 'consumer_secret': settings.get('authomatic.pr.twitter.secret',
100 'consumer_secret': settings.get('authomatic.pr.twitter.secret',
101 ''),
101 ''),
102 },
102 },
103 # callback http://yourapp.com/social_auth/facebook
103 # callback http://yourapp.com/social_auth/facebook
104 'facebook': {
104 'facebook': {
105 'class_': oauth2.Facebook,
105 'class_': oauth2.Facebook,
106 'consumer_key': settings.get('authomatic.pr.facebook.app_id', ''),
106 'consumer_key': settings.get('authomatic.pr.facebook.app_id', ''),
107 'consumer_secret': settings.get('authomatic.pr.facebook.secret',
107 'consumer_secret': settings.get('authomatic.pr.facebook.secret',
108 ''),
108 ''),
109 'scope': ['email'],
109 'scope': ['email'],
110 },
110 },
111 # callback http://yourapp.com/social_auth/google
111 # callback http://yourapp.com/social_auth/google
112 'google': {
112 'google': {
113 'class_': oauth2.Google,
113 'class_': oauth2.Google,
114 'consumer_key': settings.get('authomatic.pr.google.key', ''),
114 'consumer_key': settings.get('authomatic.pr.google.key', ''),
115 'consumer_secret': settings.get(
115 'consumer_secret': settings.get(
116 'authomatic.pr.google.secret', ''),
116 'authomatic.pr.google.secret', ''),
117 'scope': ['profile', 'email'],
117 'scope': ['profile', 'email'],
118 },
118 },
119 'github': {
119 'github': {
120 'class_': oauth2.GitHub,
120 'class_': oauth2.GitHub,
121 'consumer_key': settings.get('authomatic.pr.github.key', ''),
121 'consumer_key': settings.get('authomatic.pr.github.key', ''),
122 'consumer_secret': settings.get(
122 'consumer_secret': settings.get(
123 'authomatic.pr.github.secret', ''),
123 'authomatic.pr.github.secret', ''),
124 'scope': ['repo', 'public_repo', 'user:email'],
124 'scope': ['repo', 'public_repo', 'user:email'],
125 'access_headers': {'User-Agent': 'AppEnlight'},
125 'access_headers': {'User-Agent': 'AppEnlight'},
126 },
126 },
127 'bitbucket': {
127 'bitbucket': {
128 'class_': oauth1.Bitbucket,
128 'class_': oauth1.Bitbucket,
129 'consumer_key': settings.get('authomatic.pr.bitbucket.key', ''),
129 'consumer_key': settings.get('authomatic.pr.bitbucket.key', ''),
130 'consumer_secret': settings.get(
130 'consumer_secret': settings.get(
131 'authomatic.pr.bitbucket.secret', '')
131 'authomatic.pr.bitbucket.secret', '')
132 }
132 }
133 }
133 }
134 return Authomatic(
134 return Authomatic(
135 config=authomatic_conf, secret=settings['authomatic.secret'])
135 config=authomatic_conf, secret=settings['authomatic.secret'])
@@ -1,490 +1,491 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 """
17 """
18 Utility functions.
18 Utility functions.
19 """
19 """
20 import logging
20 import logging
21 import requests
21 import requests
22 import hashlib
22 import hashlib
23 import json
23 import json
24 import copy
24 import copy
25 import uuid
25 import uuid
26 import appenlight.lib.helpers as h
26 import appenlight.lib.helpers as h
27 from collections import namedtuple
27 from collections import namedtuple
28 from datetime import timedelta, datetime, date
28 from datetime import timedelta, datetime, date
29 from dogpile.cache.api import NO_VALUE
29 from dogpile.cache.api import NO_VALUE
30 from appenlight.models import Datastores
30 from appenlight.models import Datastores
31 from appenlight.validators import (LogSearchSchema,
31 from appenlight.validators import (LogSearchSchema,
32 TagListSchema,
32 TagListSchema,
33 accepted_search_params)
33 accepted_search_params)
34 from itsdangerous import TimestampSigner
34 from itsdangerous import TimestampSigner
35 from ziggurat_foundations.permissions import ALL_PERMISSIONS
35 from ziggurat_foundations.permissions import ALL_PERMISSIONS
36 from ziggurat_foundations.models.services.user import UserService
36 from dateutil.relativedelta import relativedelta
37 from dateutil.relativedelta import relativedelta
37 from dateutil.rrule import rrule, MONTHLY, DAILY
38 from dateutil.rrule import rrule, MONTHLY, DAILY
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41
42
42 Stat = namedtuple('Stat', 'start_interval value')
43 Stat = namedtuple('Stat', 'start_interval value')
43
44
44
45
45 def default_extractor(item):
46 def default_extractor(item):
46 """
47 """
47 :param item - item to extract date from
48 :param item - item to extract date from
48 """
49 """
49 if hasattr(item, 'start_interval'):
50 if hasattr(item, 'start_interval'):
50 return item.start_interval
51 return item.start_interval
51 return item['start_interval']
52 return item['start_interval']
52
53
53
54
54 # fast gap generator
55 # fast gap generator
55 def gap_gen_default(start, step, itemiterator, end_time=None,
56 def gap_gen_default(start, step, itemiterator, end_time=None,
56 iv_extractor=None):
57 iv_extractor=None):
57 """ generates a list of time/value items based on step and itemiterator
58 """ generates a list of time/value items based on step and itemiterator
58 if there are entries missing from iterator time/None will be returned
59 if there are entries missing from iterator time/None will be returned
59 instead
60 instead
60 :param start - datetime - what time should we start generating our values
61 :param start - datetime - what time should we start generating our values
61 :param step - timedelta - stepsize
62 :param step - timedelta - stepsize
62 :param itemiterator - iterable - we will check this iterable for values
63 :param itemiterator - iterable - we will check this iterable for values
63 corresponding to generated steps
64 corresponding to generated steps
64 :param end_time - datetime - when last step is >= end_time stop iterating
65 :param end_time - datetime - when last step is >= end_time stop iterating
65 :param iv_extractor - extracts current step from iterable items
66 :param iv_extractor - extracts current step from iterable items
66 """
67 """
67
68
68 if not iv_extractor:
69 if not iv_extractor:
69 iv_extractor = default_extractor
70 iv_extractor = default_extractor
70
71
71 next_step = start
72 next_step = start
72 minutes = step.total_seconds() / 60.0
73 minutes = step.total_seconds() / 60.0
73 while next_step.minute % minutes != 0:
74 while next_step.minute % minutes != 0:
74 next_step = next_step.replace(minute=next_step.minute - 1)
75 next_step = next_step.replace(minute=next_step.minute - 1)
75 for item in itemiterator:
76 for item in itemiterator:
76 item_start_interval = iv_extractor(item)
77 item_start_interval = iv_extractor(item)
77 # do we have a match for current time step in our data?
78 # do we have a match for current time step in our data?
78 # no gen a new tuple with 0 values
79 # no gen a new tuple with 0 values
79 while next_step < item_start_interval:
80 while next_step < item_start_interval:
80 yield Stat(next_step, None)
81 yield Stat(next_step, None)
81 next_step = next_step + step
82 next_step = next_step + step
82 if next_step == item_start_interval:
83 if next_step == item_start_interval:
83 yield Stat(item_start_interval, item)
84 yield Stat(item_start_interval, item)
84 next_step = next_step + step
85 next_step = next_step + step
85 if end_time:
86 if end_time:
86 while next_step < end_time:
87 while next_step < end_time:
87 yield Stat(next_step, None)
88 yield Stat(next_step, None)
88 next_step = next_step + step
89 next_step = next_step + step
89
90
90
91
91 class DateTimeEncoder(json.JSONEncoder):
92 class DateTimeEncoder(json.JSONEncoder):
92 """ Simple datetime to ISO encoder for json serialization"""
93 """ Simple datetime to ISO encoder for json serialization"""
93
94
94 def default(self, obj):
95 def default(self, obj):
95 if isinstance(obj, date):
96 if isinstance(obj, date):
96 return obj.isoformat()
97 return obj.isoformat()
97 if isinstance(obj, datetime):
98 if isinstance(obj, datetime):
98 return obj.isoformat()
99 return obj.isoformat()
99 return json.JSONEncoder.default(self, obj)
100 return json.JSONEncoder.default(self, obj)
100
101
101
102
102 def channelstream_request(secret, endpoint, payload, throw_exceptions=False,
103 def channelstream_request(secret, endpoint, payload, throw_exceptions=False,
103 servers=None):
104 servers=None):
104 responses = []
105 responses = []
105 if not servers:
106 if not servers:
106 servers = []
107 servers = []
107
108
108 signer = TimestampSigner(secret)
109 signer = TimestampSigner(secret)
109 sig_for_server = signer.sign(endpoint)
110 sig_for_server = signer.sign(endpoint)
110 for secret, server in [(s['secret'], s['server']) for s in servers]:
111 for secret, server in [(s['secret'], s['server']) for s in servers]:
111 response = {}
112 response = {}
112 secret_headers = {'x-channelstream-secret': sig_for_server,
113 secret_headers = {'x-channelstream-secret': sig_for_server,
113 'x-channelstream-endpoint': endpoint,
114 'x-channelstream-endpoint': endpoint,
114 'Content-Type': 'application/json'}
115 'Content-Type': 'application/json'}
115 url = '%s%s' % (server, endpoint)
116 url = '%s%s' % (server, endpoint)
116 try:
117 try:
117 response = requests.post(url,
118 response = requests.post(url,
118 data=json.dumps(payload,
119 data=json.dumps(payload,
119 cls=DateTimeEncoder),
120 cls=DateTimeEncoder),
120 headers=secret_headers,
121 headers=secret_headers,
121 verify=False,
122 verify=False,
122 timeout=2).json()
123 timeout=2).json()
123 except requests.exceptions.RequestException as e:
124 except requests.exceptions.RequestException as e:
124 if throw_exceptions:
125 if throw_exceptions:
125 raise
126 raise
126 responses.append(response)
127 responses.append(response)
127 return responses
128 return responses
128
129
129
130
130 def add_cors_headers(response):
131 def add_cors_headers(response):
131 # allow CORS
132 # allow CORS
132 response.headers.add('Access-Control-Allow-Origin', '*')
133 response.headers.add('Access-Control-Allow-Origin', '*')
133 response.headers.add('XDomainRequestAllowed', '1')
134 response.headers.add('XDomainRequestAllowed', '1')
134 response.headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
135 response.headers.add('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
135 # response.headers.add('Access-Control-Allow-Credentials', 'true')
136 # response.headers.add('Access-Control-Allow-Credentials', 'true')
136 response.headers.add('Access-Control-Allow-Headers',
137 response.headers.add('Access-Control-Allow-Headers',
137 'Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Pragma, Origin, Connection, Referer, Cookie')
138 'Content-Type, Depth, User-Agent, X-File-Size, X-Requested-With, If-Modified-Since, X-File-Name, Cache-Control, Pragma, Origin, Connection, Referer, Cookie')
138 response.headers.add('Access-Control-Max-Age', '86400')
139 response.headers.add('Access-Control-Max-Age', '86400')
139
140
140
141
141 from sqlalchemy.sql import compiler
142 from sqlalchemy.sql import compiler
142 from psycopg2.extensions import adapt as sqlescape
143 from psycopg2.extensions import adapt as sqlescape
143
144
144
145
145 # or use the appropiate escape function from your db driver
146 # or use the appropiate escape function from your db driver
146
147
147 def compile_query(query):
148 def compile_query(query):
148 dialect = query.session.bind.dialect
149 dialect = query.session.bind.dialect
149 statement = query.statement
150 statement = query.statement
150 comp = compiler.SQLCompiler(dialect, statement)
151 comp = compiler.SQLCompiler(dialect, statement)
151 comp.compile()
152 comp.compile()
152 enc = dialect.encoding
153 enc = dialect.encoding
153 params = {}
154 params = {}
154 for k, v in comp.params.items():
155 for k, v in comp.params.items():
155 if isinstance(v, str):
156 if isinstance(v, str):
156 v = v.encode(enc)
157 v = v.encode(enc)
157 params[k] = sqlescape(v)
158 params[k] = sqlescape(v)
158 return (comp.string.encode(enc) % params).decode(enc)
159 return (comp.string.encode(enc) % params).decode(enc)
159
160
160
161
161 def convert_es_type(input_data):
162 def convert_es_type(input_data):
162 """
163 """
163 This might need to convert some text or other types to corresponding ES types
164 This might need to convert some text or other types to corresponding ES types
164 """
165 """
165 return str(input_data)
166 return str(input_data)
166
167
167
168
168 ProtoVersion = namedtuple('ProtoVersion', ['major', 'minor', 'patch'])
169 ProtoVersion = namedtuple('ProtoVersion', ['major', 'minor', 'patch'])
169
170
170
171
171 def parse_proto(input_data):
172 def parse_proto(input_data):
172 try:
173 try:
173 parts = [int(x) for x in input_data.split('.')]
174 parts = [int(x) for x in input_data.split('.')]
174 while len(parts) < 3:
175 while len(parts) < 3:
175 parts.append(0)
176 parts.append(0)
176 return ProtoVersion(*parts)
177 return ProtoVersion(*parts)
177 except Exception as e:
178 except Exception as e:
178 log.info('Unknown protocol version: %s' % e)
179 log.info('Unknown protocol version: %s' % e)
179 return ProtoVersion(99, 99, 99)
180 return ProtoVersion(99, 99, 99)
180
181
181
182
182 def es_index_name_limiter(start_date=None, end_date=None, months_in_past=6,
183 def es_index_name_limiter(start_date=None, end_date=None, months_in_past=6,
183 ixtypes=None):
184 ixtypes=None):
184 """
185 """
185 This function limits the search to 6 months by default so we don't have to
186 This function limits the search to 6 months by default so we don't have to
186 query 300 elasticsearch indices for 20 years of historical data for example
187 query 300 elasticsearch indices for 20 years of historical data for example
187 """
188 """
188
189
189 # should be cached later
190 # should be cached later
190 def get_possible_names():
191 def get_possible_names():
191 return list(Datastores.es.aliases().keys())
192 return list(Datastores.es.aliases().keys())
192
193
193 possible_names = get_possible_names()
194 possible_names = get_possible_names()
194 es_index_types = []
195 es_index_types = []
195 if not ixtypes:
196 if not ixtypes:
196 ixtypes = ['reports', 'metrics', 'logs']
197 ixtypes = ['reports', 'metrics', 'logs']
197 for t in ixtypes:
198 for t in ixtypes:
198 if t == 'reports':
199 if t == 'reports':
199 es_index_types.append('rcae_r_%s')
200 es_index_types.append('rcae_r_%s')
200 elif t == 'logs':
201 elif t == 'logs':
201 es_index_types.append('rcae_l_%s')
202 es_index_types.append('rcae_l_%s')
202 elif t == 'metrics':
203 elif t == 'metrics':
203 es_index_types.append('rcae_m_%s')
204 es_index_types.append('rcae_m_%s')
204 elif t == 'uptime':
205 elif t == 'uptime':
205 es_index_types.append('rcae_u_%s')
206 es_index_types.append('rcae_u_%s')
206 elif t == 'slow_calls':
207 elif t == 'slow_calls':
207 es_index_types.append('rcae_sc_%s')
208 es_index_types.append('rcae_sc_%s')
208
209
209 if start_date:
210 if start_date:
210 start_date = copy.copy(start_date)
211 start_date = copy.copy(start_date)
211 else:
212 else:
212 if not end_date:
213 if not end_date:
213 end_date = datetime.utcnow()
214 end_date = datetime.utcnow()
214 start_date = end_date + relativedelta(months=months_in_past * -1)
215 start_date = end_date + relativedelta(months=months_in_past * -1)
215
216
216 if not end_date:
217 if not end_date:
217 end_date = start_date + relativedelta(months=months_in_past)
218 end_date = start_date + relativedelta(months=months_in_past)
218
219
219 index_dates = list(rrule(MONTHLY,
220 index_dates = list(rrule(MONTHLY,
220 dtstart=start_date.date().replace(day=1),
221 dtstart=start_date.date().replace(day=1),
221 until=end_date.date(),
222 until=end_date.date(),
222 count=36))
223 count=36))
223 index_names = []
224 index_names = []
224 for ix_type in es_index_types:
225 for ix_type in es_index_types:
225 to_extend = [ix_type % d.strftime('%Y_%m') for d in index_dates
226 to_extend = [ix_type % d.strftime('%Y_%m') for d in index_dates
226 if ix_type % d.strftime('%Y_%m') in possible_names]
227 if ix_type % d.strftime('%Y_%m') in possible_names]
227 index_names.extend(to_extend)
228 index_names.extend(to_extend)
228 for day in list(rrule(DAILY, dtstart=start_date.date(),
229 for day in list(rrule(DAILY, dtstart=start_date.date(),
229 until=end_date.date(), count=366)):
230 until=end_date.date(), count=366)):
230 ix_name = ix_type % day.strftime('%Y_%m_%d')
231 ix_name = ix_type % day.strftime('%Y_%m_%d')
231 if ix_name in possible_names:
232 if ix_name in possible_names:
232 index_names.append(ix_name)
233 index_names.append(ix_name)
233 return index_names
234 return index_names
234
235
235
236
236 def build_filter_settings_from_query_dict(
237 def build_filter_settings_from_query_dict(
237 request, params=None, override_app_ids=None,
238 request, params=None, override_app_ids=None,
238 resource_permissions=None):
239 resource_permissions=None):
239 """
240 """
240 Builds list of normalized search terms for ES from query params
241 Builds list of normalized search terms for ES from query params
241 ensuring application list is restricted to only applications user
242 ensuring application list is restricted to only applications user
242 has access to
243 has access to
243
244
244 :param params (dictionary)
245 :param params (dictionary)
245 :param override_app_ids - list of application id's to use instead of
246 :param override_app_ids - list of application id's to use instead of
246 applications user normally has access to
247 applications user normally has access to
247 """
248 """
248 params = copy.deepcopy(params)
249 params = copy.deepcopy(params)
249 applications = []
250 applications = []
250 if not resource_permissions:
251 if not resource_permissions:
251 resource_permissions = ['view']
252 resource_permissions = ['view']
252
253
253 if request.user:
254 if request.user:
254 applications = request.user.resources_with_perms(
255 applications = UserService.resources_with_perms(
255 resource_permissions, resource_types=['application'])
256 request.user, resource_permissions, resource_types=['application'])
256
257
257 # CRITICAL - this ensures our resultset is limited to only the ones
258 # CRITICAL - this ensures our resultset is limited to only the ones
258 # user has view permissions
259 # user has view permissions
259 all_possible_app_ids = set([app.resource_id for app in applications])
260 all_possible_app_ids = set([app.resource_id for app in applications])
260
261
261 # if override is preset we force permission for app to be present
262 # if override is preset we force permission for app to be present
262 # this allows users to see dashboards and applications they would
263 # this allows users to see dashboards and applications they would
263 # normally not be able to
264 # normally not be able to
264
265
265 if override_app_ids:
266 if override_app_ids:
266 all_possible_app_ids = set(override_app_ids)
267 all_possible_app_ids = set(override_app_ids)
267
268
268 schema = LogSearchSchema().bind(resources=all_possible_app_ids)
269 schema = LogSearchSchema().bind(resources=all_possible_app_ids)
269 tag_schema = TagListSchema()
270 tag_schema = TagListSchema()
270 filter_settings = schema.deserialize(params)
271 filter_settings = schema.deserialize(params)
271 tag_list = []
272 tag_list = []
272 for k, v in list(filter_settings.items()):
273 for k, v in list(filter_settings.items()):
273 if k in accepted_search_params:
274 if k in accepted_search_params:
274 continue
275 continue
275 tag_list.append({"name": k, "value": v, "op": 'eq'})
276 tag_list.append({"name": k, "value": v, "op": 'eq'})
276 # remove the key from filter_settings
277 # remove the key from filter_settings
277 filter_settings.pop(k, None)
278 filter_settings.pop(k, None)
278 tags = tag_schema.deserialize(tag_list)
279 tags = tag_schema.deserialize(tag_list)
279 filter_settings['tags'] = tags
280 filter_settings['tags'] = tags
280 return filter_settings
281 return filter_settings
281
282
282
283
283 def gen_uuid():
284 def gen_uuid():
284 return str(uuid.uuid4())
285 return str(uuid.uuid4())
285
286
286
287
287 def gen_uuid4_sha_hex():
288 def gen_uuid4_sha_hex():
288 return hashlib.sha1(uuid.uuid4().bytes).hexdigest()
289 return hashlib.sha1(uuid.uuid4().bytes).hexdigest()
289
290
290
291
291 def permission_tuple_to_dict(data):
292 def permission_tuple_to_dict(data):
292 out = {
293 out = {
293 "user_name": None,
294 "user_name": None,
294 "perm_name": data.perm_name,
295 "perm_name": data.perm_name,
295 "owner": data.owner,
296 "owner": data.owner,
296 "type": data.type,
297 "type": data.type,
297 "resource_name": None,
298 "resource_name": None,
298 "resource_type": None,
299 "resource_type": None,
299 "resource_id": None,
300 "resource_id": None,
300 "group_name": None,
301 "group_name": None,
301 "group_id": None
302 "group_id": None
302 }
303 }
303 if data.user:
304 if data.user:
304 out["user_name"] = data.user.user_name
305 out["user_name"] = data.user.user_name
305 if data.perm_name == ALL_PERMISSIONS:
306 if data.perm_name == ALL_PERMISSIONS:
306 out['perm_name'] = '__all_permissions__'
307 out['perm_name'] = '__all_permissions__'
307 if data.resource:
308 if data.resource:
308 out['resource_name'] = data.resource.resource_name
309 out['resource_name'] = data.resource.resource_name
309 out['resource_type'] = data.resource.resource_type
310 out['resource_type'] = data.resource.resource_type
310 out['resource_id'] = data.resource.resource_id
311 out['resource_id'] = data.resource.resource_id
311 if data.group:
312 if data.group:
312 out['group_name'] = data.group.group_name
313 out['group_name'] = data.group.group_name
313 out['group_id'] = data.group.id
314 out['group_id'] = data.group.id
314 return out
315 return out
315
316
316
317
317 def get_cached_buckets(request, stats_since, end_time, fn, cache_key,
318 def get_cached_buckets(request, stats_since, end_time, fn, cache_key,
318 gap_gen=None, db_session=None, step_interval=None,
319 gap_gen=None, db_session=None, step_interval=None,
319 iv_extractor=None,
320 iv_extractor=None,
320 rerange=False, *args, **kwargs):
321 rerange=False, *args, **kwargs):
321 """ Takes "fn" that should return some data and tries to load the data
322 """ Takes "fn" that should return some data and tries to load the data
322 dividing it into daily buckets - if the stats_since and end time give a
323 dividing it into daily buckets - if the stats_since and end time give a
323 delta bigger than 24hours, then only "todays" data is computed on the fly
324 delta bigger than 24hours, then only "todays" data is computed on the fly
324
325
325 :param request: (request) request object
326 :param request: (request) request object
326 :param stats_since: (datetime) start date of buckets range
327 :param stats_since: (datetime) start date of buckets range
327 :param end_time: (datetime) end date of buckets range - utcnow() if None
328 :param end_time: (datetime) end date of buckets range - utcnow() if None
328 :param fn: (callable) callable to use to populate buckets should have
329 :param fn: (callable) callable to use to populate buckets should have
329 following signature:
330 following signature:
330 def get_data(request, since_when, until, *args, **kwargs):
331 def get_data(request, since_when, until, *args, **kwargs):
331
332
332 :param cache_key: (string) cache key that will be used to build bucket
333 :param cache_key: (string) cache key that will be used to build bucket
333 caches
334 caches
334 :param gap_gen: (callable) gap generator - should return step intervals
335 :param gap_gen: (callable) gap generator - should return step intervals
335 to use with out `fn` callable
336 to use with out `fn` callable
336 :param db_session: (Session) sqlalchemy session
337 :param db_session: (Session) sqlalchemy session
337 :param step_interval: (timedelta) optional step interval if we want to
338 :param step_interval: (timedelta) optional step interval if we want to
338 override the default determined from total start/end time delta
339 override the default determined from total start/end time delta
339 :param iv_extractor: (callable) used to get step intervals from data
340 :param iv_extractor: (callable) used to get step intervals from data
340 returned by `fn` callable
341 returned by `fn` callable
341 :param rerange: (bool) handy if we want to change ranges from hours to
342 :param rerange: (bool) handy if we want to change ranges from hours to
342 days when cached data is missing - will shorten execution time if `fn`
343 days when cached data is missing - will shorten execution time if `fn`
343 callable supports that and we are working with multiple rows - like metrics
344 callable supports that and we are working with multiple rows - like metrics
344 :param args:
345 :param args:
345 :param kwargs:
346 :param kwargs:
346
347
347 :return: iterable
348 :return: iterable
348 """
349 """
349 if not end_time:
350 if not end_time:
350 end_time = datetime.utcnow().replace(second=0, microsecond=0)
351 end_time = datetime.utcnow().replace(second=0, microsecond=0)
351 delta = end_time - stats_since
352 delta = end_time - stats_since
352 # if smaller than 3 days we want to group by 5min else by 1h,
353 # if smaller than 3 days we want to group by 5min else by 1h,
353 # for 60 min group by min
354 # for 60 min group by min
354 if not gap_gen:
355 if not gap_gen:
355 gap_gen = gap_gen_default
356 gap_gen = gap_gen_default
356 if not iv_extractor:
357 if not iv_extractor:
357 iv_extractor = default_extractor
358 iv_extractor = default_extractor
358
359
359 # do not use custom interval if total time range with new iv would exceed
360 # do not use custom interval if total time range with new iv would exceed
360 # end time
361 # end time
361 if not step_interval or stats_since + step_interval >= end_time:
362 if not step_interval or stats_since + step_interval >= end_time:
362 if delta < h.time_deltas.get('12h')['delta']:
363 if delta < h.time_deltas.get('12h')['delta']:
363 step_interval = timedelta(seconds=60)
364 step_interval = timedelta(seconds=60)
364 elif delta < h.time_deltas.get('3d')['delta']:
365 elif delta < h.time_deltas.get('3d')['delta']:
365 step_interval = timedelta(seconds=60 * 5)
366 step_interval = timedelta(seconds=60 * 5)
366 elif delta > h.time_deltas.get('2w')['delta']:
367 elif delta > h.time_deltas.get('2w')['delta']:
367 step_interval = timedelta(days=1)
368 step_interval = timedelta(days=1)
368 else:
369 else:
369 step_interval = timedelta(minutes=60)
370 step_interval = timedelta(minutes=60)
370
371
371 if step_interval >= timedelta(minutes=60):
372 if step_interval >= timedelta(minutes=60):
372 log.info('cached_buckets:{}: adjusting start time '
373 log.info('cached_buckets:{}: adjusting start time '
373 'for hourly or daily intervals'.format(cache_key))
374 'for hourly or daily intervals'.format(cache_key))
374 stats_since = stats_since.replace(hour=0, minute=0)
375 stats_since = stats_since.replace(hour=0, minute=0)
375
376
376 ranges = [i.start_interval for i in list(gap_gen(stats_since,
377 ranges = [i.start_interval for i in list(gap_gen(stats_since,
377 step_interval, [],
378 step_interval, [],
378 end_time=end_time))]
379 end_time=end_time))]
379 buckets = {}
380 buckets = {}
380 storage_key = 'buckets:' + cache_key + '{}|{}'
381 storage_key = 'buckets:' + cache_key + '{}|{}'
381 # this means we basicly cache per hour in 3-14 day intervals but i think
382 # this means we basicly cache per hour in 3-14 day intervals but i think
382 # its fine at this point - will be faster than db access anyways
383 # its fine at this point - will be faster than db access anyways
383
384
384 if len(ranges) >= 1:
385 if len(ranges) >= 1:
385 last_ranges = [ranges[-1]]
386 last_ranges = [ranges[-1]]
386 else:
387 else:
387 last_ranges = []
388 last_ranges = []
388 if step_interval >= timedelta(minutes=60):
389 if step_interval >= timedelta(minutes=60):
389 for r in ranges:
390 for r in ranges:
390 k = storage_key.format(step_interval.total_seconds(), r)
391 k = storage_key.format(step_interval.total_seconds(), r)
391 value = request.registry.cache_regions.redis_day_30.get(k)
392 value = request.registry.cache_regions.redis_day_30.get(k)
392 # last buckets are never loaded from cache
393 # last buckets are never loaded from cache
393 is_last_result = (
394 is_last_result = (
394 r >= end_time - timedelta(hours=6) or r in last_ranges)
395 r >= end_time - timedelta(hours=6) or r in last_ranges)
395 if value is not NO_VALUE and not is_last_result:
396 if value is not NO_VALUE and not is_last_result:
396 log.info("cached_buckets:{}: "
397 log.info("cached_buckets:{}: "
397 "loading range {} from cache".format(cache_key, r))
398 "loading range {} from cache".format(cache_key, r))
398 buckets[r] = value
399 buckets[r] = value
399 else:
400 else:
400 log.info("cached_buckets:{}: "
401 log.info("cached_buckets:{}: "
401 "loading range {} from storage".format(cache_key, r))
402 "loading range {} from storage".format(cache_key, r))
402 range_size = step_interval
403 range_size = step_interval
403 if (step_interval == timedelta(minutes=60) and
404 if (step_interval == timedelta(minutes=60) and
404 not is_last_result and rerange):
405 not is_last_result and rerange):
405 range_size = timedelta(days=1)
406 range_size = timedelta(days=1)
406 r = r.replace(hour=0, minute=0)
407 r = r.replace(hour=0, minute=0)
407 log.info("cached_buckets:{}: "
408 log.info("cached_buckets:{}: "
408 "loading collapsed "
409 "loading collapsed "
409 "range {} {}".format(cache_key, r,
410 "range {} {}".format(cache_key, r,
410 r + range_size))
411 r + range_size))
411 bucket_data = fn(
412 bucket_data = fn(
412 request, r, r + range_size, step_interval,
413 request, r, r + range_size, step_interval,
413 gap_gen, bucket_count=len(ranges), *args, **kwargs)
414 gap_gen, bucket_count=len(ranges), *args, **kwargs)
414 for b in bucket_data:
415 for b in bucket_data:
415 b_iv = iv_extractor(b)
416 b_iv = iv_extractor(b)
416 buckets[b_iv] = b
417 buckets[b_iv] = b
417 k2 = storage_key.format(
418 k2 = storage_key.format(
418 step_interval.total_seconds(), b_iv)
419 step_interval.total_seconds(), b_iv)
419 request.registry.cache_regions.redis_day_30.set(k2, b)
420 request.registry.cache_regions.redis_day_30.set(k2, b)
420 log.info("cached_buckets:{}: saving cache".format(cache_key))
421 log.info("cached_buckets:{}: saving cache".format(cache_key))
421 else:
422 else:
422 # bucket count is 1 for short time ranges <= 24h from now
423 # bucket count is 1 for short time ranges <= 24h from now
423 bucket_data = fn(request, stats_since, end_time, step_interval,
424 bucket_data = fn(request, stats_since, end_time, step_interval,
424 gap_gen, bucket_count=1, *args, **kwargs)
425 gap_gen, bucket_count=1, *args, **kwargs)
425 for b in bucket_data:
426 for b in bucket_data:
426 buckets[iv_extractor(b)] = b
427 buckets[iv_extractor(b)] = b
427 return buckets
428 return buckets
428
429
429
430
430 def get_cached_split_data(request, stats_since, end_time, fn, cache_key,
431 def get_cached_split_data(request, stats_since, end_time, fn, cache_key,
431 db_session=None, *args, **kwargs):
432 db_session=None, *args, **kwargs):
432 """ Takes "fn" that should return some data and tries to load the data
433 """ Takes "fn" that should return some data and tries to load the data
433 dividing it into 2 buckets - cached "since_from" bucket and "today"
434 dividing it into 2 buckets - cached "since_from" bucket and "today"
434 bucket - then the data can be reduced into single value
435 bucket - then the data can be reduced into single value
435
436
436 Data is cached if the stats_since and end time give a delta bigger
437 Data is cached if the stats_since and end time give a delta bigger
437 than 24hours - then only 24h is computed on the fly
438 than 24hours - then only 24h is computed on the fly
438 """
439 """
439 if not end_time:
440 if not end_time:
440 end_time = datetime.utcnow().replace(second=0, microsecond=0)
441 end_time = datetime.utcnow().replace(second=0, microsecond=0)
441 delta = end_time - stats_since
442 delta = end_time - stats_since
442
443
443 if delta >= timedelta(minutes=60):
444 if delta >= timedelta(minutes=60):
444 log.info('cached_split_data:{}: adjusting start time '
445 log.info('cached_split_data:{}: adjusting start time '
445 'for hourly or daily intervals'.format(cache_key))
446 'for hourly or daily intervals'.format(cache_key))
446 stats_since = stats_since.replace(hour=0, minute=0)
447 stats_since = stats_since.replace(hour=0, minute=0)
447
448
448 storage_key = 'buckets_split_data:' + cache_key + ':{}|{}'
449 storage_key = 'buckets_split_data:' + cache_key + ':{}|{}'
449 old_end_time = end_time.replace(hour=0, minute=0)
450 old_end_time = end_time.replace(hour=0, minute=0)
450
451
451 final_storage_key = storage_key.format(delta.total_seconds(),
452 final_storage_key = storage_key.format(delta.total_seconds(),
452 old_end_time)
453 old_end_time)
453 older_data = None
454 older_data = None
454
455
455 cdata = request.registry.cache_regions.redis_day_7.get(
456 cdata = request.registry.cache_regions.redis_day_7.get(
456 final_storage_key)
457 final_storage_key)
457
458
458 if cdata:
459 if cdata:
459 log.info("cached_split_data:{}: found old "
460 log.info("cached_split_data:{}: found old "
460 "bucket data".format(cache_key))
461 "bucket data".format(cache_key))
461 older_data = cdata
462 older_data = cdata
462
463
463 if (stats_since < end_time - h.time_deltas.get('24h')['delta'] and
464 if (stats_since < end_time - h.time_deltas.get('24h')['delta'] and
464 not cdata):
465 not cdata):
465 log.info("cached_split_data:{}: didn't find the "
466 log.info("cached_split_data:{}: didn't find the "
466 "start bucket in cache so load older data".format(cache_key))
467 "start bucket in cache so load older data".format(cache_key))
467 recent_stats_since = old_end_time
468 recent_stats_since = old_end_time
468 older_data = fn(request, stats_since, recent_stats_since,
469 older_data = fn(request, stats_since, recent_stats_since,
469 db_session=db_session, *args, **kwargs)
470 db_session=db_session, *args, **kwargs)
470 request.registry.cache_regions.redis_day_7.set(final_storage_key,
471 request.registry.cache_regions.redis_day_7.set(final_storage_key,
471 older_data)
472 older_data)
472 elif stats_since < end_time - h.time_deltas.get('24h')['delta']:
473 elif stats_since < end_time - h.time_deltas.get('24h')['delta']:
473 recent_stats_since = old_end_time
474 recent_stats_since = old_end_time
474 else:
475 else:
475 recent_stats_since = stats_since
476 recent_stats_since = stats_since
476
477
477 log.info("cached_split_data:{}: loading fresh "
478 log.info("cached_split_data:{}: loading fresh "
478 "data bucksts from last 24h ".format(cache_key))
479 "data bucksts from last 24h ".format(cache_key))
479 todays_data = fn(request, recent_stats_since, end_time,
480 todays_data = fn(request, recent_stats_since, end_time,
480 db_session=db_session, *args, **kwargs)
481 db_session=db_session, *args, **kwargs)
481 return older_data, todays_data
482 return older_data, todays_data
482
483
483
484
484 def in_batches(seq, size):
485 def in_batches(seq, size):
485 """
486 """
486 Splits am iterable into batches of specified size
487 Splits am iterable into batches of specified size
487 :param seq (iterable)
488 :param seq (iterable)
488 :param size integer
489 :param size integer
489 """
490 """
490 return (seq[pos:pos + size] for pos in range(0, len(seq), size))
491 return (seq[pos:pos + size] for pos in range(0, len(seq), size))
@@ -1,79 +1,79 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import sqlalchemy as sa
17 import sqlalchemy as sa
18
18
19 from appenlight.models.resource import Resource
19 from ziggurat_foundations.models.services.resource import ResourceService
20 from appenlight.models import Base, get_db_session
20 from appenlight.models import Base, get_db_session
21 from sqlalchemy.orm import validates
21 from sqlalchemy.orm import validates
22 from ziggurat_foundations.models.base import BaseModel
22 from ziggurat_foundations.models.base import BaseModel
23
23
24
24
25 class AlertChannelAction(Base, BaseModel):
25 class AlertChannelAction(Base, BaseModel):
26 """
26 """
27 Stores notifications conditions for user's alert channels
27 Stores notifications conditions for user's alert channels
28 This is later used for rule parsing like "alert if http_status == 500"
28 This is later used for rule parsing like "alert if http_status == 500"
29 """
29 """
30 __tablename__ = 'alert_channels_actions'
30 __tablename__ = 'alert_channels_actions'
31
31
32 types = ['report', 'chart']
32 types = ['report', 'chart']
33
33
34 owner_id = sa.Column(sa.Integer,
34 owner_id = sa.Column(sa.Integer,
35 sa.ForeignKey('users.id', onupdate='CASCADE',
35 sa.ForeignKey('users.id', onupdate='CASCADE',
36 ondelete='CASCADE'))
36 ondelete='CASCADE'))
37 resource_id = sa.Column(sa.Integer())
37 resource_id = sa.Column(sa.Integer())
38 action = sa.Column(sa.Unicode(10), nullable=False, default='always')
38 action = sa.Column(sa.Unicode(10), nullable=False, default='always')
39 type = sa.Column(sa.Unicode(10), nullable=False)
39 type = sa.Column(sa.Unicode(10), nullable=False)
40 other_id = sa.Column(sa.Unicode(40))
40 other_id = sa.Column(sa.Unicode(40))
41 pkey = sa.Column(sa.Integer(), nullable=False, primary_key=True)
41 pkey = sa.Column(sa.Integer(), nullable=False, primary_key=True)
42 rule = sa.Column(sa.dialects.postgresql.JSON,
42 rule = sa.Column(sa.dialects.postgresql.JSON,
43 nullable=False, default={'field': 'http_status',
43 nullable=False, default={'field': 'http_status',
44 "op": "ge", "value": "500"})
44 "op": "ge", "value": "500"})
45 config = sa.Column(sa.dialects.postgresql.JSON)
45 config = sa.Column(sa.dialects.postgresql.JSON)
46 name = sa.Column(sa.Unicode(255))
46 name = sa.Column(sa.Unicode(255))
47
47
48 @validates('notify_type')
48 @validates('notify_type')
49 def validate_email(self, key, notify_type):
49 def validate_email(self, key, notify_type):
50 assert notify_type in ['always', 'only_first']
50 assert notify_type in ['always', 'only_first']
51 return notify_type
51 return notify_type
52
52
53 def resource_name(self, db_session=None):
53 def resource_name(self, db_session=None):
54 db_session = get_db_session(db_session)
54 db_session = get_db_session(db_session)
55 if self.resource_id:
55 if self.resource_id:
56 return Resource.by_resource_id(self.resource_id,
56 return ResourceService.by_resource_id(
57 db_session=db_session).resource_name
57 self.resource_id, db_session=db_session).resource_name
58 else:
58 else:
59 return 'any resource'
59 return 'any resource'
60
60
61 def get_dict(self, exclude_keys=None, include_keys=None,
61 def get_dict(self, exclude_keys=None, include_keys=None,
62 extended_info=False):
62 extended_info=False):
63 """
63 """
64 Returns dictionary with required information that will be consumed by
64 Returns dictionary with required information that will be consumed by
65 angular
65 angular
66 """
66 """
67 instance_dict = super(AlertChannelAction, self).get_dict()
67 instance_dict = super(AlertChannelAction, self).get_dict()
68 exclude_keys_list = exclude_keys or []
68 exclude_keys_list = exclude_keys or []
69 include_keys_list = include_keys or []
69 include_keys_list = include_keys or []
70 if extended_info:
70 if extended_info:
71 instance_dict['channels'] = [
71 instance_dict['channels'] = [
72 c.get_dict(extended_info=False) for c in self.channels]
72 c.get_dict(extended_info=False) for c in self.channels]
73
73
74 d = {}
74 d = {}
75 for k in instance_dict.keys():
75 for k in instance_dict.keys():
76 if (k not in exclude_keys_list and
76 if (k not in exclude_keys_list and
77 (k in include_keys_list or not include_keys)):
77 (k in include_keys_list or not include_keys)):
78 d[k] = instance_dict[k]
78 d[k] = instance_dict[k]
79 return d
79 return d
@@ -1,165 +1,165 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import sqlalchemy as sa
17 import sqlalchemy as sa
18 import logging
18 import logging
19
19
20 from datetime import datetime
20 from datetime import datetime
21 from appenlight.models import Base, get_db_session
21 from appenlight.models import Base, get_db_session
22 from appenlight.models.services.report_stat import ReportStatService
22 from appenlight.models.services.report_stat import ReportStatService
23 from appenlight.models.resource import Resource
24 from appenlight.models.integrations import IntegrationException
23 from appenlight.models.integrations import IntegrationException
25 from pyramid.threadlocal import get_current_request
24 from pyramid.threadlocal import get_current_request
26 from sqlalchemy.dialects.postgresql import JSON
25 from sqlalchemy.dialects.postgresql import JSON
27 from ziggurat_foundations.models.base import BaseModel
26 from ziggurat_foundations.models.base import BaseModel
27 from ziggurat_foundations.models.services.resource import ResourceService
28
28
29 log = logging.getLogger(__name__)
29 log = logging.getLogger(__name__)
30
30
31
31
32 class Event(Base, BaseModel):
32 class Event(Base, BaseModel):
33 __tablename__ = 'events'
33 __tablename__ = 'events'
34
34
35 types = {'error_report_alert': 1,
35 types = {'error_report_alert': 1,
36 'slow_report_alert': 3,
36 'slow_report_alert': 3,
37 'comment': 5,
37 'comment': 5,
38 'assignment': 6,
38 'assignment': 6,
39 'uptime_alert': 7,
39 'uptime_alert': 7,
40 'chart_alert': 9}
40 'chart_alert': 9}
41
41
42 statuses = {'active': 1,
42 statuses = {'active': 1,
43 'closed': 0}
43 'closed': 0}
44
44
45 id = sa.Column(sa.Integer, primary_key=True)
45 id = sa.Column(sa.Integer, primary_key=True)
46 start_date = sa.Column(sa.DateTime, default=datetime.utcnow)
46 start_date = sa.Column(sa.DateTime, default=datetime.utcnow)
47 end_date = sa.Column(sa.DateTime)
47 end_date = sa.Column(sa.DateTime)
48 status = sa.Column(sa.Integer, default=1)
48 status = sa.Column(sa.Integer, default=1)
49 event_type = sa.Column(sa.Integer, default=1)
49 event_type = sa.Column(sa.Integer, default=1)
50 origin_user_id = sa.Column(sa.Integer(), sa.ForeignKey('users.id'),
50 origin_user_id = sa.Column(sa.Integer(), sa.ForeignKey('users.id'),
51 nullable=True)
51 nullable=True)
52 target_user_id = sa.Column(sa.Integer(), sa.ForeignKey('users.id'),
52 target_user_id = sa.Column(sa.Integer(), sa.ForeignKey('users.id'),
53 nullable=True)
53 nullable=True)
54 resource_id = sa.Column(sa.Integer(),
54 resource_id = sa.Column(sa.Integer(),
55 sa.ForeignKey('resources.resource_id'),
55 sa.ForeignKey('resources.resource_id'),
56 nullable=True)
56 nullable=True)
57 target_id = sa.Column(sa.Integer)
57 target_id = sa.Column(sa.Integer)
58 target_uuid = sa.Column(sa.Unicode(40))
58 target_uuid = sa.Column(sa.Unicode(40))
59 text = sa.Column(sa.UnicodeText())
59 text = sa.Column(sa.UnicodeText())
60 values = sa.Column(JSON(), nullable=False, default=None)
60 values = sa.Column(JSON(), nullable=False, default=None)
61
61
62 def __repr__(self):
62 def __repr__(self):
63 return '<Event %s, app:%s, %s>' % (self.unified_alert_name(),
63 return '<Event %s, app:%s, %s>' % (self.unified_alert_name(),
64 self.resource_id,
64 self.resource_id,
65 self.unified_alert_action())
65 self.unified_alert_action())
66
66
67 @property
67 @property
68 def reverse_types(self):
68 def reverse_types(self):
69 return dict([(v, k) for k, v in self.types.items()])
69 return dict([(v, k) for k, v in self.types.items()])
70
70
71 def unified_alert_name(self):
71 def unified_alert_name(self):
72 return self.reverse_types[self.event_type]
72 return self.reverse_types[self.event_type]
73
73
74 def unified_alert_action(self):
74 def unified_alert_action(self):
75 event_name = self.reverse_types[self.event_type]
75 event_name = self.reverse_types[self.event_type]
76 if self.status == Event.statuses['closed']:
76 if self.status == Event.statuses['closed']:
77 return "CLOSE"
77 return "CLOSE"
78 if self.status != Event.statuses['closed']:
78 if self.status != Event.statuses['closed']:
79 return "OPEN"
79 return "OPEN"
80 return event_name
80 return event_name
81
81
82 def send_alerts(self, request=None, resource=None, db_session=None):
82 def send_alerts(self, request=None, resource=None, db_session=None):
83 """" Sends alerts to applicable channels """
83 """" Sends alerts to applicable channels """
84 db_session = get_db_session(db_session)
84 db_session = get_db_session(db_session)
85 db_session.flush()
85 db_session.flush()
86 if not resource:
86 if not resource:
87 resource = Resource.by_resource_id(self.resource_id)
87 resource = ResourceService.by_resource_id(self.resource_id)
88 if not request:
88 if not request:
89 request = get_current_request()
89 request = get_current_request()
90 if not resource:
90 if not resource:
91 return
91 return
92 users = set([p.user for p in resource.users_for_perm('view')])
92 users = set([p.user for p in ResourceService.users_for_perm(resource, 'view')])
93 for user in users:
93 for user in users:
94 for channel in user.alert_channels:
94 for channel in user.alert_channels:
95 matches_resource = not channel.resources or resource in [r.resource_id for r in channel.resources]
95 matches_resource = not channel.resources or resource in [r.resource_id for r in channel.resources]
96 if (
96 if (
97 not channel.channel_validated or
97 not channel.channel_validated or
98 not channel.send_alerts or
98 not channel.send_alerts or
99 not matches_resource
99 not matches_resource
100 ):
100 ):
101 continue
101 continue
102 else:
102 else:
103 try:
103 try:
104 channel.notify_alert(resource=resource,
104 channel.notify_alert(resource=resource,
105 event=self,
105 event=self,
106 user=user,
106 user=user,
107 request=request)
107 request=request)
108 except IntegrationException as e:
108 except IntegrationException as e:
109 log.warning('%s' % e)
109 log.warning('%s' % e)
110
110
111 def validate_or_close(self, since_when, db_session=None):
111 def validate_or_close(self, since_when, db_session=None):
112 """ Checks if alerts should stay open or it's time to close them.
112 """ Checks if alerts should stay open or it's time to close them.
113 Generates close alert event if alerts get closed """
113 Generates close alert event if alerts get closed """
114 event_types = [Event.types['error_report_alert'],
114 event_types = [Event.types['error_report_alert'],
115 Event.types['slow_report_alert']]
115 Event.types['slow_report_alert']]
116 app = Resource.by_resource_id(self.resource_id)
116 app = ResourceService.by_resource_id(self.resource_id)
117 # if app was deleted close instantly
117 # if app was deleted close instantly
118 if not app:
118 if not app:
119 self.close()
119 self.close()
120 return
120 return
121
121
122 if self.event_type in event_types:
122 if self.event_type in event_types:
123 total = ReportStatService.count_by_type(
123 total = ReportStatService.count_by_type(
124 self.event_type, self.resource_id, since_when)
124 self.event_type, self.resource_id, since_when)
125 if Event.types['error_report_alert'] == self.event_type:
125 if Event.types['error_report_alert'] == self.event_type:
126 threshold = app.error_report_threshold
126 threshold = app.error_report_threshold
127 if Event.types['slow_report_alert'] == self.event_type:
127 if Event.types['slow_report_alert'] == self.event_type:
128 threshold = app.slow_report_threshold
128 threshold = app.slow_report_threshold
129
129
130 if total < threshold:
130 if total < threshold:
131 self.close()
131 self.close()
132
132
133 def close(self, db_session=None):
133 def close(self, db_session=None):
134 """
134 """
135 Closes an event and sends notification to affected users
135 Closes an event and sends notification to affected users
136 """
136 """
137 self.end_date = datetime.utcnow()
137 self.end_date = datetime.utcnow()
138 self.status = Event.statuses['closed']
138 self.status = Event.statuses['closed']
139 log.warning('ALERT: CLOSE: %s' % self)
139 log.warning('ALERT: CLOSE: %s' % self)
140 self.send_alerts()
140 self.send_alerts()
141
141
142 def text_representation(self):
142 def text_representation(self):
143 alert_type = self.unified_alert_name()
143 alert_type = self.unified_alert_name()
144 text = ''
144 text = ''
145 if 'slow_report' in alert_type:
145 if 'slow_report' in alert_type:
146 text += 'Slow report alert'
146 text += 'Slow report alert'
147 if 'error_report' in alert_type:
147 if 'error_report' in alert_type:
148 text += 'Exception report alert'
148 text += 'Exception report alert'
149 if 'uptime_alert' in alert_type:
149 if 'uptime_alert' in alert_type:
150 text += 'Uptime alert'
150 text += 'Uptime alert'
151 if 'chart_alert' in alert_type:
151 if 'chart_alert' in alert_type:
152 text += 'Metrics value alert'
152 text += 'Metrics value alert'
153
153
154 alert_action = self.unified_alert_action()
154 alert_action = self.unified_alert_action()
155 if alert_action == 'OPEN':
155 if alert_action == 'OPEN':
156 text += ' got opened.'
156 text += ' got opened.'
157 if alert_action == 'CLOSE':
157 if alert_action == 'CLOSE':
158 text += ' got closed.'
158 text += ' got closed.'
159 return text
159 return text
160
160
161 def get_dict(self, request=None):
161 def get_dict(self, request=None):
162 dict_data = super(Event, self).get_dict()
162 dict_data = super(Event, self).get_dict()
163 dict_data['text'] = self.text_representation()
163 dict_data['text'] = self.text_representation()
164 dict_data['resource_name'] = self.resource.resource_name
164 dict_data['resource_name'] = self.resource.resource_name
165 return dict_data
165 return dict_data
@@ -1,83 +1,84 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import sqlalchemy as sa
17 import sqlalchemy as sa
18 from appenlight.models import Base
18 from appenlight.models import Base
19 from appenlight.lib.utils import permission_tuple_to_dict
19 from appenlight.lib.utils import permission_tuple_to_dict
20 from pyramid.security import Allow, ALL_PERMISSIONS
20 from pyramid.security import Allow, ALL_PERMISSIONS
21 from ziggurat_foundations.models.resource import ResourceMixin
21 from ziggurat_foundations.models.resource import ResourceMixin
22 from ziggurat_foundations.models.services.resource import ResourceService
22
23
23
24
24 class Resource(ResourceMixin, Base):
25 class Resource(ResourceMixin, Base):
25 events = sa.orm.relationship('Event',
26 events = sa.orm.relationship('Event',
26 lazy='dynamic',
27 lazy='dynamic',
27 backref='resource',
28 backref='resource',
28 passive_deletes=True,
29 passive_deletes=True,
29 passive_updates=True)
30 passive_updates=True)
30
31
31 @property
32 @property
32 def owner_user_name(self):
33 def owner_user_name(self):
33 if self.owner:
34 if self.owner:
34 return self.owner.user_name
35 return self.owner.user_name
35
36
36 @property
37 @property
37 def owner_group_name(self):
38 def owner_group_name(self):
38 if self.owner_group:
39 if self.owner_group:
39 return self.owner_group.group_name
40 return self.owner_group.group_name
40
41
41 def get_dict(self, exclude_keys=None, include_keys=None,
42 def get_dict(self, exclude_keys=None, include_keys=None,
42 include_perms=False, include_processing_rules=False):
43 include_perms=False, include_processing_rules=False):
43 result = super(Resource, self).get_dict(exclude_keys, include_keys)
44 result = super(Resource, self).get_dict(exclude_keys, include_keys)
44 result['possible_permissions'] = self.__possible_permissions__
45 result['possible_permissions'] = self.__possible_permissions__
45 if include_perms:
46 if include_perms:
46 result['current_permissions'] = self.user_permissions_list
47 result['current_permissions'] = self.user_permissions_list
47 else:
48 else:
48 result['current_permissions'] = []
49 result['current_permissions'] = []
49 if include_processing_rules:
50 if include_processing_rules:
50 result["postprocessing_rules"] = [rule.get_dict() for rule
51 result["postprocessing_rules"] = [rule.get_dict() for rule
51 in self.postprocess_conf]
52 in self.postprocess_conf]
52 else:
53 else:
53 result["postprocessing_rules"] = []
54 result["postprocessing_rules"] = []
54 exclude_keys_list = exclude_keys or []
55 exclude_keys_list = exclude_keys or []
55 include_keys_list = include_keys or []
56 include_keys_list = include_keys or []
56 d = {}
57 d = {}
57 for k in result.keys():
58 for k in result.keys():
58 if (k not in exclude_keys_list and
59 if (k not in exclude_keys_list and
59 (k in include_keys_list or not include_keys)):
60 (k in include_keys_list or not include_keys)):
60 d[k] = result[k]
61 d[k] = result[k]
61 for k in ['owner_user_name', 'owner_group_name']:
62 for k in ['owner_user_name', 'owner_group_name']:
62 if (k not in exclude_keys_list and
63 if (k not in exclude_keys_list and
63 (k in include_keys_list or not include_keys)):
64 (k in include_keys_list or not include_keys)):
64 d[k] = getattr(self, k)
65 d[k] = getattr(self, k)
65 return d
66 return d
66
67
67 @property
68 @property
68 def user_permissions_list(self):
69 def user_permissions_list(self):
69 return [permission_tuple_to_dict(perm) for perm in
70 return [permission_tuple_to_dict(perm) for perm in
70 self.users_for_perm('__any_permission__',
71 ResourceService.users_for_perm(
71 limit_group_permissions=True)]
72 self, '__any_permission__', limit_group_permissions=True)]
72
73
73 @property
74 @property
74 def __acl__(self):
75 def __acl__(self):
75 acls = []
76 acls = []
76
77
77 if self.owner_user_id:
78 if self.owner_user_id:
78 acls.extend([(Allow, self.owner_user_id, ALL_PERMISSIONS,), ])
79 acls.extend([(Allow, self.owner_user_id, ALL_PERMISSIONS,), ])
79
80
80 if self.owner_group_id:
81 if self.owner_group_id:
81 acls.extend([(Allow, "group:%s" % self.owner_group_id,
82 acls.extend([(Allow, "group:%s" % self.owner_group_id,
82 ALL_PERMISSIONS,), ])
83 ALL_PERMISSIONS,), ])
83 return acls
84 return acls
@@ -1,109 +1,109 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import sqlalchemy as sa
17 import sqlalchemy as sa
18 from pyramid.threadlocal import get_current_registry
18 from pyramid.threadlocal import get_current_registry
19 from paginate_sqlalchemy import SqlalchemyOrmPage
19 from paginate_sqlalchemy import SqlalchemyOrmPage
20 from ziggurat_foundations.models.services.user import UserService
21
20 from appenlight.models import get_db_session
22 from appenlight.models import get_db_session
21 from appenlight.models.event import Event
23 from appenlight.models.event import Event
22 from appenlight.models.services.base import BaseService
24 from appenlight.models.services.base import BaseService
23
25
24
26
25 class EventService(BaseService):
27 class EventService(BaseService):
26 @classmethod
28 @classmethod
27 def for_resource(cls, resource_ids, event_type=None, status=None,
29 def for_resource(cls, resource_ids, event_type=None, status=None,
28 since_when=None, limit=20, event_id=None,
30 since_when=None, limit=20, event_id=None,
29 target_uuid=None, order_by=None, or_target_user_id=None,
31 target_uuid=None, order_by=None, or_target_user_id=None,
30 db_session=None):
32 db_session=None):
31 """
33 """
32 Fetches events including based on passed params OR if target_user_id
34 Fetches events including based on passed params OR if target_user_id
33 is present include events that just target this user
35 is present include events that just target this user
34 """
36 """
35 db_session = get_db_session(db_session)
37 db_session = get_db_session(db_session)
36 query = db_session.query(Event)
38 query = db_session.query(Event)
37 query = query.options(sa.orm.joinedload(Event.resource))
39 query = query.options(sa.orm.joinedload(Event.resource))
38 and_cond = [Event.resource_id.in_(resource_ids)]
40 and_cond = [Event.resource_id.in_(resource_ids)]
39 if not resource_ids:
41 if not resource_ids:
40 and_cond = [Event.resource_id == -999]
42 and_cond = [Event.resource_id == -999]
41
43
42 if event_type:
44 if event_type:
43 and_cond.append(Event.event_type == event_type)
45 and_cond.append(Event.event_type == event_type)
44 if status:
46 if status:
45 and_cond.append(Event.status == status)
47 and_cond.append(Event.status == status)
46 if since_when:
48 if since_when:
47 and_cond.append(Event.start_date >= since_when)
49 and_cond.append(Event.start_date >= since_when)
48 if event_id:
50 if event_id:
49 and_cond.append(Event.id == event_id)
51 and_cond.append(Event.id == event_id)
50 if target_uuid:
52 if target_uuid:
51 and_cond.append(Event.target_uuid == target_uuid)
53 and_cond.append(Event.target_uuid == target_uuid)
52
54
53 or_cond = []
55 or_cond = []
54
56
55 if or_target_user_id:
57 if or_target_user_id:
56 or_cond.append(sa.or_(Event.target_user_id == or_target_user_id))
58 or_cond.append(sa.or_(Event.target_user_id == or_target_user_id))
57
59
58 query = query.filter(sa.or_(sa.and_(*and_cond),
60 query = query.filter(sa.or_(sa.and_(*and_cond),
59 *or_cond))
61 *or_cond))
60 if not order_by:
62 if not order_by:
61 query = query.order_by(sa.desc(Event.start_date))
63 query = query.order_by(sa.desc(Event.start_date))
62 if limit:
64 if limit:
63 query = query.limit(limit)
65 query = query.limit(limit)
64
66
65 return query
67 return query
66
68
67 @classmethod
69 @classmethod
68 def by_type_and_status(cls, event_types, status_types, since_when=None,
70 def by_type_and_status(cls, event_types, status_types, since_when=None,
69 older_than=None, db_session=None, app_ids=None):
71 older_than=None, db_session=None, app_ids=None):
70 db_session = get_db_session(db_session)
72 db_session = get_db_session(db_session)
71 query = db_session.query(Event)
73 query = db_session.query(Event)
72 query = query.filter(Event.event_type.in_(event_types))
74 query = query.filter(Event.event_type.in_(event_types))
73 query = query.filter(Event.status.in_(status_types))
75 query = query.filter(Event.status.in_(status_types))
74 if since_when:
76 if since_when:
75 query = query.filter(Event.start_date >= since_when)
77 query = query.filter(Event.start_date >= since_when)
76 if older_than:
78 if older_than:
77 query = query.filter(Event.start_date <= older_than)
79 query = query.filter(Event.start_date <= older_than)
78 if app_ids:
80 if app_ids:
79 query = query.filter(Event.resource_id.in_(app_ids))
81 query = query.filter(Event.resource_id.in_(app_ids))
80 return query
82 return query
81
83
82 @classmethod
84 @classmethod
83 def latest_for_user(cls, user, db_session=None):
85 def latest_for_user(cls, user, db_session=None):
84 registry = get_current_registry()
86 registry = get_current_registry()
85 resources = user.resources_with_perms(
87 resources = UserService.resources_with_perms(user, ['view'], resource_types=registry.resource_types)
86 ['view'], resource_types=registry.resource_types)
87 resource_ids = [r.resource_id for r in resources]
88 resource_ids = [r.resource_id for r in resources]
88 db_session = get_db_session(db_session)
89 db_session = get_db_session(db_session)
89 return EventService.for_resource(
90 return EventService.for_resource(
90 resource_ids, or_target_user_id=user.id, limit=10,
91 resource_ids, or_target_user_id=user.id, limit=10,
91 db_session=db_session)
92 db_session=db_session)
92
93
93 @classmethod
94 @classmethod
94 def get_paginator(cls, user, page=1, item_count=None, items_per_page=50,
95 def get_paginator(cls, user, page=1, item_count=None, items_per_page=50,
95 order_by=None, filter_settings=None, db_session=None):
96 order_by=None, filter_settings=None, db_session=None):
96 if not filter_settings:
97 if not filter_settings:
97 filter_settings = {}
98 filter_settings = {}
98 registry = get_current_registry()
99 registry = get_current_registry()
99 resources = user.resources_with_perms(
100 resources = UserService.resources_with_perms(user, ['view'], resource_types=registry.resource_types)
100 ['view'], resource_types=registry.resource_types)
101 resource_ids = [r.resource_id for r in resources]
101 resource_ids = [r.resource_id for r in resources]
102 query = EventService.for_resource(
102 query = EventService.for_resource(
103 resource_ids, or_target_user_id=user.id, limit=100,
103 resource_ids, or_target_user_id=user.id, limit=100,
104 db_session=db_session)
104 db_session=db_session)
105
105
106 paginator = SqlalchemyOrmPage(query, page=page,
106 paginator = SqlalchemyOrmPage(query, page=page,
107 items_per_page=items_per_page,
107 items_per_page=items_per_page,
108 **filter_settings)
108 **filter_settings)
109 return paginator
109 return paginator
@@ -1,27 +1,27 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 from appenlight.models import get_db_session
17 from appenlight.models import get_db_session
18 from appenlight.models.group import Group
18 from appenlight.models.group import Group
19 from appenlight.models.services.base import BaseService
19 from ziggurat_foundations.models.services.group import GroupService
20
20
21
21
22 class GroupService(BaseService):
22 class GroupService(GroupService):
23 @classmethod
23 @classmethod
24 def by_id(cls, group_id, db_session=None):
24 def by_id(cls, group_id, db_session=None):
25 db_session = get_db_session(db_session)
25 db_session = get_db_session(db_session)
26 query = db_session.query(Group).filter(Group.id == group_id)
26 query = db_session.query(Group).filter(Group.id == group_id)
27 return query.first()
27 return query.first()
@@ -1,33 +1,33 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 from appenlight.models.group_resource_permission import GroupResourcePermission
17 from appenlight.models.group_resource_permission import GroupResourcePermission
18 from appenlight.models import get_db_session
18 from appenlight.models import get_db_session
19 from appenlight.models.services.base import BaseService
19 from ziggurat_foundations.models.services.group_resource_permission import GroupResourcePermissionService
20
20
21
21
22 class GroupResourcePermissionService(BaseService):
22 class GroupResourcePermissionService(GroupResourcePermissionService):
23 @classmethod
23 @classmethod
24 def by_resource_group_and_perm(cls, group_id, perm_name, resource_id,
24 def by_resource_group_and_perm(cls, group_id, perm_name, resource_id,
25 db_session=None):
25 db_session=None):
26 """ return all instances by user name, perm name and resource id """
26 """ return all instances by user name, perm name and resource id """
27 db_session = get_db_session(db_session)
27 db_session = get_db_session(db_session)
28 query = db_session.query(GroupResourcePermission)
28 query = db_session.query(GroupResourcePermission)
29 query = query.filter(GroupResourcePermission.group_id == group_id)
29 query = query.filter(GroupResourcePermission.group_id == group_id)
30 query = query.filter(
30 query = query.filter(
31 GroupResourcePermission.resource_id == resource_id)
31 GroupResourcePermission.resource_id == resource_id)
32 query = query.filter(GroupResourcePermission.perm_name == perm_name)
32 query = query.filter(GroupResourcePermission.perm_name == perm_name)
33 return query.first()
33 return query.first()
@@ -1,151 +1,152 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import logging
17 import logging
18 import pyramid_mailer
18 import pyramid_mailer
19 import pyramid.renderers
19 import pyramid.renderers
20 import sqlalchemy as sa
20 import sqlalchemy as sa
21
21
22 from collections import namedtuple
22 from collections import namedtuple
23 from datetime import datetime
23 from datetime import datetime
24
24
25 from ziggurat_foundations.models.services.user import UserService
26
25 from appenlight.lib.rule import Rule
27 from appenlight.lib.rule import Rule
26 from appenlight.models import get_db_session
28 from appenlight.models import get_db_session
27 from appenlight.models.integrations import IntegrationException
29 from appenlight.models.integrations import IntegrationException
28 from appenlight.models.report import REPORT_TYPE_MATRIX
30 from appenlight.models.report import REPORT_TYPE_MATRIX
29 from appenlight.models.user import User
31 from appenlight.models.user import User
30 from appenlight.models.services.base import BaseService
31 from paginate_sqlalchemy import SqlalchemyOrmPage
32 from paginate_sqlalchemy import SqlalchemyOrmPage
32 from pyramid.threadlocal import get_current_registry
33 from pyramid.threadlocal import get_current_registry
33
34
34 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
35
36
36 GroupOccurence = namedtuple('GroupOccurence', ['occurences', 'group'])
37 GroupOccurence = namedtuple('GroupOccurence', ['occurences', 'group'])
37
38
38
39
39 class UserService(BaseService):
40 class UserService(UserService):
40 @classmethod
41 @classmethod
41 def all(cls, db_session=None):
42 def all(cls, db_session=None):
42 return get_db_session(db_session).query(User).order_by(User.user_name)
43 return get_db_session(db_session).query(User).order_by(User.user_name)
43
44
44 @classmethod
45 @classmethod
45 def send_email(cls, request, recipients, variables, template,
46 def send_email(cls, request, recipients, variables, template,
46 immediately=False, silent=False):
47 immediately=False, silent=False):
47 html = pyramid.renderers.render(template, variables, request)
48 html = pyramid.renderers.render(template, variables, request)
48 title = variables.get('email_title',
49 title = variables.get('email_title',
49 variables.get('title', "No Title"))
50 variables.get('title', "No Title"))
50 title = title.replace('\r', '').replace('\n', '')
51 title = title.replace('\r', '').replace('\n', '')
51 sender = "{} <{}>".format(
52 sender = "{} <{}>".format(
52 request.registry.settings['mailing.from_name'],
53 request.registry.settings['mailing.from_name'],
53 request.registry.settings['mailing.from_email'])
54 request.registry.settings['mailing.from_email'])
54 message = pyramid_mailer.message.Message(
55 message = pyramid_mailer.message.Message(
55 subject=title, sender=sender, recipients=recipients, html=html)
56 subject=title, sender=sender, recipients=recipients, html=html)
56 if immediately:
57 if immediately:
57 try:
58 try:
58 request.registry.mailer.send_immediately(message)
59 request.registry.mailer.send_immediately(message)
59 except Exception as e:
60 except Exception as e:
60 log.warning('Exception %s' % e)
61 log.warning('Exception %s' % e)
61 if not silent:
62 if not silent:
62 raise
63 raise
63 else:
64 else:
64 request.registry.mailer.send(message)
65 request.registry.mailer.send(message)
65
66
66 @classmethod
67 @classmethod
67 def get_paginator(cls, page=1, item_count=None, items_per_page=50,
68 def get_paginator(cls, page=1, item_count=None, items_per_page=50,
68 order_by=None, filter_settings=None,
69 order_by=None, filter_settings=None,
69 exclude_columns=None, db_session=None):
70 exclude_columns=None, db_session=None):
70 registry = get_current_registry()
71 registry = get_current_registry()
71 if not exclude_columns:
72 if not exclude_columns:
72 exclude_columns = []
73 exclude_columns = []
73 if not filter_settings:
74 if not filter_settings:
74 filter_settings = {}
75 filter_settings = {}
75 db_session = get_db_session(db_session)
76 db_session = get_db_session(db_session)
76 q = db_session.query(User)
77 q = db_session.query(User)
77 if filter_settings.get('order_col'):
78 if filter_settings.get('order_col'):
78 order_col = filter_settings.get('order_col')
79 order_col = filter_settings.get('order_col')
79 if filter_settings.get('order_dir') == 'dsc':
80 if filter_settings.get('order_dir') == 'dsc':
80 sort_on = 'desc'
81 sort_on = 'desc'
81 else:
82 else:
82 sort_on = 'asc'
83 sort_on = 'asc'
83 q = q.order_by(getattr(sa, sort_on)(getattr(User, order_col)))
84 q = q.order_by(getattr(sa, sort_on)(getattr(User, order_col)))
84 else:
85 else:
85 q = q.order_by(sa.desc(User.registered_date))
86 q = q.order_by(sa.desc(User.registered_date))
86 # remove urlgen or it never caches count
87 # remove urlgen or it never caches count
87 cache_params = dict(filter_settings)
88 cache_params = dict(filter_settings)
88 cache_params.pop('url', None)
89 cache_params.pop('url', None)
89 cache_params.pop('url_maker', None)
90 cache_params.pop('url_maker', None)
90
91
91 @registry.cache_regions.redis_min_5.cache_on_arguments()
92 @registry.cache_regions.redis_min_5.cache_on_arguments()
92 def estimate_users(cache_key):
93 def estimate_users(cache_key):
93 o_q = q.order_by(False)
94 o_q = q.order_by(False)
94 return o_q.count()
95 return o_q.count()
95
96
96 item_count = estimate_users(cache_params)
97 item_count = estimate_users(cache_params)
97 # if the number of pages is low we may want to invalidate the count to
98 # if the number of pages is low we may want to invalidate the count to
98 # provide 'real time' update - use case -
99 # provide 'real time' update - use case -
99 # errors just started to flow in
100 # errors just started to flow in
100 if item_count < 1000:
101 if item_count < 1000:
101 item_count = estimate_users.refresh(cache_params)
102 item_count = estimate_users.refresh(cache_params)
102 paginator = SqlalchemyOrmPage(q, page=page,
103 paginator = SqlalchemyOrmPage(q, page=page,
103 item_count=item_count,
104 item_count=item_count,
104 items_per_page=items_per_page,
105 items_per_page=items_per_page,
105 **filter_settings)
106 **filter_settings)
106 return paginator
107 return paginator
107
108
108 @classmethod
109 @classmethod
109 def get_valid_channels(cls, user):
110 def get_valid_channels(cls, user):
110 return [channel for channel in user.alert_channels
111 return [channel for channel in user.alert_channels
111 if channel.channel_validated]
112 if channel.channel_validated]
112
113
113 @classmethod
114 @classmethod
114 def report_notify(cls, user, request, application, report_groups,
115 def report_notify(cls, user, request, application, report_groups,
115 occurence_dict, db_session=None):
116 occurence_dict, db_session=None):
116 db_session = get_db_session(db_session)
117 db_session = get_db_session(db_session)
117 if not report_groups:
118 if not report_groups:
118 return True
119 return True
119 since_when = datetime.utcnow()
120 since_when = datetime.utcnow()
120 for channel in cls.get_valid_channels(user):
121 for channel in cls.get_valid_channels(user):
121 confirmed_groups = []
122 confirmed_groups = []
122
123
123 for group in report_groups:
124 for group in report_groups:
124 occurences = occurence_dict.get(group.id, 1)
125 occurences = occurence_dict.get(group.id, 1)
125 for action in channel.channel_actions:
126 for action in channel.channel_actions:
126 not_matched = (
127 not_matched = (
127 action.resource_id and action.resource_id !=
128 action.resource_id and action.resource_id !=
128 application.resource_id)
129 application.resource_id)
129 if action.type != 'report' or not_matched:
130 if action.type != 'report' or not_matched:
130 continue
131 continue
131 should_notify = (action.action == 'always' or
132 should_notify = (action.action == 'always' or
132 not group.notified)
133 not group.notified)
133 rule_obj = Rule(action.rule, REPORT_TYPE_MATRIX)
134 rule_obj = Rule(action.rule, REPORT_TYPE_MATRIX)
134 report_dict = group.get_report().get_dict(request)
135 report_dict = group.get_report().get_dict(request)
135 if rule_obj.match(report_dict) and should_notify:
136 if rule_obj.match(report_dict) and should_notify:
136 item = GroupOccurence(occurences, group)
137 item = GroupOccurence(occurences, group)
137 if item not in confirmed_groups:
138 if item not in confirmed_groups:
138 confirmed_groups.append(item)
139 confirmed_groups.append(item)
139
140
140 # send individual reports
141 # send individual reports
141 total_confirmed = len(confirmed_groups)
142 total_confirmed = len(confirmed_groups)
142 if not total_confirmed:
143 if not total_confirmed:
143 continue
144 continue
144 try:
145 try:
145 channel.notify_reports(resource=application,
146 channel.notify_reports(resource=application,
146 user=user,
147 user=user,
147 request=request,
148 request=request,
148 since_when=since_when,
149 since_when=since_when,
149 reports=confirmed_groups)
150 reports=confirmed_groups)
150 except IntegrationException as e:
151 except IntegrationException as e:
151 log.warning('%s' % e)
152 log.warning('%s' % e)
@@ -1,133 +1,133 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import logging
17 import logging
18 import sqlalchemy as sa
18 import sqlalchemy as sa
19 from datetime import datetime
19 from datetime import datetime
20 from appenlight.models import Base, get_db_session
20 from appenlight.models import Base, get_db_session
21 from appenlight.models.services.event import EventService
21 from appenlight.models.services.event import EventService
22 from appenlight.models.integrations import IntegrationException
22 from appenlight.models.integrations import IntegrationException
23 from pyramid.threadlocal import get_current_request
23 from pyramid.threadlocal import get_current_request
24 from ziggurat_foundations.models.user import UserMixin
24 from ziggurat_foundations.models.user import UserMixin
25 from ziggurat_foundations.models.services.user import UserService
25
26
26 log = logging.getLogger(__name__)
27 log = logging.getLogger(__name__)
27
28
28
29
29 class User(UserMixin, Base):
30 class User(UserMixin, Base):
30 __possible_permissions__ = []
31 __possible_permissions__ = []
31
32
32 first_name = sa.Column(sa.Unicode(25))
33 first_name = sa.Column(sa.Unicode(25))
33 last_name = sa.Column(sa.Unicode(25))
34 last_name = sa.Column(sa.Unicode(25))
34 company_name = sa.Column(sa.Unicode(255), default='')
35 company_name = sa.Column(sa.Unicode(255), default='')
35 company_address = sa.Column(sa.Unicode(255), default='')
36 company_address = sa.Column(sa.Unicode(255), default='')
36 zip_code = sa.Column(sa.Unicode(25), default='')
37 zip_code = sa.Column(sa.Unicode(25), default='')
37 city = sa.Column(sa.Unicode(50), default='')
38 city = sa.Column(sa.Unicode(50), default='')
38 default_report_sort = sa.Column(sa.Unicode(25), default='newest')
39 default_report_sort = sa.Column(sa.Unicode(25), default='newest')
39 notes = sa.Column(sa.UnicodeText, default='')
40 notes = sa.Column(sa.UnicodeText, default='')
40 notifications = sa.Column(sa.Boolean(), default=True)
41 notifications = sa.Column(sa.Boolean(), default=True)
41 registration_ip = sa.Column(sa.UnicodeText(), default='')
42 registration_ip = sa.Column(sa.UnicodeText(), default='')
42 alert_channels = sa.orm.relationship('AlertChannel',
43 alert_channels = sa.orm.relationship('AlertChannel',
43 cascade="all,delete-orphan",
44 cascade="all,delete-orphan",
44 passive_deletes=True,
45 passive_deletes=True,
45 passive_updates=True,
46 passive_updates=True,
46 backref='owner',
47 backref='owner',
47 order_by='AlertChannel.channel_name, '
48 order_by='AlertChannel.channel_name, '
48 'AlertChannel.channel_value')
49 'AlertChannel.channel_value')
49
50
50 alert_actions = sa.orm.relationship('AlertChannelAction',
51 alert_actions = sa.orm.relationship('AlertChannelAction',
51 cascade="all,delete-orphan",
52 cascade="all,delete-orphan",
52 passive_deletes=True,
53 passive_deletes=True,
53 passive_updates=True,
54 passive_updates=True,
54 backref='owner',
55 backref='owner',
55 order_by='AlertChannelAction.pkey')
56 order_by='AlertChannelAction.pkey')
56
57
57 auth_tokens = sa.orm.relationship('AuthToken',
58 auth_tokens = sa.orm.relationship('AuthToken',
58 cascade="all,delete-orphan",
59 cascade="all,delete-orphan",
59 passive_deletes=True,
60 passive_deletes=True,
60 passive_updates=True,
61 passive_updates=True,
61 backref='owner',
62 backref='owner',
62 order_by='AuthToken.creation_date')
63 order_by='AuthToken.creation_date')
63
64
64 def get_dict(self, exclude_keys=None, include_keys=None,
65 def get_dict(self, exclude_keys=None, include_keys=None,
65 extended_info=False):
66 extended_info=False):
66 result = super(User, self).get_dict(exclude_keys, include_keys)
67 result = super(User, self).get_dict(exclude_keys, include_keys)
67 if extended_info:
68 if extended_info:
68 result['groups'] = [g.group_name for g in self.groups]
69 result['groups'] = [g.group_name for g in self.groups]
69 result['permissions'] = [p.perm_name for p in self.permissions]
70 result['permissions'] = [p.perm_name for p in UserService.permissions(self)]
70 request = get_current_request()
71 request = get_current_request()
71 apps = self.resources_with_perms(
72 apps = UserService.resources_with_perms(self,
72 ['view'], resource_types=['application'])
73 ['view'], resource_types=['application'])
73 result['applications'] = sorted(
74 result['applications'] = sorted(
74 [{'resource_id': a.resource_id,
75 [{'resource_id': a.resource_id,
75 'resource_name': a.resource_name}
76 'resource_name': a.resource_name}
76 for a in apps.all()],
77 for a in apps.all()],
77 key=lambda x: x['resource_name'].lower())
78 key=lambda x: x['resource_name'].lower())
78 result['assigned_reports'] = [r.get_dict(request) for r
79 result['assigned_reports'] = [r.get_dict(request) for r
79 in self.assigned_report_groups]
80 in self.assigned_report_groups]
80 result['latest_events'] = [ev.get_dict(request) for ev
81 result['latest_events'] = [ev.get_dict(request) for ev
81 in self.latest_events()]
82 in self.latest_events()]
82
83
83 exclude_keys_list = exclude_keys or []
84 exclude_keys_list = exclude_keys or []
84 include_keys_list = include_keys or []
85 include_keys_list = include_keys or []
85 d = {}
86 d = {}
86 for k in result.keys():
87 for k in result.keys():
87 if (k not in exclude_keys_list and
88 if (k not in exclude_keys_list and
88 (k in include_keys_list or not include_keys)):
89 (k in include_keys_list or not include_keys)):
89 d[k] = result[k]
90 d[k] = result[k]
90 return d
91 return d
91
92
92 def __repr__(self):
93 def __repr__(self):
93 return '<User: %s, id: %s>' % (self.user_name, self.id)
94 return '<User: %s, id: %s>' % (self.user_name, self.id)
94
95
95 @property
96 @property
96 def assigned_report_groups(self):
97 def assigned_report_groups(self):
97 from appenlight.models.report_group import ReportGroup
98 from appenlight.models.report_group import ReportGroup
98
99
99 resources = self.resources_with_perms(
100 resources = UserService.resources_with_perms(self, ['view'], resource_types=['application'])
100 ['view'], resource_types=['application'])
101 query = self.assigned_reports_relation
101 query = self.assigned_reports_relation
102 rid_list = [r.resource_id for r in resources]
102 rid_list = [r.resource_id for r in resources]
103 query = query.filter(ReportGroup.resource_id.in_(rid_list))
103 query = query.filter(ReportGroup.resource_id.in_(rid_list))
104 query = query.limit(50)
104 query = query.limit(50)
105 return query
105 return query
106
106
107 def feed_report(self, report):
107 def feed_report(self, report):
108 """ """
108 """ """
109 if not hasattr(self, 'current_reports'):
109 if not hasattr(self, 'current_reports'):
110 self.current_reports = []
110 self.current_reports = []
111 self.current_reports.append(report)
111 self.current_reports.append(report)
112
112
113 def send_digest(self, request, application, reports, since_when=None,
113 def send_digest(self, request, application, reports, since_when=None,
114 db_session=None):
114 db_session=None):
115 db_session = get_db_session(db_session)
115 db_session = get_db_session(db_session)
116 if not reports:
116 if not reports:
117 return True
117 return True
118 if not since_when:
118 if not since_when:
119 since_when = datetime.utcnow()
119 since_when = datetime.utcnow()
120 for channel in self.alert_channels:
120 for channel in self.alert_channels:
121 if not channel.channel_validated or not channel.daily_digest:
121 if not channel.channel_validated or not channel.daily_digest:
122 continue
122 continue
123 try:
123 try:
124 channel.send_digest(resource=application,
124 channel.send_digest(resource=application,
125 user=self,
125 user=self,
126 request=request,
126 request=request,
127 since_when=since_when,
127 since_when=since_when,
128 reports=reports)
128 reports=reports)
129 except IntegrationException as e:
129 except IntegrationException as e:
130 log.warning('%s' % e)
130 log.warning('%s' % e)
131
131
132 def latest_events(self):
132 def latest_events(self):
133 return EventService.latest_for_user(self)
133 return EventService.latest_for_user(self)
@@ -1,153 +1,155 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import argparse
17 import argparse
18 import getpass
18 import getpass
19 import logging
19 import logging
20
20
21 from pyramid.paster import setup_logging, bootstrap
21 from pyramid.paster import setup_logging, bootstrap
22 from pyramid.threadlocal import get_current_request
22 from pyramid.threadlocal import get_current_request
23 from ziggurat_foundations.models.services.user import UserService
24
23
25
24 from appenlight.forms import UserRegisterForm
26 from appenlight.forms import UserRegisterForm
25 from appenlight.lib.ext_json import json
27 from appenlight.lib.ext_json import json
26 from appenlight.models import (
28 from appenlight.models import (
27 DBSession,
29 DBSession,
28 Group,
30 Group,
29 GroupPermission,
31 GroupPermission,
30 User,
32 User,
31 AuthToken
33 AuthToken
32 )
34 )
33 from appenlight.models.services.group import GroupService
35 from appenlight.models.services.group import GroupService
34
36
35 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
36
38
37 _ = str
39 _ = str
38
40
39
41
40 def is_yes(input_data):
42 def is_yes(input_data):
41 return input_data in ['y', 'yes']
43 return input_data in ['y', 'yes']
42
44
43
45
44 def is_no(input_data):
46 def is_no(input_data):
45 return input_data in ['n', 'no']
47 return input_data in ['n', 'no']
46
48
47
49
48 def main():
50 def main():
49 parser = argparse.ArgumentParser(
51 parser = argparse.ArgumentParser(
50 description='Populate AppEnlight database',
52 description='Populate AppEnlight database',
51 add_help=False)
53 add_help=False)
52 parser.add_argument('-c', '--config', required=True,
54 parser.add_argument('-c', '--config', required=True,
53 help='Configuration ini file of application')
55 help='Configuration ini file of application')
54 parser.add_argument('--username', default=None,
56 parser.add_argument('--username', default=None,
55 help='User to create')
57 help='User to create')
56 parser.add_argument('--password', default=None,
58 parser.add_argument('--password', default=None,
57 help='Password for created user')
59 help='Password for created user')
58 parser.add_argument('--email', default=None,
60 parser.add_argument('--email', default=None,
59 help='Email for created user')
61 help='Email for created user')
60 parser.add_argument('--auth-token', default=None,
62 parser.add_argument('--auth-token', default=None,
61 help='Auth token for created user')
63 help='Auth token for created user')
62 args = parser.parse_args()
64 args = parser.parse_args()
63 config_uri = args.config
65 config_uri = args.config
64
66
65 setup_logging(config_uri)
67 setup_logging(config_uri)
66 env = bootstrap(config_uri)
68 env = bootstrap(config_uri)
67 request = env['request']
69 request = env['request']
68 with get_current_request().tm:
70 with get_current_request().tm:
69 group = GroupService.by_id(1)
71 group = GroupService.by_id(1)
70 if not group:
72 if not group:
71 group = Group(id=1, group_name='Administrators',
73 group = Group(id=1, group_name='Administrators',
72 description="Top level permission owners")
74 description="Top level permission owners")
73 DBSession.add(group)
75 DBSession.add(group)
74 permission = GroupPermission(perm_name='root_administration')
76 permission = GroupPermission(perm_name='root_administration')
75 group.permissions.append(permission)
77 group.permissions.append(permission)
76
78
77 create_user = True if args.username else None
79 create_user = True if args.username else None
78 while create_user is None:
80 while create_user is None:
79 response = input(
81 response = input(
80 'Do you want to create a new admin? (n)\n').lower()
82 'Do you want to create a new admin? (n)\n').lower()
81
83
82 if is_yes(response or 'n'):
84 if is_yes(response or 'n'):
83 create_user = True
85 create_user = True
84 elif is_no(response or 'n'):
86 elif is_no(response or 'n'):
85 create_user = False
87 create_user = False
86
88
87 if create_user:
89 if create_user:
88 csrf_token = request.session.get_csrf_token()
90 csrf_token = request.session.get_csrf_token()
89 user_name = args.username
91 user_name = args.username
90 print('*********************************************************')
92 print('*********************************************************')
91 while user_name is None:
93 while user_name is None:
92 response = input('What is the username of new admin?\n')
94 response = input('What is the username of new admin?\n')
93 form = UserRegisterForm(
95 form = UserRegisterForm(
94 user_name=response, csrf_token=csrf_token,
96 user_name=response, csrf_token=csrf_token,
95 csrf_context=request)
97 csrf_context=request)
96 form.validate()
98 form.validate()
97 if form.user_name.errors:
99 if form.user_name.errors:
98 print(form.user_name.errors[0])
100 print(form.user_name.errors[0])
99 else:
101 else:
100 user_name = response
102 user_name = response
101 print('The admin username is "{}"\n'.format(user_name))
103 print('The admin username is "{}"\n'.format(user_name))
102 print('*********************************************************')
104 print('*********************************************************')
103 email = args.email
105 email = args.email
104 while email is None:
106 while email is None:
105 response = input('What is the email of admin account?\n')
107 response = input('What is the email of admin account?\n')
106 form = UserRegisterForm(
108 form = UserRegisterForm(
107 email=response, csrf_token=csrf_token,
109 email=response, csrf_token=csrf_token,
108 csrf_context=request)
110 csrf_context=request)
109 form.validate()
111 form.validate()
110 if form.email.errors:
112 if form.email.errors:
111 print(form.email.errors[0])
113 print(form.email.errors[0])
112 else:
114 else:
113 email = response
115 email = response
114 print('The admin email is "{}"\n'.format(email))
116 print('The admin email is "{}"\n'.format(email))
115 print('*********************************************************')
117 print('*********************************************************')
116 user_password = args.password
118 user_password = args.password
117 confirmed_password = args.password
119 confirmed_password = args.password
118 while user_password is None or confirmed_password is None:
120 while user_password is None or confirmed_password is None:
119 response = getpass.getpass(
121 response = getpass.getpass(
120 'What is the password for admin account?\n')
122 'What is the password for admin account?\n')
121 form = UserRegisterForm(
123 form = UserRegisterForm(
122 user_password=response, csrf_token=csrf_token,
124 user_password=response, csrf_token=csrf_token,
123 csrf_context=request)
125 csrf_context=request)
124 form.validate()
126 form.validate()
125 if form.user_password.errors:
127 if form.user_password.errors:
126 print(form.user_password.errors[0])
128 print(form.user_password.errors[0])
127 else:
129 else:
128 user_password = response
130 user_password = response
129
131
130 response = getpass.getpass('Please confirm the password.\n')
132 response = getpass.getpass('Please confirm the password.\n')
131 if user_password == response:
133 if user_password == response:
132 confirmed_password = response
134 confirmed_password = response
133 else:
135 else:
134 print('Passwords do not match. Please try again')
136 print('Passwords do not match. Please try again')
135 print('*********************************************************')
137 print('*********************************************************')
136
138
137 with get_current_request().tm:
139 with get_current_request().tm:
138 if create_user:
140 if create_user:
139 group = GroupService.by_id(1)
141 group = GroupService.by_id(1)
140 user = User(user_name=user_name, email=email, status=1)
142 user = User(user_name=user_name, email=email, status=1)
141 user.regenerate_security_code()
143 UserService.regenerate_security_code(user)
142 user.set_password(user_password)
144 UserService.set_password(user, user_password)
143 DBSession.add(user)
145 DBSession.add(user)
144 token = AuthToken(description="Uptime monitoring token")
146 token = AuthToken(description="Uptime monitoring token")
145 if args.auth_token:
147 if args.auth_token:
146 token.token = args.auth_token
148 token.token = args.auth_token
147 user.auth_tokens.append(token)
149 user.auth_tokens.append(token)
148 group.users.append(user)
150 group.users.append(user)
149 print('USER CREATED')
151 print('USER CREATED')
150 print(json.dumps(user.get_dict()))
152 print(json.dumps(user.get_dict()))
151 print('*********************************************************')
153 print('*********************************************************')
152 print('AUTH TOKEN')
154 print('AUTH TOKEN')
153 print(json.dumps(user.auth_tokens[0].get_dict()))
155 print(json.dumps(user.auth_tokens[0].get_dict()))
@@ -1,346 +1,348 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 from pyramid.security import Allow, Everyone, Authenticated, ALL_PERMISSIONS
17 from pyramid.security import Allow, Everyone, Authenticated, ALL_PERMISSIONS
18 from pyramid.authentication import CallbackAuthenticationPolicy
18 from pyramid.authentication import CallbackAuthenticationPolicy
19 import appenlight.models.resource
19 import appenlight.models.resource
20 from appenlight.models.services.auth_token import AuthTokenService
20 from appenlight.models.services.auth_token import AuthTokenService
21 from appenlight.models.services.application import ApplicationService
21 from appenlight.models.services.application import ApplicationService
22 from appenlight.models.services.report_group import ReportGroupService
22 from appenlight.models.services.report_group import ReportGroupService
23 from appenlight.models.services.plugin_config import PluginConfigService
23 from appenlight.models.services.plugin_config import PluginConfigService
24 from appenlight.lib import to_integer_safe
24 from appenlight.lib import to_integer_safe
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest
26 from ziggurat_foundations.permissions import permission_to_04_acls
26 from ziggurat_foundations.permissions import permission_to_04_acls
27 from ziggurat_foundations.models.services.user import UserService
28 from ziggurat_foundations.models.services.resource import ResourceService
27 import defusedxml.ElementTree as ElementTree
29 import defusedxml.ElementTree as ElementTree
28 import urllib.request, urllib.error, urllib.parse
30 import urllib.request, urllib.error, urllib.parse
29 import logging
31 import logging
30 import re
32 import re
31 from xml.sax.saxutils import quoteattr
33 from xml.sax.saxutils import quoteattr
32
34
33 log = logging.getLogger(__name__)
35 log = logging.getLogger(__name__)
34
36
35
37
36 def groupfinder(userid, request):
38 def groupfinder(userid, request):
37 if userid and hasattr(request, 'user') and request.user:
39 if userid and hasattr(request, 'user') and request.user:
38 groups = ['group:%s' % g.id for g in request.user.groups]
40 groups = ['group:%s' % g.id for g in request.user.groups]
39 return groups
41 return groups
40 return []
42 return []
41
43
42
44
43 class AuthTokenAuthenticationPolicy(CallbackAuthenticationPolicy):
45 class AuthTokenAuthenticationPolicy(CallbackAuthenticationPolicy):
44 def __init__(self, callback=None):
46 def __init__(self, callback=None):
45 self.callback = callback
47 self.callback = callback
46
48
47 def remember(self, request, principal, **kw):
49 def remember(self, request, principal, **kw):
48 return []
50 return []
49
51
50 def forget(self, request):
52 def forget(self, request):
51 return []
53 return []
52
54
53 def unauthenticated_userid(self, request):
55 def unauthenticated_userid(self, request):
54 token = request.headers.get('x-appenlight-auth-token')
56 token = request.headers.get('x-appenlight-auth-token')
55 if token:
57 if token:
56 auth_token = AuthTokenService.by_token(token)
58 auth_token = AuthTokenService.by_token(token)
57 if auth_token and not auth_token.is_expired:
59 if auth_token and not auth_token.is_expired:
58 log.info('%s is valid' % auth_token)
60 log.info('%s is valid' % auth_token)
59 return auth_token.owner_id
61 return auth_token.owner_id
60 elif auth_token:
62 elif auth_token:
61 log.warning('%s is expired' % auth_token)
63 log.warning('%s is expired' % auth_token)
62 else:
64 else:
63 log.warning('token: %s is not found' % token)
65 log.warning('token: %s is not found' % token)
64
66
65 def authenticated_userid(self, request):
67 def authenticated_userid(self, request):
66 return self.unauthenticated_userid(request)
68 return self.unauthenticated_userid(request)
67
69
68
70
69 def rewrite_root_perm(perm_user, perm_name):
71 def rewrite_root_perm(perm_user, perm_name):
70 """
72 """
71 Translates root_administration into ALL_PERMISSIONS object
73 Translates root_administration into ALL_PERMISSIONS object
72 """
74 """
73 if perm_name == 'root_administration':
75 if perm_name == 'root_administration':
74 return (Allow, perm_user, ALL_PERMISSIONS,)
76 return (Allow, perm_user, ALL_PERMISSIONS,)
75 else:
77 else:
76 return (Allow, perm_user, perm_name,)
78 return (Allow, perm_user, perm_name,)
77
79
78
80
79 def add_root_superperm(request, context):
81 def add_root_superperm(request, context):
80 """
82 """
81 Adds ALL_PERMISSIONS to every resource if user somehow has 'root_permission'
83 Adds ALL_PERMISSIONS to every resource if user somehow has 'root_permission'
82 non-resource permission
84 non-resource permission
83 """
85 """
84 if hasattr(request, 'user') and request.user:
86 if hasattr(request, 'user') and request.user:
85 acls = permission_to_04_acls(request.user.permissions)
87 acls = permission_to_04_acls(UserService.permissions(request.user))
86 for perm_user, perm_name in acls:
88 for perm_user, perm_name in acls:
87 if perm_name == 'root_administration':
89 if perm_name == 'root_administration':
88 context.__acl__.append(rewrite_root_perm(perm_user, perm_name))
90 context.__acl__.append(rewrite_root_perm(perm_user, perm_name))
89
91
90
92
91 class RootFactory(object):
93 class RootFactory(object):
92 """
94 """
93 General factory for non-resource/report specific pages
95 General factory for non-resource/report specific pages
94 """
96 """
95
97
96 def __init__(self, request):
98 def __init__(self, request):
97 self.__acl__ = [(Allow, Authenticated, 'authenticated'),
99 self.__acl__ = [(Allow, Authenticated, 'authenticated'),
98 (Allow, Authenticated, 'create_resources')]
100 (Allow, Authenticated, 'create_resources')]
99 # general page factory - append custom non resource permissions
101 # general page factory - append custom non resource permissions
100 if hasattr(request, 'user') and request.user:
102 if hasattr(request, 'user') and request.user:
101 acls = permission_to_04_acls(request.user.permissions)
103 acls = permission_to_04_acls(UserService.permissions(request.user))
102 for perm_user, perm_name in acls:
104 for perm_user, perm_name in acls:
103 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
105 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
104
106
105 class ResourceFactory(object):
107 class ResourceFactory(object):
106 """
108 """
107 Checks permissions to specific resource based on user permissions or
109 Checks permissions to specific resource based on user permissions or
108 API key headers
110 API key headers
109 """
111 """
110
112
111 def __init__(self, request):
113 def __init__(self, request):
112 Resource = appenlight.models.resource.Resource
114 Resource = appenlight.models.resource.Resource
113
115
114 self.__acl__ = []
116 self.__acl__ = []
115 resource_id = request.matchdict.get("resource_id",
117 resource_id = request.matchdict.get("resource_id",
116 request.GET.get("resource_id"))
118 request.GET.get("resource_id"))
117 resource_id = to_integer_safe(resource_id)
119 resource_id = to_integer_safe(resource_id)
118 self.resource = Resource.by_resource_id(resource_id) \
120 self.resource = ResourceService.by_resource_id(resource_id) \
119 if resource_id else None
121 if resource_id else None
120 if self.resource and request.user:
122 if self.resource and request.user:
121 self.__acl__ = self.resource.__acl__
123 self.__acl__ = self.resource.__acl__
122 permissions = self.resource.perms_for_user(request.user)
124 permissions = ResourceService.perms_for_user(self.resource, request.user)
123 for perm_user, perm_name in permission_to_04_acls(permissions):
125 for perm_user, perm_name in permission_to_04_acls(permissions):
124 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
126 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
125 add_root_superperm(request, self)
127 add_root_superperm(request, self)
126
128
127
129
128 class ResourceReportFactory(object):
130 class ResourceReportFactory(object):
129 """
131 """
130 Checks permissions to specific resource based on user permissions or
132 Checks permissions to specific resource based on user permissions or
131 API key headers
133 API key headers
132 Resource is fetched based on report group information
134 Resource is fetched based on report group information
133 """
135 """
134
136
135 def __init__(self, request):
137 def __init__(self, request):
136 Resource = appenlight.models.resource.Resource
138 Resource = appenlight.models.resource.Resource
137
139
138 self.__acl__ = []
140 self.__acl__ = []
139 group_id = request.matchdict.get("group_id",
141 group_id = request.matchdict.get("group_id",
140 request.params.get("group_id"))
142 request.params.get("group_id"))
141 group_id = to_integer_safe(group_id)
143 group_id = to_integer_safe(group_id)
142 self.report_group = ReportGroupService.by_id(
144 self.report_group = ReportGroupService.by_id(
143 group_id) if group_id else None
145 group_id) if group_id else None
144 if not self.report_group:
146 if not self.report_group:
145 raise HTTPNotFound()
147 raise HTTPNotFound()
146
148
147 self.public = self.report_group.public
149 self.public = self.report_group.public
148 self.resource = Resource.by_resource_id(self.report_group.resource_id) \
150 self.resource = ResourceService.by_resource_id(self.report_group.resource_id) \
149 if self.report_group else None
151 if self.report_group else None
150
152
151 if self.resource:
153 if self.resource:
152 self.__acl__ = self.resource.__acl__
154 self.__acl__ = self.resource.__acl__
153 if request.user:
155 if request.user:
154 permissions = self.resource.perms_for_user(request.user)
156 permissions = ResourceService.perms_for_user(self.resource, request.user)
155 for perm_user, perm_name in permission_to_04_acls(permissions):
157 for perm_user, perm_name in permission_to_04_acls(permissions):
156 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
158 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
157 if self.public:
159 if self.public:
158 self.__acl__.append((Allow, Everyone, 'view',))
160 self.__acl__.append((Allow, Everyone, 'view',))
159 if not request.user:
161 if not request.user:
160 # unauthed users need to visit using both group and report pair
162 # unauthed users need to visit using both group and report pair
161 report_id = request.params.get('reportId',
163 report_id = request.params.get('reportId',
162 request.params.get('report_id', -1))
164 request.params.get('report_id', -1))
163 report = self.report_group.get_report(report_id, public=True)
165 report = self.report_group.get_report(report_id, public=True)
164 if not report:
166 if not report:
165 raise HTTPNotFound()
167 raise HTTPNotFound()
166 add_root_superperm(request, self)
168 add_root_superperm(request, self)
167
169
168
170
169 class APIFactory(object):
171 class APIFactory(object):
170 """
172 """
171 Checks permissions to perform client API actions based on keys
173 Checks permissions to perform client API actions based on keys
172 """
174 """
173
175
174 def __init__(self, request):
176 def __init__(self, request):
175 self.__acl__ = []
177 self.__acl__ = []
176 self.possibly_public = False
178 self.possibly_public = False
177 private_api_key = request.headers.get(
179 private_api_key = request.headers.get(
178 'x-appenlight-api-key',
180 'x-appenlight-api-key',
179 request.params.get('api_key')
181 request.params.get('api_key')
180 )
182 )
181 log.debug("private key: %s" % private_api_key)
183 log.debug("private key: %s" % private_api_key)
182 if private_api_key:
184 if private_api_key:
183 self.resource = ApplicationService.by_api_key_cached()(
185 self.resource = ApplicationService.by_api_key_cached()(
184 private_api_key)
186 private_api_key)
185 # then try public key
187 # then try public key
186 else:
188 else:
187 public_api_key = request.headers.get(
189 public_api_key = request.headers.get(
188 'x-appenlight-public-api-key',
190 'x-appenlight-public-api-key',
189 request.GET.get('public_api_key'))
191 request.GET.get('public_api_key'))
190 log.debug("public key: %s" % public_api_key)
192 log.debug("public key: %s" % public_api_key)
191 self.resource = ApplicationService.by_public_api_key(
193 self.resource = ApplicationService.by_public_api_key(
192 public_api_key, from_cache=True, request=request)
194 public_api_key, from_cache=True, request=request)
193 self.possibly_public = True
195 self.possibly_public = True
194 if self.resource:
196 if self.resource:
195 self.__acl__.append((Allow, Everyone, 'create',))
197 self.__acl__.append((Allow, Everyone, 'create',))
196
198
197
199
198 class AirbrakeV2APIFactory(object):
200 class AirbrakeV2APIFactory(object):
199 """
201 """
200 Check permission based on Airbrake XML report
202 Check permission based on Airbrake XML report
201 """
203 """
202
204
203 def __init__(self, request):
205 def __init__(self, request):
204 self.__acl__ = []
206 self.__acl__ = []
205 self.possibly_public = False
207 self.possibly_public = False
206 fixed_xml_data = ''
208 fixed_xml_data = ''
207 try:
209 try:
208 data = request.GET.get('data')
210 data = request.GET.get('data')
209 if data:
211 if data:
210 self.possibly_public = True
212 self.possibly_public = True
211 except (UnicodeDecodeError, UnicodeEncodeError) as exc:
213 except (UnicodeDecodeError, UnicodeEncodeError) as exc:
212 log.warning(
214 log.warning(
213 'Problem parsing Airbrake data: %s, failed decoding' % exc)
215 'Problem parsing Airbrake data: %s, failed decoding' % exc)
214 raise HTTPBadRequest()
216 raise HTTPBadRequest()
215 try:
217 try:
216 if not data:
218 if not data:
217 data = request.body
219 data = request.body
218 # fix shitty airbrake js client not escaping line method attr
220 # fix shitty airbrake js client not escaping line method attr
219
221
220 def repl(input):
222 def repl(input):
221 return 'line method=%s file' % quoteattr(input.group(1))
223 return 'line method=%s file' % quoteattr(input.group(1))
222
224
223 fixed_xml_data = re.sub('line method="(.*?)" file', repl, data)
225 fixed_xml_data = re.sub('line method="(.*?)" file', repl, data)
224 root = ElementTree.fromstring(fixed_xml_data)
226 root = ElementTree.fromstring(fixed_xml_data)
225 except Exception as exc:
227 except Exception as exc:
226 log.info(
228 log.info(
227 'Problem parsing Airbrake '
229 'Problem parsing Airbrake '
228 'data: %s, trying unquoting' % exc)
230 'data: %s, trying unquoting' % exc)
229 self.possibly_public = True
231 self.possibly_public = True
230 try:
232 try:
231 root = ElementTree.fromstring(urllib.parse.unquote(fixed_xml_data))
233 root = ElementTree.fromstring(urllib.parse.unquote(fixed_xml_data))
232 except Exception as exc:
234 except Exception as exc:
233 log.warning('Problem parsing Airbrake '
235 log.warning('Problem parsing Airbrake '
234 'data: %s, failed completly' % exc)
236 'data: %s, failed completly' % exc)
235 raise HTTPBadRequest()
237 raise HTTPBadRequest()
236 self.airbrake_xml_etree = root
238 self.airbrake_xml_etree = root
237 api_key = root.findtext('api-key', '')
239 api_key = root.findtext('api-key', '')
238
240
239 self.resource = ApplicationService.by_api_key_cached()(api_key)
241 self.resource = ApplicationService.by_api_key_cached()(api_key)
240 if not self.resource:
242 if not self.resource:
241 self.resource = ApplicationService.by_public_api_key(api_key,
243 self.resource = ApplicationService.by_public_api_key(api_key,
242 from_cache=True,
244 from_cache=True,
243 request=request)
245 request=request)
244 if self.resource:
246 if self.resource:
245 self.possibly_public = True
247 self.possibly_public = True
246
248
247 if self.resource:
249 if self.resource:
248 self.__acl__.append((Allow, Everyone, 'create',))
250 self.__acl__.append((Allow, Everyone, 'create',))
249
251
250
252
251 def parse_sentry_header(header):
253 def parse_sentry_header(header):
252 parsed = header.split(' ', 1)[1].split(',') or []
254 parsed = header.split(' ', 1)[1].split(',') or []
253 return dict([x.strip().split('=') for x in parsed])
255 return dict([x.strip().split('=') for x in parsed])
254
256
255
257
256 class SentryAPIFactory(object):
258 class SentryAPIFactory(object):
257 """
259 """
258 Check permission based on Sentry payload
260 Check permission based on Sentry payload
259 """
261 """
260
262
261 def __init__(self, request):
263 def __init__(self, request):
262 self.__acl__ = []
264 self.__acl__ = []
263 self.possibly_public = False
265 self.possibly_public = False
264 if request.headers.get('X-Sentry-Auth', '').startswith('Sentry'):
266 if request.headers.get('X-Sentry-Auth', '').startswith('Sentry'):
265 header_string = request.headers['X-Sentry-Auth']
267 header_string = request.headers['X-Sentry-Auth']
266 result = parse_sentry_header(header_string)
268 result = parse_sentry_header(header_string)
267 elif request.headers.get('Authorization', '').startswith('Sentry'):
269 elif request.headers.get('Authorization', '').startswith('Sentry'):
268 header_string = request.headers['Authorization']
270 header_string = request.headers['Authorization']
269 result = parse_sentry_header(header_string)
271 result = parse_sentry_header(header_string)
270 else:
272 else:
271 result = dict((k, v) for k, v in list(request.GET.items())
273 result = dict((k, v) for k, v in list(request.GET.items())
272 if k.startswith('sentry_'))
274 if k.startswith('sentry_'))
273 key = result.get('sentry_key')
275 key = result.get('sentry_key')
274 log.info('sentry request {}'.format(result))
276 log.info('sentry request {}'.format(result))
275
277
276 self.resource = ApplicationService.by_api_key_cached()(key)
278 self.resource = ApplicationService.by_api_key_cached()(key)
277 if not self.resource or \
279 if not self.resource or \
278 result.get('sentry_client', '').startswith('raven-js'):
280 result.get('sentry_client', '').startswith('raven-js'):
279 self.resource = ApplicationService.by_public_api_key(
281 self.resource = ApplicationService.by_public_api_key(
280 key, from_cache=True, request=request)
282 key, from_cache=True, request=request)
281 if self.resource:
283 if self.resource:
282 self.__acl__.append((Allow, Everyone, 'create',))
284 self.__acl__.append((Allow, Everyone, 'create',))
283
285
284
286
285 class ResourcePluginConfigFactory(object):
287 class ResourcePluginConfigFactory(object):
286
288
287 def __init__(self, request):
289 def __init__(self, request):
288 Resource = appenlight.models.resource.Resource
290 Resource = appenlight.models.resource.Resource
289 self.__acl__ = []
291 self.__acl__ = []
290 self.resource = None
292 self.resource = None
291 plugin_id = to_integer_safe(request.matchdict.get('id'))
293 plugin_id = to_integer_safe(request.matchdict.get('id'))
292 self.plugin = PluginConfigService.by_id(plugin_id)
294 self.plugin = PluginConfigService.by_id(plugin_id)
293 if not self.plugin:
295 if not self.plugin:
294 raise HTTPNotFound()
296 raise HTTPNotFound()
295 if self.plugin.resource_id:
297 if self.plugin.resource_id:
296 self.resource = Resource.by_resource_id(self.plugin.resource_id)
298 self.resource = ResourceService.by_resource_id(self.plugin.resource_id)
297 if self.resource:
299 if self.resource:
298 self.__acl__ = self.resource.__acl__
300 self.__acl__ = self.resource.__acl__
299 if request.user and self.resource:
301 if request.user and self.resource:
300 permissions = self.resource.perms_for_user(request.user)
302 permissions = ResourceService.perms_for_user(self.resource, request.user)
301 for perm_user, perm_name in permission_to_04_acls(permissions):
303 for perm_user, perm_name in permission_to_04_acls(permissions):
302 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
304 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
303
305
304 add_root_superperm(request, self)
306 add_root_superperm(request, self)
305
307
306
308
307 class ResourceJSONBodyFactory(object):
309 class ResourceJSONBodyFactory(object):
308 """
310 """
309 Checks permissions to specific resource based on user permissions or
311 Checks permissions to specific resource based on user permissions or
310 API key headers from json body
312 API key headers from json body
311 """
313 """
312
314
313 def __init__(self, request):
315 def __init__(self, request):
314 Resource = appenlight.models.resource.Resource
316 Resource = appenlight.models.resource.Resource
315
317
316 self.__acl__ = []
318 self.__acl__ = []
317 resource_id = request.unsafe_json_body().get('resource_id')
319 resource_id = request.unsafe_json_body().get('resource_id')
318 resource_id = to_integer_safe(resource_id)
320 resource_id = to_integer_safe(resource_id)
319 self.resource = Resource.by_resource_id(resource_id)
321 self.resource = ResourceService.by_resource_id(resource_id)
320 if self.resource and request.user:
322 if self.resource and request.user:
321 self.__acl__ = self.resource.__acl__
323 self.__acl__ = self.resource.__acl__
322 permissions = self.resource.perms_for_user(request.user)
324 permissions = ResourceService.perms_for_user(self.resource, request.user)
323 for perm_user, perm_name in permission_to_04_acls(permissions):
325 for perm_user, perm_name in permission_to_04_acls(permissions):
324 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
326 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
325 add_root_superperm(request, self)
327 add_root_superperm(request, self)
326
328
327
329
328 class ResourcePluginMixedFactory(object):
330 class ResourcePluginMixedFactory(object):
329 def __init__(self, request):
331 def __init__(self, request):
330 Resource = appenlight.models.resource.Resource
332 Resource = appenlight.models.resource.Resource
331 self.__acl__ = []
333 self.__acl__ = []
332 json_body = request.safe_json_body
334 json_body = request.safe_json_body
333 self.resource = None
335 self.resource = None
334 if json_body:
336 if json_body:
335 resource_id = json_body.get('resource_id')
337 resource_id = json_body.get('resource_id')
336 else:
338 else:
337 resource_id = request.GET.get('resource_id')
339 resource_id = request.GET.get('resource_id')
338 if resource_id:
340 if resource_id:
339 resource_id = to_integer_safe(resource_id)
341 resource_id = to_integer_safe(resource_id)
340 self.resource = Resource.by_resource_id(resource_id)
342 self.resource = ResourceService.by_resource_id(resource_id)
341 if self.resource and request.user:
343 if self.resource and request.user:
342 self.__acl__ = self.resource.__acl__
344 self.__acl__ = self.resource.__acl__
343 permissions = self.resource.perms_for_user(request.user)
345 permissions = ResourceService.perms_for_user(self.resource, request.user)
344 for perm_user, perm_name in permission_to_04_acls(permissions):
346 for perm_user, perm_name in permission_to_04_acls(permissions):
345 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
347 self.__acl__.append(rewrite_root_perm(perm_user, perm_name))
346 add_root_superperm(request, self)
348 add_root_superperm(request, self)
@@ -1,1592 +1,1592 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import copy
17 import copy
18 import logging
18 import logging
19 import mock
19 import mock
20 import pyramid
20 import pyramid
21 import pytest
21 import pytest
22 import sqlalchemy as sa
22 import sqlalchemy as sa
23 import webob
23 import webob
24
24
25 from datetime import datetime
25 from datetime import datetime
26 from pyramid import testing
26 from pyramid import testing
27
27
28
28
29 from appenlight.models import DBSession
29 from appenlight.models import DBSession
30 from appenlight.lib.ext_json import json
30 from appenlight.lib.ext_json import json
31
31
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class DummyContext(object):
36 class DummyContext(object):
37 pass
37 pass
38
38
39
39
40 @pytest.mark.usefixtures('base_app')
40 @pytest.mark.usefixtures('base_app')
41 class BasicTest(object):
41 class BasicTest(object):
42 pass
42 pass
43
43
44
44
45 @pytest.mark.usefixtures('base_app')
45 @pytest.mark.usefixtures('base_app')
46 class TestMigration(object):
46 class TestMigration(object):
47 def test_migration(self):
47 def test_migration(self):
48 assert 1 == 1
48 assert 1 == 1
49
49
50
50
51 class TestSentryProto_7(object):
51 class TestSentryProto_7(object):
52 def test_log_payload(self):
52 def test_log_payload(self):
53 import appenlight.tests.payload_examples as payload_examples
53 import appenlight.tests.payload_examples as payload_examples
54 from appenlight.lib.enums import ParsedSentryEventType
54 from appenlight.lib.enums import ParsedSentryEventType
55 from appenlight.lib.utils.sentry import parse_sentry_event
55 from appenlight.lib.utils.sentry import parse_sentry_event
56 event_dict, event_type = parse_sentry_event(
56 event_dict, event_type = parse_sentry_event(
57 payload_examples.SENTRY_LOG_PAYLOAD_7)
57 payload_examples.SENTRY_LOG_PAYLOAD_7)
58 assert ParsedSentryEventType.LOG == event_type
58 assert ParsedSentryEventType.LOG == event_type
59 assert event_dict['log_level'] == 'CRITICAL'
59 assert event_dict['log_level'] == 'CRITICAL'
60 assert event_dict['message'] == 'TEST from django logging'
60 assert event_dict['message'] == 'TEST from django logging'
61 assert event_dict['namespace'] == 'testlogger'
61 assert event_dict['namespace'] == 'testlogger'
62 assert event_dict['request_id'] == '9a6172f2e6d2444582f83a6c333d9cfb'
62 assert event_dict['request_id'] == '9a6172f2e6d2444582f83a6c333d9cfb'
63 assert event_dict['server'] == 'ergo-virtual-machine'
63 assert event_dict['server'] == 'ergo-virtual-machine'
64 assert event_dict['date'] == datetime.utcnow().date().strftime(
64 assert event_dict['date'] == datetime.utcnow().date().strftime(
65 '%Y-%m-%dT%H:%M:%SZ')
65 '%Y-%m-%dT%H:%M:%SZ')
66 tags = [('site', 'example.com'),
66 tags = [('site', 'example.com'),
67 ('sys.argv', ["'manage.py'", "'runserver'"]),
67 ('sys.argv', ["'manage.py'", "'runserver'"]),
68 ('price', 6),
68 ('price', 6),
69 ('tag', "'extra'"),
69 ('tag', "'extra'"),
70 ('dupa', True),
70 ('dupa', True),
71 ('project', 'sentry'),
71 ('project', 'sentry'),
72 ('sentry_culprit', 'testlogger in index'),
72 ('sentry_culprit', 'testlogger in index'),
73 ('sentry_language', 'python'),
73 ('sentry_language', 'python'),
74 ('sentry_release', 'test')]
74 ('sentry_release', 'test')]
75 assert sorted(event_dict['tags']) == sorted(tags)
75 assert sorted(event_dict['tags']) == sorted(tags)
76
76
77 def test_report_payload(self):
77 def test_report_payload(self):
78 import appenlight.tests.payload_examples as payload_examples
78 import appenlight.tests.payload_examples as payload_examples
79 from appenlight.lib.enums import ParsedSentryEventType
79 from appenlight.lib.enums import ParsedSentryEventType
80 from appenlight.lib.utils.sentry import parse_sentry_event
80 from appenlight.lib.utils.sentry import parse_sentry_event
81 utcnow = datetime.utcnow().date().strftime('%Y-%m-%dT%H:%M:%SZ')
81 utcnow = datetime.utcnow().date().strftime('%Y-%m-%dT%H:%M:%SZ')
82 event_dict, event_type = parse_sentry_event(
82 event_dict, event_type = parse_sentry_event(
83 payload_examples.SENTRY_PYTHON_PAYLOAD_7)
83 payload_examples.SENTRY_PYTHON_PAYLOAD_7)
84 assert ParsedSentryEventType.ERROR_REPORT == event_type
84 assert ParsedSentryEventType.ERROR_REPORT == event_type
85 assert event_dict['client'] == 'sentry'
85 assert event_dict['client'] == 'sentry'
86 assert event_dict[
86 assert event_dict[
87 'error'] == 'Exception: test 500 ' \
87 'error'] == 'Exception: test 500 ' \
88 '\u0142\xf3\u201c\u0107\u201c\u0107\u017c\u0105'
88 '\u0142\xf3\u201c\u0107\u201c\u0107\u017c\u0105'
89 assert event_dict['language'] == 'python'
89 assert event_dict['language'] == 'python'
90 assert event_dict['ip'] == '127.0.0.1'
90 assert event_dict['ip'] == '127.0.0.1'
91 assert event_dict['request_id'] == '9fae652c8c1c4d6a8eee09260f613a98'
91 assert event_dict['request_id'] == '9fae652c8c1c4d6a8eee09260f613a98'
92 assert event_dict['server'] == 'ergo-virtual-machine'
92 assert event_dict['server'] == 'ergo-virtual-machine'
93 assert event_dict['start_time'] == utcnow
93 assert event_dict['start_time'] == utcnow
94 assert event_dict['url'] == 'http://127.0.0.1:8000/error'
94 assert event_dict['url'] == 'http://127.0.0.1:8000/error'
95 assert event_dict['user_agent'] == 'Mozilla/5.0 (X11; Linux x86_64) ' \
95 assert event_dict['user_agent'] == 'Mozilla/5.0 (X11; Linux x86_64) ' \
96 'AppleWebKit/537.36 (KHTML, ' \
96 'AppleWebKit/537.36 (KHTML, ' \
97 'like Gecko) Chrome/47.0.2526.106 ' \
97 'like Gecko) Chrome/47.0.2526.106 ' \
98 'Safari/537.36'
98 'Safari/537.36'
99 assert event_dict['view_name'] == 'djangoapp.views in error'
99 assert event_dict['view_name'] == 'djangoapp.views in error'
100 tags = [('site', 'example.com'), ('sentry_release', 'test')]
100 tags = [('site', 'example.com'), ('sentry_release', 'test')]
101 assert sorted(event_dict['tags']) == sorted(tags)
101 assert sorted(event_dict['tags']) == sorted(tags)
102 extra = [('sys.argv', ["'manage.py'", "'runserver'"]),
102 extra = [('sys.argv', ["'manage.py'", "'runserver'"]),
103 ('project', 'sentry')]
103 ('project', 'sentry')]
104 assert sorted(event_dict['extra']) == sorted(extra)
104 assert sorted(event_dict['extra']) == sorted(extra)
105 request = event_dict['request']
105 request = event_dict['request']
106 assert request['url'] == 'http://127.0.0.1:8000/error'
106 assert request['url'] == 'http://127.0.0.1:8000/error'
107 assert request['cookies'] == {'appenlight': 'X'}
107 assert request['cookies'] == {'appenlight': 'X'}
108 assert request['data'] is None
108 assert request['data'] is None
109 assert request['method'] == 'GET'
109 assert request['method'] == 'GET'
110 assert request['query_string'] == ''
110 assert request['query_string'] == ''
111 assert request['env'] == {'REMOTE_ADDR': '127.0.0.1',
111 assert request['env'] == {'REMOTE_ADDR': '127.0.0.1',
112 'SERVER_NAME': 'localhost',
112 'SERVER_NAME': 'localhost',
113 'SERVER_PORT': '8000'}
113 'SERVER_PORT': '8000'}
114 assert request['headers'] == {
114 assert request['headers'] == {
115 'Accept': 'text/html,application/xhtml+xml,'
115 'Accept': 'text/html,application/xhtml+xml,'
116 'application/xml;q=0.9,image/webp,*/*;q=0.8',
116 'application/xml;q=0.9,image/webp,*/*;q=0.8',
117 'Accept-Encoding': 'gzip, deflate, sdch',
117 'Accept-Encoding': 'gzip, deflate, sdch',
118 'Accept-Language': 'en-US,en;q=0.8,pl;q=0.6',
118 'Accept-Language': 'en-US,en;q=0.8,pl;q=0.6',
119 'Connection': 'keep-alive',
119 'Connection': 'keep-alive',
120 'Content-Length': '',
120 'Content-Length': '',
121 'Content-Type': 'text/plain',
121 'Content-Type': 'text/plain',
122 'Cookie': 'appenlight=X',
122 'Cookie': 'appenlight=X',
123 'Dnt': '1',
123 'Dnt': '1',
124 'Host': '127.0.0.1:8000',
124 'Host': '127.0.0.1:8000',
125 'Upgrade-Insecure-Requests': '1',
125 'Upgrade-Insecure-Requests': '1',
126 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) '
126 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) '
127 'AppleWebKit/537.36 (KHTML, like Gecko) '
127 'AppleWebKit/537.36 (KHTML, like Gecko) '
128 'Chrome/47.0.2526.106 Safari/537.36'}
128 'Chrome/47.0.2526.106 Safari/537.36'}
129 traceback = event_dict['traceback']
129 traceback = event_dict['traceback']
130 assert traceback[0]['cline'] == 'response = wrapped_callback(request, ' \
130 assert traceback[0]['cline'] == 'response = wrapped_callback(request, ' \
131 '*callback_args, **callback_kwargs)'
131 '*callback_args, **callback_kwargs)'
132 assert traceback[0]['file'] == 'django/core/handlers/base.py'
132 assert traceback[0]['file'] == 'django/core/handlers/base.py'
133 assert traceback[0]['fn'] == 'get_response'
133 assert traceback[0]['fn'] == 'get_response'
134 assert traceback[0]['line'] == 111
134 assert traceback[0]['line'] == 111
135 assert traceback[0]['module'] == 'django.core.handlers.base'
135 assert traceback[0]['module'] == 'django.core.handlers.base'
136
136
137 assert traceback[1]['cline'] == "raise Exception(u'test 500 " \
137 assert traceback[1]['cline'] == "raise Exception(u'test 500 " \
138 "\u0142\xf3\u201c\u0107\u201c\u0107" \
138 "\u0142\xf3\u201c\u0107\u201c\u0107" \
139 "\u017c\u0105')"
139 "\u017c\u0105')"
140 assert traceback[1]['file'] == 'djangoapp/views.py'
140 assert traceback[1]['file'] == 'djangoapp/views.py'
141 assert traceback[1]['fn'] == 'error'
141 assert traceback[1]['fn'] == 'error'
142 assert traceback[1]['line'] == 84
142 assert traceback[1]['line'] == 84
143 assert traceback[1]['module'] == 'djangoapp.views'
143 assert traceback[1]['module'] == 'djangoapp.views'
144 assert sorted(traceback[1]['vars']) == sorted([
144 assert sorted(traceback[1]['vars']) == sorted([
145 ('c',
145 ('c',
146 '<sqlite3.Cursor object at 0x7fe7c82af8f0>'),
146 '<sqlite3.Cursor object at 0x7fe7c82af8f0>'),
147 ('request',
147 ('request',
148 '<WSGIRequest at 0x140633490316304>'),
148 '<WSGIRequest at 0x140633490316304>'),
149 ('conn',
149 ('conn',
150 '<sqlite3.Connection object at 0x7fe7c8b23bf8>')])
150 '<sqlite3.Connection object at 0x7fe7c8b23bf8>')])
151
151
152
152
153 class TestAPIReports_0_5_Validation(object):
153 class TestAPIReports_0_5_Validation(object):
154 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
154 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
155 def test_no_payload(self, dummy_json):
155 def test_no_payload(self, dummy_json):
156 import colander
156 import colander
157 from appenlight.validators import ReportListSchema_0_5
157 from appenlight.validators import ReportListSchema_0_5
158 utcnow = datetime.utcnow()
158 utcnow = datetime.utcnow()
159 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
159 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
160 with pytest.raises(colander.Invalid):
160 with pytest.raises(colander.Invalid):
161 schema.deserialize(dummy_json)
161 schema.deserialize(dummy_json)
162
162
163 def test_minimal_payload(self):
163 def test_minimal_payload(self):
164 dummy_json = [{}]
164 dummy_json = [{}]
165 import colander
165 import colander
166 from appenlight.validators import ReportListSchema_0_5
166 from appenlight.validators import ReportListSchema_0_5
167 utcnow = datetime.utcnow()
167 utcnow = datetime.utcnow()
168 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
168 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
169 with pytest.raises(colander.Invalid):
169 with pytest.raises(colander.Invalid):
170 schema.deserialize(dummy_json)
170 schema.deserialize(dummy_json)
171
171
172 def test_minimal_payload(self):
172 def test_minimal_payload(self):
173 dummy_json = [{'report_details': [{}]}]
173 dummy_json = [{'report_details': [{}]}]
174 from appenlight.validators import ReportListSchema_0_5
174 from appenlight.validators import ReportListSchema_0_5
175 utcnow = datetime.utcnow()
175 utcnow = datetime.utcnow()
176 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
176 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
177
177
178 deserialized = schema.deserialize(dummy_json)
178 deserialized = schema.deserialize(dummy_json)
179
179
180 expected_deserialization = [
180 expected_deserialization = [
181 {'language': 'unknown',
181 {'language': 'unknown',
182 'server': 'unknown',
182 'server': 'unknown',
183 'occurences': 1,
183 'occurences': 1,
184 'priority': 5,
184 'priority': 5,
185 'view_name': '',
185 'view_name': '',
186 'client': 'unknown',
186 'client': 'unknown',
187 'http_status': 200,
187 'http_status': 200,
188 'error': '',
188 'error': '',
189 'tags': None,
189 'tags': None,
190 'username': '',
190 'username': '',
191 'traceback': None,
191 'traceback': None,
192 'extra': None,
192 'extra': None,
193 'url': '',
193 'url': '',
194 'ip': None,
194 'ip': None,
195 'start_time': utcnow,
195 'start_time': utcnow,
196 'group_string': None,
196 'group_string': None,
197 'request': {},
197 'request': {},
198 'request_stats': None,
198 'request_stats': None,
199 'end_time': None,
199 'end_time': None,
200 'request_id': '',
200 'request_id': '',
201 'message': '',
201 'message': '',
202 'slow_calls': [],
202 'slow_calls': [],
203 'user_agent': ''
203 'user_agent': ''
204 }
204 }
205 ]
205 ]
206 assert deserialized == expected_deserialization
206 assert deserialized == expected_deserialization
207
207
208 def test_full_payload(self):
208 def test_full_payload(self):
209 import appenlight.tests.payload_examples as payload_examples
209 import appenlight.tests.payload_examples as payload_examples
210 from appenlight.validators import ReportListSchema_0_5
210 from appenlight.validators import ReportListSchema_0_5
211 PYTHON_PAYLOAD = copy.deepcopy(payload_examples.PYTHON_PAYLOAD_0_5)
211 PYTHON_PAYLOAD = copy.deepcopy(payload_examples.PYTHON_PAYLOAD_0_5)
212 utcnow = datetime.utcnow()
212 utcnow = datetime.utcnow()
213 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
213 schema = ReportListSchema_0_5().bind(utcnow=utcnow)
214 PYTHON_PAYLOAD["tags"] = [("foo", 1), ("action", "test"), ("baz", 1.1),
214 PYTHON_PAYLOAD["tags"] = [("foo", 1), ("action", "test"), ("baz", 1.1),
215 ("date",
215 ("date",
216 utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
216 utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
217 dummy_json = [PYTHON_PAYLOAD]
217 dummy_json = [PYTHON_PAYLOAD]
218 deserialized = schema.deserialize(dummy_json)[0]
218 deserialized = schema.deserialize(dummy_json)[0]
219 assert deserialized['error'] == PYTHON_PAYLOAD['error']
219 assert deserialized['error'] == PYTHON_PAYLOAD['error']
220 assert deserialized['language'] == PYTHON_PAYLOAD['language']
220 assert deserialized['language'] == PYTHON_PAYLOAD['language']
221 assert deserialized['server'] == PYTHON_PAYLOAD['server']
221 assert deserialized['server'] == PYTHON_PAYLOAD['server']
222 assert deserialized['priority'] == PYTHON_PAYLOAD['priority']
222 assert deserialized['priority'] == PYTHON_PAYLOAD['priority']
223 assert deserialized['view_name'] == PYTHON_PAYLOAD['view_name']
223 assert deserialized['view_name'] == PYTHON_PAYLOAD['view_name']
224 assert deserialized['client'] == PYTHON_PAYLOAD['client']
224 assert deserialized['client'] == PYTHON_PAYLOAD['client']
225 assert deserialized['http_status'] == PYTHON_PAYLOAD['http_status']
225 assert deserialized['http_status'] == PYTHON_PAYLOAD['http_status']
226 assert deserialized['error'] == PYTHON_PAYLOAD['error']
226 assert deserialized['error'] == PYTHON_PAYLOAD['error']
227 assert deserialized['occurences'] == PYTHON_PAYLOAD['occurences']
227 assert deserialized['occurences'] == PYTHON_PAYLOAD['occurences']
228 assert deserialized['username'] == PYTHON_PAYLOAD['username']
228 assert deserialized['username'] == PYTHON_PAYLOAD['username']
229 assert deserialized['traceback'] == PYTHON_PAYLOAD['traceback']
229 assert deserialized['traceback'] == PYTHON_PAYLOAD['traceback']
230 assert deserialized['url'] == PYTHON_PAYLOAD['url']
230 assert deserialized['url'] == PYTHON_PAYLOAD['url']
231 assert deserialized['ip'] == PYTHON_PAYLOAD['ip']
231 assert deserialized['ip'] == PYTHON_PAYLOAD['ip']
232 assert deserialized['start_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
232 assert deserialized['start_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
233 PYTHON_PAYLOAD['start_time']
233 PYTHON_PAYLOAD['start_time']
234 assert deserialized['ip'] == PYTHON_PAYLOAD['ip']
234 assert deserialized['ip'] == PYTHON_PAYLOAD['ip']
235 assert deserialized['group_string'] is None
235 assert deserialized['group_string'] is None
236 assert deserialized['request_stats'] == PYTHON_PAYLOAD['request_stats']
236 assert deserialized['request_stats'] == PYTHON_PAYLOAD['request_stats']
237 assert deserialized['end_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
237 assert deserialized['end_time'].strftime('%Y-%m-%dT%H:%M:%S.0') == \
238 PYTHON_PAYLOAD['end_time']
238 PYTHON_PAYLOAD['end_time']
239 assert deserialized['request_id'] == PYTHON_PAYLOAD['request_id']
239 assert deserialized['request_id'] == PYTHON_PAYLOAD['request_id']
240 assert deserialized['message'] == PYTHON_PAYLOAD['message']
240 assert deserialized['message'] == PYTHON_PAYLOAD['message']
241 assert deserialized['user_agent'] == PYTHON_PAYLOAD['user_agent']
241 assert deserialized['user_agent'] == PYTHON_PAYLOAD['user_agent']
242 assert deserialized['slow_calls'][0]['start'].strftime(
242 assert deserialized['slow_calls'][0]['start'].strftime(
243 '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][
243 '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][
244 'start']
244 'start']
245 assert deserialized['slow_calls'][0]['end'].strftime(
245 assert deserialized['slow_calls'][0]['end'].strftime(
246 '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][
246 '%Y-%m-%dT%H:%M:%S.0') == PYTHON_PAYLOAD['slow_calls'][0][
247 'end']
247 'end']
248 assert deserialized['slow_calls'][0]['statement'] == \
248 assert deserialized['slow_calls'][0]['statement'] == \
249 PYTHON_PAYLOAD['slow_calls'][0]['statement']
249 PYTHON_PAYLOAD['slow_calls'][0]['statement']
250 assert deserialized['slow_calls'][0]['parameters'] == \
250 assert deserialized['slow_calls'][0]['parameters'] == \
251 PYTHON_PAYLOAD['slow_calls'][0]['parameters']
251 PYTHON_PAYLOAD['slow_calls'][0]['parameters']
252 assert deserialized['slow_calls'][0]['type'] == \
252 assert deserialized['slow_calls'][0]['type'] == \
253 PYTHON_PAYLOAD['slow_calls'][0]['type']
253 PYTHON_PAYLOAD['slow_calls'][0]['type']
254 assert deserialized['slow_calls'][0]['subtype'] == \
254 assert deserialized['slow_calls'][0]['subtype'] == \
255 PYTHON_PAYLOAD['slow_calls'][0]['subtype']
255 PYTHON_PAYLOAD['slow_calls'][0]['subtype']
256 assert deserialized['slow_calls'][0]['location'] == ''
256 assert deserialized['slow_calls'][0]['location'] == ''
257 assert deserialized['tags'] == [
257 assert deserialized['tags'] == [
258 ('foo', 1), ('action', 'test'),
258 ('foo', 1), ('action', 'test'),
259 ('baz', 1.1), ('date', utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
259 ('baz', 1.1), ('date', utcnow.strftime('%Y-%m-%dT%H:%M:%S.0'))]
260
260
261
261
262 @pytest.mark.usefixtures('log_schema')
262 @pytest.mark.usefixtures('log_schema')
263 class TestAPILogsValidation(object):
263 class TestAPILogsValidation(object):
264 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
264 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
265 def test_no_payload(self, dummy_json, log_schema):
265 def test_no_payload(self, dummy_json, log_schema):
266 import colander
266 import colander
267
267
268 with pytest.raises(colander.Invalid):
268 with pytest.raises(colander.Invalid):
269 log_schema.deserialize(dummy_json)
269 log_schema.deserialize(dummy_json)
270
270
271 def test_minimal_payload(self, log_schema):
271 def test_minimal_payload(self, log_schema):
272 dummy_json = [{}]
272 dummy_json = [{}]
273 deserialized = log_schema.deserialize(dummy_json)[0]
273 deserialized = log_schema.deserialize(dummy_json)[0]
274 expected = {'log_level': 'UNKNOWN',
274 expected = {'log_level': 'UNKNOWN',
275 'namespace': '',
275 'namespace': '',
276 'server': 'unknown',
276 'server': 'unknown',
277 'request_id': '',
277 'request_id': '',
278 'primary_key': None,
278 'primary_key': None,
279 'date': datetime.utcnow(),
279 'date': datetime.utcnow(),
280 'message': '',
280 'message': '',
281 'tags': None}
281 'tags': None}
282 assert deserialized['log_level'] == expected['log_level']
282 assert deserialized['log_level'] == expected['log_level']
283 assert deserialized['message'] == expected['message']
283 assert deserialized['message'] == expected['message']
284 assert deserialized['namespace'] == expected['namespace']
284 assert deserialized['namespace'] == expected['namespace']
285 assert deserialized['request_id'] == expected['request_id']
285 assert deserialized['request_id'] == expected['request_id']
286 assert deserialized['server'] == expected['server']
286 assert deserialized['server'] == expected['server']
287 assert deserialized['tags'] == expected['tags']
287 assert deserialized['tags'] == expected['tags']
288 assert deserialized['primary_key'] == expected['primary_key']
288 assert deserialized['primary_key'] == expected['primary_key']
289
289
290 def test_normal_payload(self, log_schema):
290 def test_normal_payload(self, log_schema):
291 import appenlight.tests.payload_examples as payload_examples
291 import appenlight.tests.payload_examples as payload_examples
292 deserialized = log_schema.deserialize(payload_examples.LOG_EXAMPLES)[0]
292 deserialized = log_schema.deserialize(payload_examples.LOG_EXAMPLES)[0]
293 expected = payload_examples.LOG_EXAMPLES[0]
293 expected = payload_examples.LOG_EXAMPLES[0]
294 assert deserialized['log_level'] == expected['log_level']
294 assert deserialized['log_level'] == expected['log_level']
295 assert deserialized['message'] == expected['message']
295 assert deserialized['message'] == expected['message']
296 assert deserialized['namespace'] == expected['namespace']
296 assert deserialized['namespace'] == expected['namespace']
297 assert deserialized['request_id'] == expected['request_id']
297 assert deserialized['request_id'] == expected['request_id']
298 assert deserialized['server'] == expected['server']
298 assert deserialized['server'] == expected['server']
299 assert deserialized['date'].strftime('%Y-%m-%dT%H:%M:%S.%f') == \
299 assert deserialized['date'].strftime('%Y-%m-%dT%H:%M:%S.%f') == \
300 expected['date']
300 expected['date']
301 assert deserialized['tags'][0][0] == "tag_name"
301 assert deserialized['tags'][0][0] == "tag_name"
302 assert deserialized['tags'][0][1] == "tag_value"
302 assert deserialized['tags'][0][1] == "tag_value"
303 assert deserialized['tags'][1][0] == "tag_name2"
303 assert deserialized['tags'][1][0] == "tag_name2"
304 assert deserialized['tags'][1][1] == 2
304 assert deserialized['tags'][1][1] == 2
305
305
306 def test_normal_payload_date_without_microseconds(self, log_schema):
306 def test_normal_payload_date_without_microseconds(self, log_schema):
307 import appenlight.tests.payload_examples as payload_examples
307 import appenlight.tests.payload_examples as payload_examples
308 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
308 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
309 LOG_EXAMPLE[0]['date'] = datetime.utcnow().strftime(
309 LOG_EXAMPLE[0]['date'] = datetime.utcnow().strftime(
310 '%Y-%m-%dT%H:%M:%S')
310 '%Y-%m-%dT%H:%M:%S')
311 deserialized = log_schema.deserialize(LOG_EXAMPLE)
311 deserialized = log_schema.deserialize(LOG_EXAMPLE)
312 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M:%S') == \
312 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M:%S') == \
313 LOG_EXAMPLE[0]['date']
313 LOG_EXAMPLE[0]['date']
314
314
315 def test_normal_payload_date_without_seconds(self, log_schema):
315 def test_normal_payload_date_without_seconds(self, log_schema):
316 import appenlight.tests.payload_examples as payload_examples
316 import appenlight.tests.payload_examples as payload_examples
317 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
317 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
318 LOG_EXAMPLE[0]['date'] = datetime.utcnow().date().strftime(
318 LOG_EXAMPLE[0]['date'] = datetime.utcnow().date().strftime(
319 '%Y-%m-%dT%H:%M')
319 '%Y-%m-%dT%H:%M')
320 deserialized = log_schema.deserialize(LOG_EXAMPLE)
320 deserialized = log_schema.deserialize(LOG_EXAMPLE)
321 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') == \
321 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') == \
322 LOG_EXAMPLE[0]['date']
322 LOG_EXAMPLE[0]['date']
323
323
324 def test_payload_empty_date(self, log_schema):
324 def test_payload_empty_date(self, log_schema):
325 import appenlight.tests.payload_examples as payload_examples
325 import appenlight.tests.payload_examples as payload_examples
326 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
326 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
327 LOG_EXAMPLE[0]['date'] = None
327 LOG_EXAMPLE[0]['date'] = None
328 deserialized = log_schema.deserialize(LOG_EXAMPLE)
328 deserialized = log_schema.deserialize(LOG_EXAMPLE)
329 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None
329 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None
330
330
331 def test_payload_no_date(self, log_schema):
331 def test_payload_no_date(self, log_schema):
332 import appenlight.tests.payload_examples as payload_examples
332 import appenlight.tests.payload_examples as payload_examples
333 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
333 LOG_EXAMPLE = copy.deepcopy(payload_examples.LOG_EXAMPLES)
334 LOG_EXAMPLE[0].pop('date', None)
334 LOG_EXAMPLE[0].pop('date', None)
335 deserialized = log_schema.deserialize(LOG_EXAMPLE)
335 deserialized = log_schema.deserialize(LOG_EXAMPLE)
336 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None
336 assert deserialized[0]['date'].strftime('%Y-%m-%dT%H:%M') is not None
337
337
338
338
339 @pytest.mark.usefixtures('general_metrics_schema')
339 @pytest.mark.usefixtures('general_metrics_schema')
340 class TestAPIGeneralMetricsValidation(object):
340 class TestAPIGeneralMetricsValidation(object):
341 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
341 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
342 def test_no_payload(self, dummy_json, general_metrics_schema):
342 def test_no_payload(self, dummy_json, general_metrics_schema):
343 import colander
343 import colander
344
344
345 with pytest.raises(colander.Invalid):
345 with pytest.raises(colander.Invalid):
346 general_metrics_schema.deserialize(dummy_json)
346 general_metrics_schema.deserialize(dummy_json)
347
347
348 def test_minimal_payload(self, general_metrics_schema):
348 def test_minimal_payload(self, general_metrics_schema):
349 dummy_json = [{'tags': [['counter_a', 15.5], ['counter_b', 63]]}]
349 dummy_json = [{'tags': [['counter_a', 15.5], ['counter_b', 63]]}]
350 deserialized = general_metrics_schema.deserialize(dummy_json)[0]
350 deserialized = general_metrics_schema.deserialize(dummy_json)[0]
351 expected = {'namespace': '',
351 expected = {'namespace': '',
352 'server_name': 'unknown',
352 'server_name': 'unknown',
353 'tags': [('counter_a', 15.5), ('counter_b', 63)],
353 'tags': [('counter_a', 15.5), ('counter_b', 63)],
354 'timestamp': datetime.utcnow()}
354 'timestamp': datetime.utcnow()}
355 assert deserialized['namespace'] == expected['namespace']
355 assert deserialized['namespace'] == expected['namespace']
356 assert deserialized['server_name'] == expected['server_name']
356 assert deserialized['server_name'] == expected['server_name']
357 assert deserialized['tags'] == expected['tags']
357 assert deserialized['tags'] == expected['tags']
358
358
359 def test_normal_payload(self, general_metrics_schema):
359 def test_normal_payload(self, general_metrics_schema):
360 import appenlight.tests.payload_examples as payload_examples
360 import appenlight.tests.payload_examples as payload_examples
361 dummy_json = [payload_examples.METRICS_PAYLOAD]
361 dummy_json = [payload_examples.METRICS_PAYLOAD]
362 deserialized = general_metrics_schema.deserialize(dummy_json)[0]
362 deserialized = general_metrics_schema.deserialize(dummy_json)[0]
363 expected = {'namespace': 'some.monitor',
363 expected = {'namespace': 'some.monitor',
364 'server_name': 'server.name',
364 'server_name': 'server.name',
365 'tags': [('usage_foo', 15.5), ('usage_bar', 63)],
365 'tags': [('usage_foo', 15.5), ('usage_bar', 63)],
366 'timestamp': datetime.utcnow()}
366 'timestamp': datetime.utcnow()}
367 assert deserialized['namespace'] == expected['namespace']
367 assert deserialized['namespace'] == expected['namespace']
368 assert deserialized['server_name'] == expected['server_name']
368 assert deserialized['server_name'] == expected['server_name']
369 assert deserialized['tags'] == expected['tags']
369 assert deserialized['tags'] == expected['tags']
370
370
371
371
372 @pytest.mark.usefixtures('request_metrics_schema')
372 @pytest.mark.usefixtures('request_metrics_schema')
373 class TestAPIRequestMetricsValidation(object):
373 class TestAPIRequestMetricsValidation(object):
374 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
374 @pytest.mark.parametrize('dummy_json', ['', {}, [], None])
375 def test_no_payload(self, dummy_json, request_metrics_schema):
375 def test_no_payload(self, dummy_json, request_metrics_schema):
376 import colander
376 import colander
377
377
378 with pytest.raises(colander.Invalid):
378 with pytest.raises(colander.Invalid):
379 print(request_metrics_schema.deserialize(dummy_json))
379 print(request_metrics_schema.deserialize(dummy_json))
380
380
381 def test_normal_payload(self, request_metrics_schema):
381 def test_normal_payload(self, request_metrics_schema):
382 import appenlight.tests.payload_examples as payload_examples
382 import appenlight.tests.payload_examples as payload_examples
383 dummy_json = payload_examples.REQUEST_METRICS_EXAMPLES
383 dummy_json = payload_examples.REQUEST_METRICS_EXAMPLES
384 deserialized = request_metrics_schema.deserialize(dummy_json)[0]
384 deserialized = request_metrics_schema.deserialize(dummy_json)[0]
385 expected = {'metrics': [('dir/module:func',
385 expected = {'metrics': [('dir/module:func',
386 {'custom': 0.0,
386 {'custom': 0.0,
387 'custom_calls': 0.0,
387 'custom_calls': 0.0,
388 'main': 0.01664,
388 'main': 0.01664,
389 'nosql': 0.00061,
389 'nosql': 0.00061,
390 'nosql_calls': 23.0,
390 'nosql_calls': 23.0,
391 'remote': 0.0,
391 'remote': 0.0,
392 'remote_calls': 0.0,
392 'remote_calls': 0.0,
393 'requests': 1,
393 'requests': 1,
394 'sql': 0.00105,
394 'sql': 0.00105,
395 'sql_calls': 2.0,
395 'sql_calls': 2.0,
396 'tmpl': 0.0,
396 'tmpl': 0.0,
397 'tmpl_calls': 0.0}),
397 'tmpl_calls': 0.0}),
398 ('SomeView.function',
398 ('SomeView.function',
399 {'custom': 0.0,
399 {'custom': 0.0,
400 'custom_calls': 0.0,
400 'custom_calls': 0.0,
401 'main': 0.647261,
401 'main': 0.647261,
402 'nosql': 0.306554,
402 'nosql': 0.306554,
403 'nosql_calls': 140.0,
403 'nosql_calls': 140.0,
404 'remote': 0.0,
404 'remote': 0.0,
405 'remote_calls': 0.0,
405 'remote_calls': 0.0,
406 'requests': 28,
406 'requests': 28,
407 'sql': 0.0,
407 'sql': 0.0,
408 'sql_calls': 0.0,
408 'sql_calls': 0.0,
409 'tmpl': 0.0,
409 'tmpl': 0.0,
410 'tmpl_calls': 0.0})],
410 'tmpl_calls': 0.0})],
411 'server': 'some.server.hostname',
411 'server': 'some.server.hostname',
412 'timestamp': datetime.utcnow()}
412 'timestamp': datetime.utcnow()}
413 assert deserialized['server'] == expected['server']
413 assert deserialized['server'] == expected['server']
414 metric = deserialized['metrics'][0]
414 metric = deserialized['metrics'][0]
415 expected_metric = expected['metrics'][0]
415 expected_metric = expected['metrics'][0]
416 assert metric[0] == expected_metric[0]
416 assert metric[0] == expected_metric[0]
417 assert sorted(metric[1].items()) == sorted(expected_metric[1].items())
417 assert sorted(metric[1].items()) == sorted(expected_metric[1].items())
418
418
419
419
420 @pytest.mark.usefixtures('default_application')
420 @pytest.mark.usefixtures('default_application')
421 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
421 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
422 class TestAPIReportsView(object):
422 class TestAPIReportsView(object):
423 def test_no_json_payload(self, default_application):
423 def test_no_json_payload(self, default_application):
424 import colander
424 import colander
425 from appenlight.models.services.application import ApplicationService
425 from appenlight.models.services.application import ApplicationService
426 from appenlight.views.api import reports_create
426 from appenlight.views.api import reports_create
427
427
428 context = DummyContext()
428 context = DummyContext()
429 context.resource = ApplicationService.by_id(1)
429 context.resource = ApplicationService.by_id(1)
430 request = testing.DummyRequest(
430 request = testing.DummyRequest(
431 headers={'Content-Type': 'application/json'})
431 headers={'Content-Type': 'application/json'})
432 request.unsafe_json_body = ''
432 request.unsafe_json_body = ''
433 request.context = context
433 request.context = context
434 route = mock.Mock()
434 route = mock.Mock()
435 route.name = 'api_reports'
435 route.name = 'api_reports'
436 request.matched_route = route
436 request.matched_route = route
437 with pytest.raises(colander.Invalid):
437 with pytest.raises(colander.Invalid):
438 response = reports_create(request)
438 response = reports_create(request)
439
439
440 def test_single_proper_json_0_5_payload(self):
440 def test_single_proper_json_0_5_payload(self):
441 import appenlight.tests.payload_examples as payload_examples
441 import appenlight.tests.payload_examples as payload_examples
442 from appenlight.views.api import reports_create
442 from appenlight.views.api import reports_create
443 from appenlight.models.services.application import ApplicationService
443 from appenlight.models.services.application import ApplicationService
444 from appenlight.models.report_group import ReportGroup
444 from appenlight.models.report_group import ReportGroup
445 route = mock.Mock()
445 route = mock.Mock()
446 route.name = 'api_reports'
446 route.name = 'api_reports'
447 request = pyramid.threadlocal.get_current_request()
447 request = pyramid.threadlocal.get_current_request()
448 context = DummyContext()
448 context = DummyContext()
449 context.resource = ApplicationService.by_id(1)
449 context.resource = ApplicationService.by_id(1)
450 request.context = context
450 request.context = context
451 request.matched_route = route
451 request.matched_route = route
452 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
452 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
453 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD)]
453 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD)]
454 reports_create(request)
454 reports_create(request)
455 query = DBSession.query(ReportGroup)
455 query = DBSession.query(ReportGroup)
456 report = query.first()
456 report = query.first()
457 assert query.count() == 1
457 assert query.count() == 1
458 assert report.total_reports == 1
458 assert report.total_reports == 1
459
459
460 def test_grouping_0_5(self):
460 def test_grouping_0_5(self):
461 import appenlight.tests.payload_examples as payload_examples
461 import appenlight.tests.payload_examples as payload_examples
462 from appenlight.views.api import reports_create
462 from appenlight.views.api import reports_create
463 from appenlight.models.services.application import ApplicationService
463 from appenlight.models.services.application import ApplicationService
464 from appenlight.models.report_group import ReportGroup
464 from appenlight.models.report_group import ReportGroup
465 route = mock.Mock()
465 route = mock.Mock()
466 route.name = 'api_reports'
466 route.name = 'api_reports'
467 request = pyramid.threadlocal.get_current_request()
467 request = pyramid.threadlocal.get_current_request()
468 context = DummyContext()
468 context = DummyContext()
469 context.resource = ApplicationService.by_id(1)
469 context.resource = ApplicationService.by_id(1)
470 request.context = context
470 request.context = context
471 request.matched_route = route
471 request.matched_route = route
472 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
472 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
473 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD),
473 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD),
474 copy.deepcopy(PYTHON_PAYLOAD)]
474 copy.deepcopy(PYTHON_PAYLOAD)]
475 reports_create(request)
475 reports_create(request)
476 query = DBSession.query(ReportGroup)
476 query = DBSession.query(ReportGroup)
477 report = query.first()
477 report = query.first()
478 assert query.count() == 1
478 assert query.count() == 1
479 assert report.total_reports == 2
479 assert report.total_reports == 2
480
480
481 def test_grouping_different_reports_0_5(self):
481 def test_grouping_different_reports_0_5(self):
482 import appenlight.tests.payload_examples as payload_examples
482 import appenlight.tests.payload_examples as payload_examples
483 from appenlight.views.api import reports_create
483 from appenlight.views.api import reports_create
484 from appenlight.models.services.application import ApplicationService
484 from appenlight.models.services.application import ApplicationService
485 from appenlight.models.report_group import ReportGroup
485 from appenlight.models.report_group import ReportGroup
486 route = mock.Mock()
486 route = mock.Mock()
487 route.name = 'api_reports'
487 route.name = 'api_reports'
488 request = pyramid.threadlocal.get_current_request()
488 request = pyramid.threadlocal.get_current_request()
489 context = DummyContext()
489 context = DummyContext()
490 context.resource = ApplicationService.by_id(1)
490 context.resource = ApplicationService.by_id(1)
491 request.context = context
491 request.context = context
492 request.matched_route = route
492 request.matched_route = route
493 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
493 PYTHON_PAYLOAD = payload_examples.PYTHON_PAYLOAD_0_5
494 PARSED_REPORT_404 = payload_examples.PARSED_REPORT_404
494 PARSED_REPORT_404 = payload_examples.PARSED_REPORT_404
495 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD),
495 request.unsafe_json_body = [copy.deepcopy(PYTHON_PAYLOAD),
496 copy.deepcopy(PARSED_REPORT_404)]
496 copy.deepcopy(PARSED_REPORT_404)]
497 reports_create(request)
497 reports_create(request)
498 query = DBSession.query(ReportGroup)
498 query = DBSession.query(ReportGroup)
499 report = query.first()
499 report = query.first()
500 assert query.count() == 2
500 assert query.count() == 2
501 assert report.total_reports == 1
501 assert report.total_reports == 1
502
502
503
503
504 @pytest.mark.usefixtures('default_application')
504 @pytest.mark.usefixtures('default_application')
505 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
505 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
506 class TestAirbrakeXMLView(object):
506 class TestAirbrakeXMLView(object):
507
507
508 def test_normal_payload_parsing(self):
508 def test_normal_payload_parsing(self):
509 import datetime
509 import datetime
510 import defusedxml.ElementTree as ElementTree
510 import defusedxml.ElementTree as ElementTree
511 import appenlight.tests.payload_examples as payload_examples
511 import appenlight.tests.payload_examples as payload_examples
512 from appenlight.lib.utils.airbrake import parse_airbrake_xml
512 from appenlight.lib.utils.airbrake import parse_airbrake_xml
513 from appenlight.validators import ReportListSchema_0_5
513 from appenlight.validators import ReportListSchema_0_5
514
514
515 context = DummyContext()
515 context = DummyContext()
516 request = testing.DummyRequest(
516 request = testing.DummyRequest(
517 headers={'Content-Type': 'application/xml'})
517 headers={'Content-Type': 'application/xml'})
518 request.context = context
518 request.context = context
519 request.context.possibly_public = False
519 request.context.possibly_public = False
520 root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE)
520 root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE)
521 request.context.airbrake_xml_etree = root
521 request.context.airbrake_xml_etree = root
522 error_dict = parse_airbrake_xml(request)
522 error_dict = parse_airbrake_xml(request)
523 schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
523 schema = ReportListSchema_0_5().bind(utcnow=datetime.datetime.utcnow())
524 deserialized_report = schema.deserialize([error_dict])[0]
524 deserialized_report = schema.deserialize([error_dict])[0]
525 assert deserialized_report['client'] == 'Airbrake Notifier'
525 assert deserialized_report['client'] == 'Airbrake Notifier'
526 assert deserialized_report['error'] == 'NameError: undefined local variable or method `sdfdfdf\' for #<#<Class:0x000000039a8b90>:0x00000002c53df0>'
526 assert deserialized_report['error'] == 'NameError: undefined local variable or method `sdfdfdf\' for #<#<Class:0x000000039a8b90>:0x00000002c53df0>'
527 assert deserialized_report['http_status'] == 500
527 assert deserialized_report['http_status'] == 500
528 assert deserialized_report['language'] == 'unknown'
528 assert deserialized_report['language'] == 'unknown'
529 assert deserialized_report['message'] == ''
529 assert deserialized_report['message'] == ''
530 assert deserialized_report['occurences'] == 1
530 assert deserialized_report['occurences'] == 1
531 assert deserialized_report['priority'] == 5
531 assert deserialized_report['priority'] == 5
532 d_request = deserialized_report['request']
532 d_request = deserialized_report['request']
533 assert d_request['GET'] == {'test': '1234'}
533 assert d_request['GET'] == {'test': '1234'}
534 assert d_request['action_dispatch.request.parameters'] == {
534 assert d_request['action_dispatch.request.parameters'] == {
535 'action': 'index',
535 'action': 'index',
536 'controller': 'welcome',
536 'controller': 'welcome',
537 'test': '1234'}
537 'test': '1234'}
538 assert deserialized_report['request_id'] == 'c11b2267f3ad8b00a1768cae35559fa1'
538 assert deserialized_report['request_id'] == 'c11b2267f3ad8b00a1768cae35559fa1'
539 assert deserialized_report['server'] == 'ergo-desktop'
539 assert deserialized_report['server'] == 'ergo-desktop'
540 assert deserialized_report['traceback'][0] == {
540 assert deserialized_report['traceback'][0] == {
541 'cline': 'block in start_thread',
541 'cline': 'block in start_thread',
542 'file': '/home/ergo/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/webrick/server.rb',
542 'file': '/home/ergo/.rbenv/versions/1.9.3-p327/lib/ruby/1.9.1/webrick/server.rb',
543 'fn': 'block in start_thread',
543 'fn': 'block in start_thread',
544 'line': '191',
544 'line': '191',
545 'module': '',
545 'module': '',
546 'vars': {}}
546 'vars': {}}
547 assert deserialized_report['traceback'][-1] == {
547 assert deserialized_report['traceback'][-1] == {
548 'cline': '_app_views_welcome_index_html_erb___2570061166873166679_31748940',
548 'cline': '_app_views_welcome_index_html_erb___2570061166873166679_31748940',
549 'file': '[PROJECT_ROOT]/app/views/welcome/index.html.erb',
549 'file': '[PROJECT_ROOT]/app/views/welcome/index.html.erb',
550 'fn': '_app_views_welcome_index_html_erb___2570061166873166679_31748940',
550 'fn': '_app_views_welcome_index_html_erb___2570061166873166679_31748940',
551 'line': '3',
551 'line': '3',
552 'module': '',
552 'module': '',
553 'vars': {}}
553 'vars': {}}
554 assert deserialized_report['url'] == 'http://0.0.0.0:3000/welcome/index?test=1234'
554 assert deserialized_report['url'] == 'http://0.0.0.0:3000/welcome/index?test=1234'
555 assert deserialized_report['view_name'] == 'welcome:index'
555 assert deserialized_report['view_name'] == 'welcome:index'
556
556
557 def test_normal_payload_view(self):
557 def test_normal_payload_view(self):
558 import defusedxml.ElementTree as ElementTree
558 import defusedxml.ElementTree as ElementTree
559 import appenlight.tests.payload_examples as payload_examples
559 import appenlight.tests.payload_examples as payload_examples
560
560
561 from appenlight.models.services.application import ApplicationService
561 from appenlight.models.services.application import ApplicationService
562 from appenlight.views.api import airbrake_xml_compat
562 from appenlight.views.api import airbrake_xml_compat
563
563
564 context = DummyContext()
564 context = DummyContext()
565 context.resource = ApplicationService.by_id(1)
565 context.resource = ApplicationService.by_id(1)
566 request = testing.DummyRequest(
566 request = testing.DummyRequest(
567 headers={'Content-Type': 'application/xml'})
567 headers={'Content-Type': 'application/xml'})
568 request.context = context
568 request.context = context
569 request.context.possibly_public = False
569 request.context.possibly_public = False
570 root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE)
570 root = ElementTree.fromstring(payload_examples.AIRBRAKE_RUBY_EXAMPLE)
571 request.context.airbrake_xml_etree = root
571 request.context.airbrake_xml_etree = root
572 route = mock.Mock()
572 route = mock.Mock()
573 route.name = 'api_airbrake'
573 route.name = 'api_airbrake'
574 request.matched_route = route
574 request.matched_route = route
575 result = airbrake_xml_compat(request)
575 result = airbrake_xml_compat(request)
576 assert '<notice><id>' in result
576 assert '<notice><id>' in result
577
577
578
578
579 @pytest.mark.usefixtures('default_application')
579 @pytest.mark.usefixtures('default_application')
580 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
580 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
581 class TestAPILogView(object):
581 class TestAPILogView(object):
582 def test_no_json_payload(self, base_app):
582 def test_no_json_payload(self, base_app):
583 import colander
583 import colander
584 from appenlight.models.services.application import ApplicationService
584 from appenlight.models.services.application import ApplicationService
585 from appenlight.views.api import logs_create
585 from appenlight.views.api import logs_create
586
586
587 context = DummyContext()
587 context = DummyContext()
588 context.resource = ApplicationService.by_id(1)
588 context.resource = ApplicationService.by_id(1)
589 request = testing.DummyRequest(
589 request = testing.DummyRequest(
590 headers={'Content-Type': 'application/json'})
590 headers={'Content-Type': 'application/json'})
591 request.context = context
591 request.context = context
592 request.registry = base_app.registry
592 request.registry = base_app.registry
593 request.unsafe_json_body = ''
593 request.unsafe_json_body = ''
594 route = mock.Mock()
594 route = mock.Mock()
595 route.name = 'api_logs'
595 route.name = 'api_logs'
596 request.matched_route = route
596 request.matched_route = route
597 with pytest.raises(colander.Invalid):
597 with pytest.raises(colander.Invalid):
598 response = logs_create(request)
598 response = logs_create(request)
599
599
600 def test_single_json_payload(self):
600 def test_single_json_payload(self):
601 import appenlight.tests.payload_examples as payload_examples
601 import appenlight.tests.payload_examples as payload_examples
602 from appenlight.models.log import Log
602 from appenlight.models.log import Log
603 from appenlight.views.api import logs_create
603 from appenlight.views.api import logs_create
604 from appenlight.models.services.application import ApplicationService
604 from appenlight.models.services.application import ApplicationService
605 route = mock.Mock()
605 route = mock.Mock()
606 route.name = 'api_logs'
606 route.name = 'api_logs'
607 request = pyramid.threadlocal.get_current_request()
607 request = pyramid.threadlocal.get_current_request()
608 context = DummyContext()
608 context = DummyContext()
609 context.resource = ApplicationService.by_id(1)
609 context.resource = ApplicationService.by_id(1)
610 request.context = context
610 request.context = context
611 request.matched_route = route
611 request.matched_route = route
612 request.unsafe_json_body = [copy.deepcopy(
612 request.unsafe_json_body = [copy.deepcopy(
613 payload_examples.LOG_EXAMPLES[0])]
613 payload_examples.LOG_EXAMPLES[0])]
614 logs_create(request)
614 logs_create(request)
615 query = DBSession.query(Log)
615 query = DBSession.query(Log)
616 log = query.first()
616 log = query.first()
617 assert query.count() == 1
617 assert query.count() == 1
618 assert log.message == "OMG ValueError happened"
618 assert log.message == "OMG ValueError happened"
619
619
620 def test_multiple_json_payload(self):
620 def test_multiple_json_payload(self):
621 import appenlight.tests.payload_examples as payload_examples
621 import appenlight.tests.payload_examples as payload_examples
622 from appenlight.models.log import Log
622 from appenlight.models.log import Log
623 from appenlight.views.api import logs_create
623 from appenlight.views.api import logs_create
624 from appenlight.models.services.application import ApplicationService
624 from appenlight.models.services.application import ApplicationService
625 route = mock.Mock()
625 route = mock.Mock()
626 route.name = 'api_logs'
626 route.name = 'api_logs'
627 request = pyramid.threadlocal.get_current_request()
627 request = pyramid.threadlocal.get_current_request()
628 context = DummyContext()
628 context = DummyContext()
629 context.resource = ApplicationService.by_id(1)
629 context.resource = ApplicationService.by_id(1)
630 request.context = context
630 request.context = context
631 request.matched_route = route
631 request.matched_route = route
632 LOG_PAYLOAD = payload_examples.LOG_EXAMPLES[0]
632 LOG_PAYLOAD = payload_examples.LOG_EXAMPLES[0]
633 LOG_PAYLOAD2 = payload_examples.LOG_EXAMPLES[1]
633 LOG_PAYLOAD2 = payload_examples.LOG_EXAMPLES[1]
634 request.unsafe_json_body = copy.deepcopy([LOG_PAYLOAD, LOG_PAYLOAD2])
634 request.unsafe_json_body = copy.deepcopy([LOG_PAYLOAD, LOG_PAYLOAD2])
635 logs_create(request)
635 logs_create(request)
636 query = DBSession.query(Log).order_by(sa.asc(Log.log_id))
636 query = DBSession.query(Log).order_by(sa.asc(Log.log_id))
637 assert query.count() == 2
637 assert query.count() == 2
638 assert query[0].message == "OMG ValueError happened"
638 assert query[0].message == "OMG ValueError happened"
639 assert query[1].message == "OMG ValueError happened2"
639 assert query[1].message == "OMG ValueError happened2"
640
640
641 def test_public_key_rewriting(self):
641 def test_public_key_rewriting(self):
642 import appenlight.tests.payload_examples as payload_examples
642 import appenlight.tests.payload_examples as payload_examples
643 from appenlight.models.log import Log
643 from appenlight.models.log import Log
644 from appenlight.views.api import logs_create
644 from appenlight.views.api import logs_create
645 from appenlight.models.services.application import ApplicationService
645 from appenlight.models.services.application import ApplicationService
646 route = mock.Mock()
646 route = mock.Mock()
647 route.name = 'api_logs'
647 route.name = 'api_logs'
648 request = pyramid.threadlocal.get_current_request()
648 request = pyramid.threadlocal.get_current_request()
649 context = DummyContext()
649 context = DummyContext()
650 context.resource = ApplicationService.by_id(1)
650 context.resource = ApplicationService.by_id(1)
651 request.context = context
651 request.context = context
652 request.matched_route = route
652 request.matched_route = route
653
653
654 LOG_PAYLOAD = copy.deepcopy(payload_examples.LOG_EXAMPLES[0])
654 LOG_PAYLOAD = copy.deepcopy(payload_examples.LOG_EXAMPLES[0])
655 LOG_PAYLOAD2 = copy.deepcopy(payload_examples.LOG_EXAMPLES[1])
655 LOG_PAYLOAD2 = copy.deepcopy(payload_examples.LOG_EXAMPLES[1])
656 LOG_PAYLOAD['primary_key'] = 'X2'
656 LOG_PAYLOAD['primary_key'] = 'X2'
657 LOG_PAYLOAD2['primary_key'] = 'X2'
657 LOG_PAYLOAD2['primary_key'] = 'X2'
658 request.unsafe_json_body = [LOG_PAYLOAD, LOG_PAYLOAD2]
658 request.unsafe_json_body = [LOG_PAYLOAD, LOG_PAYLOAD2]
659 logs_create(request)
659 logs_create(request)
660
660
661 query = DBSession.query(Log).order_by(sa.asc(Log.log_id))
661 query = DBSession.query(Log).order_by(sa.asc(Log.log_id))
662 assert query.count() == 1
662 assert query.count() == 1
663 assert query[0].message == "OMG ValueError happened2"
663 assert query[0].message == "OMG ValueError happened2"
664
664
665 @pytest.mark.usefixtures('default_application')
665 @pytest.mark.usefixtures('default_application')
666 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
666 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
667 class TestAPIGeneralMetricsView(object):
667 class TestAPIGeneralMetricsView(object):
668 def test_no_json_payload(self, base_app):
668 def test_no_json_payload(self, base_app):
669 import colander
669 import colander
670 from appenlight.models.services.application import ApplicationService
670 from appenlight.models.services.application import ApplicationService
671 from appenlight.views.api import general_metrics_create
671 from appenlight.views.api import general_metrics_create
672 route = mock.Mock()
672 route = mock.Mock()
673 route.name = 'api_general_metrics'
673 route.name = 'api_general_metrics'
674 context = DummyContext()
674 context = DummyContext()
675 context.resource = ApplicationService.by_id(1)
675 context.resource = ApplicationService.by_id(1)
676 request = testing.DummyRequest(
676 request = testing.DummyRequest(
677 headers={'Content-Type': 'application/json'})
677 headers={'Content-Type': 'application/json'})
678 request.context = context
678 request.context = context
679 request.registry = base_app.registry
679 request.registry = base_app.registry
680 request.unsafe_json_body = ''
680 request.unsafe_json_body = ''
681 request.matched_route = route
681 request.matched_route = route
682 with pytest.raises(colander.Invalid):
682 with pytest.raises(colander.Invalid):
683 general_metrics_create(request)
683 general_metrics_create(request)
684
684
685 def test_single_json_payload(self):
685 def test_single_json_payload(self):
686 import appenlight.tests.payload_examples as payload_examples
686 import appenlight.tests.payload_examples as payload_examples
687 from appenlight.models.metric import Metric
687 from appenlight.models.metric import Metric
688 from appenlight.views.api import general_metrics_create
688 from appenlight.views.api import general_metrics_create
689 from appenlight.models.services.application import ApplicationService
689 from appenlight.models.services.application import ApplicationService
690 route = mock.Mock()
690 route = mock.Mock()
691 route.name = 'api_general_metric'
691 route.name = 'api_general_metric'
692 request = pyramid.threadlocal.get_current_request()
692 request = pyramid.threadlocal.get_current_request()
693 request.matched_route = route
693 request.matched_route = route
694 context = DummyContext()
694 context = DummyContext()
695 context.resource = ApplicationService.by_id(1)
695 context.resource = ApplicationService.by_id(1)
696 request.context = context
696 request.context = context
697 request.unsafe_json_body = payload_examples.METRICS_PAYLOAD
697 request.unsafe_json_body = payload_examples.METRICS_PAYLOAD
698 general_metrics_create(request)
698 general_metrics_create(request)
699 query = DBSession.query(Metric)
699 query = DBSession.query(Metric)
700 metric = query.first()
700 metric = query.first()
701 assert query.count() == 1
701 assert query.count() == 1
702 assert metric.namespace == 'some.monitor'
702 assert metric.namespace == 'some.monitor'
703
703
704 def test_multiple_json_payload(self):
704 def test_multiple_json_payload(self):
705 import appenlight.tests.payload_examples as payload_examples
705 import appenlight.tests.payload_examples as payload_examples
706 from appenlight.models.metric import Metric
706 from appenlight.models.metric import Metric
707 from appenlight.views.api import general_metrics_create
707 from appenlight.views.api import general_metrics_create
708 from appenlight.models.services.application import ApplicationService
708 from appenlight.models.services.application import ApplicationService
709 route = mock.Mock()
709 route = mock.Mock()
710 route.name = 'api_general_metrics'
710 route.name = 'api_general_metrics'
711 request = pyramid.threadlocal.get_current_request()
711 request = pyramid.threadlocal.get_current_request()
712 request.matched_route = route
712 request.matched_route = route
713 context = DummyContext()
713 context = DummyContext()
714 context.resource = ApplicationService.by_id(1)
714 context.resource = ApplicationService.by_id(1)
715 request.context = context
715 request.context = context
716 request.unsafe_json_body = [
716 request.unsafe_json_body = [
717 copy.deepcopy(payload_examples.METRICS_PAYLOAD),
717 copy.deepcopy(payload_examples.METRICS_PAYLOAD),
718 copy.deepcopy(payload_examples.METRICS_PAYLOAD),
718 copy.deepcopy(payload_examples.METRICS_PAYLOAD),
719 ]
719 ]
720 general_metrics_create(request)
720 general_metrics_create(request)
721 query = DBSession.query(Metric)
721 query = DBSession.query(Metric)
722 metric = query.first()
722 metric = query.first()
723 assert query.count() == 2
723 assert query.count() == 2
724 assert metric.namespace == 'some.monitor'
724 assert metric.namespace == 'some.monitor'
725
725
726
726
727 class TestGroupingMessageReplacements(object):
727 class TestGroupingMessageReplacements(object):
728 def replace_default_repr_python(self):
728 def replace_default_repr_python(self):
729 test_str = '''
729 test_str = '''
730 ConnectionError: ConnectionError((<urllib3.connection.HTTPConnection object at 0x7f87a0ba9fd0>, 'Connection to domain.gr timed out. (connect timeout=10)')) caused by: ConnectTimeoutError((<urllib3.connection.HTTPConnection object at 0x7f87a0ba9fd0>, 'Connection to domain.gr timed out. (connect timeout=10)'))
730 ConnectionError: ConnectionError((<urllib3.connection.HTTPConnection object at 0x7f87a0ba9fd0>, 'Connection to domain.gr timed out. (connect timeout=10)')) caused by: ConnectTimeoutError((<urllib3.connection.HTTPConnection object at 0x7f87a0ba9fd0>, 'Connection to domain.gr timed out. (connect timeout=10)'))
731 '''
731 '''
732 regex = r'<(.*?) object at (.*?)>'
732 regex = r'<(.*?) object at (.*?)>'
733
733
734
734
735 class TestRulesKeyGetter(object):
735 class TestRulesKeyGetter(object):
736 def test_default_dict_getter_top_key(self):
736 def test_default_dict_getter_top_key(self):
737 from appenlight.lib.rule import Rule
737 from appenlight.lib.rule import Rule
738 struct = {
738 struct = {
739 "a": {
739 "a": {
740 "b": 'b',
740 "b": 'b',
741 "c": {
741 "c": {
742 "d": 'd',
742 "d": 'd',
743 "g": {
743 "g": {
744 "h": 'h'
744 "h": 'h'
745 }
745 }
746 },
746 },
747 "e": 'e'
747 "e": 'e'
748 },
748 },
749 "f": 'f'
749 "f": 'f'
750 }
750 }
751 result = Rule.default_dict_struct_getter(struct, "a")
751 result = Rule.default_dict_struct_getter(struct, "a")
752 assert result == struct['a']
752 assert result == struct['a']
753
753
754 def test_default_dict_getter_sub_key(self):
754 def test_default_dict_getter_sub_key(self):
755 from appenlight.lib.rule import Rule
755 from appenlight.lib.rule import Rule
756 struct = {
756 struct = {
757 "a": {
757 "a": {
758 "b": 'b',
758 "b": 'b',
759 "c": {
759 "c": {
760 "d": 'd',
760 "d": 'd',
761 "g": {
761 "g": {
762 "h": 'h'
762 "h": 'h'
763 }
763 }
764 },
764 },
765 "e": 'e'
765 "e": 'e'
766 },
766 },
767 "f": 'f'
767 "f": 'f'
768 }
768 }
769 result = Rule.default_dict_struct_getter(struct, 'a:b')
769 result = Rule.default_dict_struct_getter(struct, 'a:b')
770 assert result == struct['a']['b']
770 assert result == struct['a']['b']
771 result = Rule.default_dict_struct_getter(struct, 'a:c:d')
771 result = Rule.default_dict_struct_getter(struct, 'a:c:d')
772 assert result == struct['a']['c']['d']
772 assert result == struct['a']['c']['d']
773
773
774 def test_default_obj_getter_top_key(self):
774 def test_default_obj_getter_top_key(self):
775 from appenlight.lib.rule import Rule
775 from appenlight.lib.rule import Rule
776 class TestStruct(object):
776 class TestStruct(object):
777 def __init__(self, a, b):
777 def __init__(self, a, b):
778 self.a = a
778 self.a = a
779 self.b = b
779 self.b = b
780
780
781 struct = TestStruct(a='a',
781 struct = TestStruct(a='a',
782 b=TestStruct(a='x', b='y'))
782 b=TestStruct(a='x', b='y'))
783 result = Rule.default_obj_struct_getter(struct, "a")
783 result = Rule.default_obj_struct_getter(struct, "a")
784 assert result == struct.a
784 assert result == struct.a
785
785
786 def test_default_obj_getter_sub_key(self):
786 def test_default_obj_getter_sub_key(self):
787 from appenlight.lib.rule import Rule
787 from appenlight.lib.rule import Rule
788 class TestStruct(object):
788 class TestStruct(object):
789 def __init__(self, name, a, b):
789 def __init__(self, name, a, b):
790 self.name = name
790 self.name = name
791 self.a = a
791 self.a = a
792 self.b = b
792 self.b = b
793
793
794 def __repr__(self):
794 def __repr__(self):
795 return '<obj {}>'.format(self.name)
795 return '<obj {}>'.format(self.name)
796
796
797 c = TestStruct('c', a=5, b='z')
797 c = TestStruct('c', a=5, b='z')
798 b = TestStruct('b', a=c, b='y')
798 b = TestStruct('b', a=c, b='y')
799 struct = TestStruct('a', a='a', b=b)
799 struct = TestStruct('a', a='a', b=b)
800 result = Rule.default_obj_struct_getter(struct, 'b:b')
800 result = Rule.default_obj_struct_getter(struct, 'b:b')
801 assert result == struct.b.b
801 assert result == struct.b.b
802 result = Rule.default_obj_struct_getter(struct, 'b:a:b')
802 result = Rule.default_obj_struct_getter(struct, 'b:a:b')
803 assert result == struct.b.a.b
803 assert result == struct.b.a.b
804
804
805
805
806 @pytest.mark.usefixtures('report_type_matrix')
806 @pytest.mark.usefixtures('report_type_matrix')
807 class TestRulesParsing():
807 class TestRulesParsing():
808 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
808 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
809 ('eq', 500, 500, True),
809 ('eq', 500, 500, True),
810 ('eq', 600, 500, False),
810 ('eq', 600, 500, False),
811 ('eq', 300, 500, False),
811 ('eq', 300, 500, False),
812 ('eq', "300", 500, False),
812 ('eq', "300", 500, False),
813 ('eq', "600", 500, False),
813 ('eq', "600", 500, False),
814 ('eq', "500", 500, True),
814 ('eq', "500", 500, True),
815 ('ne', 500, 500, False),
815 ('ne', 500, 500, False),
816 ('ne', 600, 500, True),
816 ('ne', 600, 500, True),
817 ('ne', 300, 500, True),
817 ('ne', 300, 500, True),
818 ('ne', "300", 500, True),
818 ('ne', "300", 500, True),
819 ('ne', "600", 500, True),
819 ('ne', "600", 500, True),
820 ('ne', "500", 500, False),
820 ('ne', "500", 500, False),
821 ('ge', 500, 500, True),
821 ('ge', 500, 500, True),
822 ('ge', 600, 500, True),
822 ('ge', 600, 500, True),
823 ('ge', 499, 500, False),
823 ('ge', 499, 500, False),
824 ('gt', 499, 500, False),
824 ('gt', 499, 500, False),
825 ('gt', 500, 500, False),
825 ('gt', 500, 500, False),
826 ('gt', 501, 500, True),
826 ('gt', 501, 500, True),
827 ('le', 499, 500, True),
827 ('le', 499, 500, True),
828 ('le', 500, 500, True),
828 ('le', 500, 500, True),
829 ('le', 501, 500, False),
829 ('le', 501, 500, False),
830 ('lt', 499, 500, True),
830 ('lt', 499, 500, True),
831 ('lt', 500, 500, False),
831 ('lt', 500, 500, False),
832 ('lt', 501, 500, False),
832 ('lt', 501, 500, False),
833 ])
833 ])
834 def test_single_op_int(self, op, struct_value, test_value, match_result,
834 def test_single_op_int(self, op, struct_value, test_value, match_result,
835 report_type_matrix):
835 report_type_matrix):
836 from appenlight.lib.rule import Rule
836 from appenlight.lib.rule import Rule
837 rule_config = {
837 rule_config = {
838 "op": op,
838 "op": op,
839 "field": "http_status",
839 "field": "http_status",
840 "value": test_value
840 "value": test_value
841 }
841 }
842 rule = Rule(rule_config, report_type_matrix)
842 rule = Rule(rule_config, report_type_matrix)
843
843
844 data = {
844 data = {
845 "http_status": struct_value
845 "http_status": struct_value
846 }
846 }
847 assert rule.match(data) is match_result
847 assert rule.match(data) is match_result
848
848
849 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
849 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
850 ('ge', "500.01", 500, True),
850 ('ge', "500.01", 500, True),
851 ('ge', "500.01", 500.02, False),
851 ('ge', "500.01", 500.02, False),
852 ('le', "500.01", 500.02, True)
852 ('le', "500.01", 500.02, True)
853 ])
853 ])
854 def test_single_op_float(self, op, struct_value, test_value, match_result,
854 def test_single_op_float(self, op, struct_value, test_value, match_result,
855 report_type_matrix):
855 report_type_matrix):
856 from appenlight.lib.rule import Rule
856 from appenlight.lib.rule import Rule
857 rule_config = {
857 rule_config = {
858 "op": op,
858 "op": op,
859 "field": "duration",
859 "field": "duration",
860 "value": test_value
860 "value": test_value
861 }
861 }
862 rule = Rule(rule_config, report_type_matrix)
862 rule = Rule(rule_config, report_type_matrix)
863
863
864 data = {
864 data = {
865 "duration": struct_value
865 "duration": struct_value
866 }
866 }
867 assert rule.match(data) is match_result
867 assert rule.match(data) is match_result
868
868
869 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
869 @pytest.mark.parametrize("op, struct_value, test_value, match_result", [
870 ('contains', 'foo bar baz', 'foo', True),
870 ('contains', 'foo bar baz', 'foo', True),
871 ('contains', 'foo bar baz', 'bar', True),
871 ('contains', 'foo bar baz', 'bar', True),
872 ('contains', 'foo bar baz', 'dupa', False),
872 ('contains', 'foo bar baz', 'dupa', False),
873 ('startswith', 'foo bar baz', 'foo', True),
873 ('startswith', 'foo bar baz', 'foo', True),
874 ('startswith', 'foo bar baz', 'bar', False),
874 ('startswith', 'foo bar baz', 'bar', False),
875 ('endswith', 'foo bar baz', 'baz', True),
875 ('endswith', 'foo bar baz', 'baz', True),
876 ('endswith', 'foo bar baz', 'bar', False),
876 ('endswith', 'foo bar baz', 'bar', False),
877 ])
877 ])
878 def test_single_op_string(self, op, struct_value, test_value,
878 def test_single_op_string(self, op, struct_value, test_value,
879 match_result, report_type_matrix):
879 match_result, report_type_matrix):
880 from appenlight.lib.rule import Rule
880 from appenlight.lib.rule import Rule
881 rule_config = {
881 rule_config = {
882 "op": op,
882 "op": op,
883 "field": "error",
883 "field": "error",
884 "value": test_value
884 "value": test_value
885 }
885 }
886 rule = Rule(rule_config, report_type_matrix)
886 rule = Rule(rule_config, report_type_matrix)
887
887
888 data = {
888 data = {
889 "error": struct_value
889 "error": struct_value
890 }
890 }
891 assert rule.match(data) is match_result
891 assert rule.match(data) is match_result
892
892
893 @pytest.mark.parametrize("field, value, s_type", [
893 @pytest.mark.parametrize("field, value, s_type", [
894 ('field_unicode', 500, str),
894 ('field_unicode', 500, str),
895 ('field_unicode', 500.0, str),
895 ('field_unicode', 500.0, str),
896 ('field_unicode', "500", str),
896 ('field_unicode', "500", str),
897 ('field_int', "500", int),
897 ('field_int', "500", int),
898 ('field_int', 500, int),
898 ('field_int', 500, int),
899 ('field_int', 500.0, int),
899 ('field_int', 500.0, int),
900 ('field_float', "500", float),
900 ('field_float', "500", float),
901 ('field_float', 500, float),
901 ('field_float', 500, float),
902 ('field_float', 500.0, float),
902 ('field_float', 500.0, float),
903 ])
903 ])
904 def test_type_normalization(self, field, value, s_type):
904 def test_type_normalization(self, field, value, s_type):
905 from appenlight.lib.rule import Rule
905 from appenlight.lib.rule import Rule
906 type_matrix = {
906 type_matrix = {
907 'field_unicode': {"type": 'unicode'},
907 'field_unicode': {"type": 'unicode'},
908 'field_float': {"type": 'float'},
908 'field_float': {"type": 'float'},
909 'field_int': {"type": 'int'},
909 'field_int': {"type": 'int'},
910 }
910 }
911
911
912 rule = Rule({}, type_matrix)
912 rule = Rule({}, type_matrix)
913 n_value = rule.normalized_type(field, value)
913 n_value = rule.normalized_type(field, value)
914 assert isinstance(n_value, s_type) is True
914 assert isinstance(n_value, s_type) is True
915
915
916
916
917 @pytest.mark.usefixtures('report_type_matrix')
917 @pytest.mark.usefixtures('report_type_matrix')
918 class TestNestedRuleParsing():
918 class TestNestedRuleParsing():
919
919
920 @pytest.mark.parametrize("data, result", [
920 @pytest.mark.parametrize("data, result", [
921 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
921 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
922 False),
922 False),
923 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
923 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
924 False),
924 False),
925 ({"http_status": 500, "group": {"priority": 1, "occurences": 11}},
925 ({"http_status": 500, "group": {"priority": 1, "occurences": 11}},
926 False),
926 False),
927 ({"http_status": 101, "group": {"priority": 3, "occurences": 5}},
927 ({"http_status": 101, "group": {"priority": 3, "occurences": 5}},
928 True),
928 True),
929 ])
929 ])
930 def test_NOT_rule(self, data, result, report_type_matrix):
930 def test_NOT_rule(self, data, result, report_type_matrix):
931 from appenlight.lib.rule import Rule
931 from appenlight.lib.rule import Rule
932 rule_config = {
932 rule_config = {
933 "field": "__NOT__",
933 "field": "__NOT__",
934 "rules": [
934 "rules": [
935 {
935 {
936 "op": "ge",
936 "op": "ge",
937 "field": "group:occurences",
937 "field": "group:occurences",
938 "value": "10"
938 "value": "10"
939 },
939 },
940 {
940 {
941 "op": "ge",
941 "op": "ge",
942 "field": "group:priority",
942 "field": "group:priority",
943 "value": "4"
943 "value": "4"
944 }
944 }
945 ]
945 ]
946 }
946 }
947
947
948 rule = Rule(rule_config, report_type_matrix)
948 rule = Rule(rule_config, report_type_matrix)
949 assert rule.match(data) is result
949 assert rule.match(data) is result
950
950
951 @pytest.mark.parametrize("data, result", [
951 @pytest.mark.parametrize("data, result", [
952 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
952 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
953 True),
953 True),
954 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
954 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
955 True),
955 True),
956 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
956 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
957 True),
957 True),
958 ({"http_status": 101, "group": {"priority": 3, "occurences": 11}},
958 ({"http_status": 101, "group": {"priority": 3, "occurences": 11}},
959 False),
959 False),
960 ])
960 ])
961 def test_nested_OR_AND_rule(self, data, result, report_type_matrix):
961 def test_nested_OR_AND_rule(self, data, result, report_type_matrix):
962 from appenlight.lib.rule import Rule
962 from appenlight.lib.rule import Rule
963 rule_config = {
963 rule_config = {
964 "field": "__OR__",
964 "field": "__OR__",
965 "rules": [
965 "rules": [
966 {
966 {
967 "field": "__AND__",
967 "field": "__AND__",
968 "rules": [
968 "rules": [
969 {
969 {
970 "op": "ge",
970 "op": "ge",
971 "field": "group:occurences",
971 "field": "group:occurences",
972 "value": "10"
972 "value": "10"
973 },
973 },
974 {
974 {
975 "op": "ge",
975 "op": "ge",
976 "field": "group:priority",
976 "field": "group:priority",
977 "value": "4"
977 "value": "4"
978 }
978 }
979 ]
979 ]
980 },
980 },
981 {
981 {
982 "op": "eq",
982 "op": "eq",
983 "field": "http_status",
983 "field": "http_status",
984 "value": "500"
984 "value": "500"
985 }
985 }
986 ]
986 ]
987 }
987 }
988
988
989 rule = Rule(rule_config, report_type_matrix)
989 rule = Rule(rule_config, report_type_matrix)
990 assert rule.match(data) is result
990 assert rule.match(data) is result
991
991
992 @pytest.mark.parametrize("data, result", [
992 @pytest.mark.parametrize("data, result", [
993 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
993 ({"http_status": 501, "group": {"priority": 7, "occurences": 11}},
994 True),
994 True),
995 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
995 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
996 True),
996 True),
997 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
997 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
998 True),
998 True),
999 ({"http_status": 101, "group": {"priority": 3, "occurences": 1}},
999 ({"http_status": 101, "group": {"priority": 3, "occurences": 1}},
1000 False),
1000 False),
1001 ])
1001 ])
1002 def test_nested_OR_OR_rule(self, data, result, report_type_matrix):
1002 def test_nested_OR_OR_rule(self, data, result, report_type_matrix):
1003 from appenlight.lib.rule import Rule
1003 from appenlight.lib.rule import Rule
1004 rule_config = {
1004 rule_config = {
1005 "field": "__OR__",
1005 "field": "__OR__",
1006 "rules": [
1006 "rules": [
1007 {"field": "__OR__",
1007 {"field": "__OR__",
1008 "rules": [
1008 "rules": [
1009 {"op": "ge",
1009 {"op": "ge",
1010 "field": "group:occurences",
1010 "field": "group:occurences",
1011 "value": "10"
1011 "value": "10"
1012 },
1012 },
1013 {"op": "ge",
1013 {"op": "ge",
1014 "field": "group:priority",
1014 "field": "group:priority",
1015 "value": "4"
1015 "value": "4"
1016 }
1016 }
1017 ]
1017 ]
1018 },
1018 },
1019 {"op": "eq",
1019 {"op": "eq",
1020 "field": "http_status",
1020 "field": "http_status",
1021 "value": "500"
1021 "value": "500"
1022 }
1022 }
1023 ]
1023 ]
1024 }
1024 }
1025
1025
1026 rule = Rule(rule_config, report_type_matrix)
1026 rule = Rule(rule_config, report_type_matrix)
1027 assert rule.match(data) is result
1027 assert rule.match(data) is result
1028
1028
1029 @pytest.mark.parametrize("data, result", [
1029 @pytest.mark.parametrize("data, result", [
1030 ({"http_status": 500, "group": {"priority": 7, "occurences": 11}},
1030 ({"http_status": 500, "group": {"priority": 7, "occurences": 11}},
1031 True),
1031 True),
1032 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
1032 ({"http_status": 101, "group": {"priority": 7, "occurences": 11}},
1033 False),
1033 False),
1034 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
1034 ({"http_status": 500, "group": {"priority": 1, "occurences": 1}},
1035 False),
1035 False),
1036 ({"http_status": 101, "group": {"priority": 3, "occurences": 1}},
1036 ({"http_status": 101, "group": {"priority": 3, "occurences": 1}},
1037 False),
1037 False),
1038 ])
1038 ])
1039 def test_nested_AND_AND_rule(self, data, result, report_type_matrix):
1039 def test_nested_AND_AND_rule(self, data, result, report_type_matrix):
1040 from appenlight.lib.rule import Rule
1040 from appenlight.lib.rule import Rule
1041 rule_config = {
1041 rule_config = {
1042 "field": "__AND__",
1042 "field": "__AND__",
1043 "rules": [
1043 "rules": [
1044 {"field": "__AND__",
1044 {"field": "__AND__",
1045 "rules": [
1045 "rules": [
1046 {"op": "ge",
1046 {"op": "ge",
1047 "field": "group:occurences",
1047 "field": "group:occurences",
1048 "value": "10"
1048 "value": "10"
1049 },
1049 },
1050 {"op": "ge",
1050 {"op": "ge",
1051 "field": "group:priority",
1051 "field": "group:priority",
1052 "value": "4"
1052 "value": "4"
1053 }]
1053 }]
1054 },
1054 },
1055 {"op": "eq",
1055 {"op": "eq",
1056 "field": "http_status",
1056 "field": "http_status",
1057 "value": "500"
1057 "value": "500"
1058 }
1058 }
1059 ]
1059 ]
1060 }
1060 }
1061
1061
1062 rule = Rule(rule_config, report_type_matrix)
1062 rule = Rule(rule_config, report_type_matrix)
1063 assert rule.match(data) is result
1063 assert rule.match(data) is result
1064
1064
1065 @pytest.mark.parametrize("data, result", [
1065 @pytest.mark.parametrize("data, result", [
1066 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1066 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1067 "url_path": '/test/register', "error": "foo test bar"}, True),
1067 "url_path": '/test/register', "error": "foo test bar"}, True),
1068 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1068 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1069 "url_path": '/test/register', "error": "foo INVALID bar"}, False),
1069 "url_path": '/test/register', "error": "foo INVALID bar"}, False),
1070 ])
1070 ])
1071 def test_nested_AND_AND_AND_rule(self, data, result, report_type_matrix):
1071 def test_nested_AND_AND_AND_rule(self, data, result, report_type_matrix):
1072 from appenlight.lib.rule import Rule
1072 from appenlight.lib.rule import Rule
1073 rule_config = {
1073 rule_config = {
1074 "field": "__AND__",
1074 "field": "__AND__",
1075 "rules": [
1075 "rules": [
1076 {"field": "__AND__",
1076 {"field": "__AND__",
1077 "rules": [
1077 "rules": [
1078 {"op": "ge",
1078 {"op": "ge",
1079 "field": "group:occurences",
1079 "field": "group:occurences",
1080 "value": "10"
1080 "value": "10"
1081 },
1081 },
1082 {"field": "__AND__",
1082 {"field": "__AND__",
1083 "rules": [
1083 "rules": [
1084 {"op": "endswith",
1084 {"op": "endswith",
1085 "field": "url_path",
1085 "field": "url_path",
1086 "value": "register"},
1086 "value": "register"},
1087 {"op": "contains",
1087 {"op": "contains",
1088 "field": "error",
1088 "field": "error",
1089 "value": "test"}]}]
1089 "value": "test"}]}]
1090 },
1090 },
1091 {"op": "eq",
1091 {"op": "eq",
1092 "field": "http_status",
1092 "field": "http_status",
1093 "value": "500"
1093 "value": "500"
1094 }
1094 }
1095 ]
1095 ]
1096 }
1096 }
1097
1097
1098 rule = Rule(rule_config, report_type_matrix)
1098 rule = Rule(rule_config, report_type_matrix)
1099 assert rule.match(data) is result
1099 assert rule.match(data) is result
1100
1100
1101 @pytest.mark.parametrize("data, result", [
1101 @pytest.mark.parametrize("data, result", [
1102 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1102 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1103 "url_path": 6, "error": 3}, False),
1103 "url_path": 6, "error": 3}, False),
1104 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1104 ({"http_status": 500, "group": {"priority": 7, "occurences": 11},
1105 "url_path": '/test/register', "error": "foo INVALID bar"}, True),
1105 "url_path": '/test/register', "error": "foo INVALID bar"}, True),
1106 ])
1106 ])
1107 def test_nested_AND_AND_OR_rule(self, data, result, report_type_matrix):
1107 def test_nested_AND_AND_OR_rule(self, data, result, report_type_matrix):
1108 from appenlight.lib.rule import Rule
1108 from appenlight.lib.rule import Rule
1109 rule_config = {
1109 rule_config = {
1110 "field": "__AND__",
1110 "field": "__AND__",
1111 "rules": [
1111 "rules": [
1112 {"field": "__AND__",
1112 {"field": "__AND__",
1113 "rules": [
1113 "rules": [
1114 {"op": "ge",
1114 {"op": "ge",
1115 "field": "group:occurences",
1115 "field": "group:occurences",
1116 "value": "10"
1116 "value": "10"
1117 },
1117 },
1118 {"field": "__OR__",
1118 {"field": "__OR__",
1119 "rules": [
1119 "rules": [
1120 {"op": "endswith",
1120 {"op": "endswith",
1121 "field": "url_path",
1121 "field": "url_path",
1122 "value": "register"
1122 "value": "register"
1123 },
1123 },
1124 {"op": "contains",
1124 {"op": "contains",
1125 "field": "error",
1125 "field": "error",
1126 "value": "test"
1126 "value": "test"
1127 }]}]
1127 }]}]
1128 },
1128 },
1129 {"op": "eq",
1129 {"op": "eq",
1130 "field": "http_status",
1130 "field": "http_status",
1131 "value": "500"
1131 "value": "500"
1132 }
1132 }
1133 ]
1133 ]
1134 }
1134 }
1135
1135
1136 rule = Rule(rule_config, report_type_matrix)
1136 rule = Rule(rule_config, report_type_matrix)
1137 assert rule.match(data) is result
1137 assert rule.match(data) is result
1138
1138
1139 @pytest.mark.parametrize("op, field, value, should_fail", [
1139 @pytest.mark.parametrize("op, field, value, should_fail", [
1140 ('eq', 'http_status', "1", False),
1140 ('eq', 'http_status', "1", False),
1141 ('ne', 'http_status', "1", False),
1141 ('ne', 'http_status', "1", False),
1142 ('ne', 'http_status', "foo", True),
1142 ('ne', 'http_status', "foo", True),
1143 ('startswith', 'http_status', "1", True),
1143 ('startswith', 'http_status', "1", True),
1144 ('eq', 'group:priority', "1", False),
1144 ('eq', 'group:priority', "1", False),
1145 ('ne', 'group:priority', "1", False),
1145 ('ne', 'group:priority', "1", False),
1146 ('ge', 'group:priority', "1", False),
1146 ('ge', 'group:priority', "1", False),
1147 ('le', 'group:priority', "1", False),
1147 ('le', 'group:priority', "1", False),
1148 ('startswith', 'group:priority', "1", True),
1148 ('startswith', 'group:priority', "1", True),
1149 ('eq', 'url_domain', "1", False),
1149 ('eq', 'url_domain', "1", False),
1150 ('ne', 'url_domain', "1", False),
1150 ('ne', 'url_domain', "1", False),
1151 ('startswith', 'url_domain', "1", False),
1151 ('startswith', 'url_domain', "1", False),
1152 ('endswith', 'url_domain', "1", False),
1152 ('endswith', 'url_domain', "1", False),
1153 ('contains', 'url_domain', "1", False),
1153 ('contains', 'url_domain', "1", False),
1154 ('ge', 'url_domain', "1", True),
1154 ('ge', 'url_domain', "1", True),
1155 ('eq', 'url_path', "1", False),
1155 ('eq', 'url_path', "1", False),
1156 ('ne', 'url_path', "1", False),
1156 ('ne', 'url_path', "1", False),
1157 ('startswith', 'url_path', "1", False),
1157 ('startswith', 'url_path', "1", False),
1158 ('endswith', 'url_path', "1", False),
1158 ('endswith', 'url_path', "1", False),
1159 ('contains', 'url_path', "1", False),
1159 ('contains', 'url_path', "1", False),
1160 ('ge', 'url_path', "1", True),
1160 ('ge', 'url_path', "1", True),
1161 ('eq', 'error', "1", False),
1161 ('eq', 'error', "1", False),
1162 ('ne', 'error', "1", False),
1162 ('ne', 'error', "1", False),
1163 ('startswith', 'error', "1", False),
1163 ('startswith', 'error', "1", False),
1164 ('endswith', 'error', "1", False),
1164 ('endswith', 'error', "1", False),
1165 ('contains', 'error', "1", False),
1165 ('contains', 'error', "1", False),
1166 ('ge', 'error', "1", True),
1166 ('ge', 'error', "1", True),
1167 ('ge', 'url_path', "1", True),
1167 ('ge', 'url_path', "1", True),
1168 ('eq', 'tags:server_name', "1", False),
1168 ('eq', 'tags:server_name', "1", False),
1169 ('ne', 'tags:server_name', "1", False),
1169 ('ne', 'tags:server_name', "1", False),
1170 ('startswith', 'tags:server_name', "1", False),
1170 ('startswith', 'tags:server_name', "1", False),
1171 ('endswith', 'tags:server_name', "1", False),
1171 ('endswith', 'tags:server_name', "1", False),
1172 ('contains', 'tags:server_name', "1", False),
1172 ('contains', 'tags:server_name', "1", False),
1173 ('ge', 'tags:server_name', "1", True),
1173 ('ge', 'tags:server_name', "1", True),
1174 ('contains', 'traceback', "1", False),
1174 ('contains', 'traceback', "1", False),
1175 ('ge', 'traceback', "1", True),
1175 ('ge', 'traceback', "1", True),
1176 ('eq', 'group:occurences', "1", False),
1176 ('eq', 'group:occurences', "1", False),
1177 ('ne', 'group:occurences', "1", False),
1177 ('ne', 'group:occurences', "1", False),
1178 ('ge', 'group:occurences', "1", False),
1178 ('ge', 'group:occurences', "1", False),
1179 ('le', 'group:occurences', "1", False),
1179 ('le', 'group:occurences', "1", False),
1180 ('contains', 'group:occurences', "1", True),
1180 ('contains', 'group:occurences', "1", True),
1181 ])
1181 ])
1182 def test_rule_validation(self, op, field, value, should_fail,
1182 def test_rule_validation(self, op, field, value, should_fail,
1183 report_type_matrix):
1183 report_type_matrix):
1184 import colander
1184 import colander
1185 from appenlight.validators import build_rule_schema
1185 from appenlight.validators import build_rule_schema
1186 rule_config = {
1186 rule_config = {
1187 "op": op,
1187 "op": op,
1188 "field": field,
1188 "field": field,
1189 "value": value
1189 "value": value
1190 }
1190 }
1191
1191
1192 schema = build_rule_schema(rule_config, report_type_matrix)
1192 schema = build_rule_schema(rule_config, report_type_matrix)
1193 if should_fail:
1193 if should_fail:
1194 with pytest.raises(colander.Invalid):
1194 with pytest.raises(colander.Invalid):
1195 schema.deserialize(rule_config)
1195 schema.deserialize(rule_config)
1196 else:
1196 else:
1197 schema.deserialize(rule_config)
1197 schema.deserialize(rule_config)
1198
1198
1199 def test_nested_proper_rule_validation(self, report_type_matrix):
1199 def test_nested_proper_rule_validation(self, report_type_matrix):
1200 from appenlight.validators import build_rule_schema
1200 from appenlight.validators import build_rule_schema
1201 rule_config = {
1201 rule_config = {
1202 "field": "__AND__",
1202 "field": "__AND__",
1203 "rules": [
1203 "rules": [
1204 {
1204 {
1205 "field": "__AND__",
1205 "field": "__AND__",
1206 "rules": [
1206 "rules": [
1207 {
1207 {
1208 "op": "ge",
1208 "op": "ge",
1209 "field": "group:occurences",
1209 "field": "group:occurences",
1210 "value": "10"
1210 "value": "10"
1211 },
1211 },
1212 {
1212 {
1213 "field": "__OR__",
1213 "field": "__OR__",
1214 "rules": [
1214 "rules": [
1215 {
1215 {
1216 "op": "endswith",
1216 "op": "endswith",
1217 "field": "url_path",
1217 "field": "url_path",
1218 "value": "register"
1218 "value": "register"
1219 },
1219 },
1220 {
1220 {
1221 "op": "contains",
1221 "op": "contains",
1222 "field": "error",
1222 "field": "error",
1223 "value": "test"
1223 "value": "test"
1224 }
1224 }
1225 ]
1225 ]
1226 }
1226 }
1227 ]
1227 ]
1228 },
1228 },
1229 {
1229 {
1230 "op": "eq",
1230 "op": "eq",
1231 "field": "http_status",
1231 "field": "http_status",
1232 "value": "500"
1232 "value": "500"
1233 }
1233 }
1234 ]
1234 ]
1235 }
1235 }
1236
1236
1237 schema = build_rule_schema(rule_config, report_type_matrix)
1237 schema = build_rule_schema(rule_config, report_type_matrix)
1238 deserialized = schema.deserialize(rule_config)
1238 deserialized = schema.deserialize(rule_config)
1239
1239
1240 def test_nested_bad_rule_validation(self, report_type_matrix):
1240 def test_nested_bad_rule_validation(self, report_type_matrix):
1241 import colander
1241 import colander
1242 from appenlight.validators import build_rule_schema
1242 from appenlight.validators import build_rule_schema
1243 rule_config = {
1243 rule_config = {
1244 "field": "__AND__",
1244 "field": "__AND__",
1245 "rules": [
1245 "rules": [
1246 {
1246 {
1247 "field": "__AND__",
1247 "field": "__AND__",
1248 "rules": [
1248 "rules": [
1249 {
1249 {
1250 "op": "ge",
1250 "op": "ge",
1251 "field": "group:occurences",
1251 "field": "group:occurences",
1252 "value": "10"
1252 "value": "10"
1253 },
1253 },
1254 {
1254 {
1255 "field": "__OR__",
1255 "field": "__OR__",
1256 "rules": [
1256 "rules": [
1257 {
1257 {
1258 "op": "gt",
1258 "op": "gt",
1259 "field": "url_path",
1259 "field": "url_path",
1260 "value": "register"
1260 "value": "register"
1261 },
1261 },
1262 {
1262 {
1263 "op": "contains",
1263 "op": "contains",
1264 "field": "error",
1264 "field": "error",
1265 "value": "test"
1265 "value": "test"
1266 }
1266 }
1267 ]
1267 ]
1268 }
1268 }
1269 ]
1269 ]
1270 },
1270 },
1271 {
1271 {
1272 "op": "eq",
1272 "op": "eq",
1273 "field": "http_status",
1273 "field": "http_status",
1274 "value": "500"
1274 "value": "500"
1275 }
1275 }
1276 ]
1276 ]
1277 }
1277 }
1278
1278
1279 schema = build_rule_schema(rule_config, report_type_matrix)
1279 schema = build_rule_schema(rule_config, report_type_matrix)
1280 with pytest.raises(colander.Invalid):
1280 with pytest.raises(colander.Invalid):
1281 deserialized = schema.deserialize(rule_config)
1281 deserialized = schema.deserialize(rule_config)
1282
1282
1283 def test_config_manipulator(self):
1283 def test_config_manipulator(self):
1284 from appenlight.lib.rule import Rule
1284 from appenlight.lib.rule import Rule
1285 type_matrix = {
1285 type_matrix = {
1286 'a': {"type": 'int',
1286 'a': {"type": 'int',
1287 "ops": ('eq', 'ne', 'ge', 'le',)},
1287 "ops": ('eq', 'ne', 'ge', 'le',)},
1288 'b': {"type": 'int',
1288 'b': {"type": 'int',
1289 "ops": ('eq', 'ne', 'ge', 'le',)},
1289 "ops": ('eq', 'ne', 'ge', 'le',)},
1290 }
1290 }
1291 rule_config = {
1291 rule_config = {
1292 "field": "__OR__",
1292 "field": "__OR__",
1293 "rules": [
1293 "rules": [
1294 {
1294 {
1295 "field": "__OR__",
1295 "field": "__OR__",
1296 "rules": [
1296 "rules": [
1297 {
1297 {
1298 "op": "ge",
1298 "op": "ge",
1299 "field": "a",
1299 "field": "a",
1300 "value": "10"
1300 "value": "10"
1301 }
1301 }
1302 ]
1302 ]
1303 },
1303 },
1304 {
1304 {
1305 "op": "eq",
1305 "op": "eq",
1306 "field": "b",
1306 "field": "b",
1307 "value": "500"
1307 "value": "500"
1308 }
1308 }
1309 ]
1309 ]
1310 }
1310 }
1311
1311
1312 def rule_manipulator(rule):
1312 def rule_manipulator(rule):
1313 if 'value' in rule.config:
1313 if 'value' in rule.config:
1314 rule.config['value'] = "1"
1314 rule.config['value'] = "1"
1315
1315
1316 rule = Rule(rule_config, type_matrix,
1316 rule = Rule(rule_config, type_matrix,
1317 config_manipulator=rule_manipulator)
1317 config_manipulator=rule_manipulator)
1318 rule.match({"a": 1,
1318 rule.match({"a": 1,
1319 "b": "2"})
1319 "b": "2"})
1320 assert rule.config['rules'][0]['rules'][0]['value'] == "1"
1320 assert rule.config['rules'][0]['rules'][0]['value'] == "1"
1321 assert rule.config['rules'][1]['value'] == "1"
1321 assert rule.config['rules'][1]['value'] == "1"
1322 assert rule.type_matrix["b"]['type'] == "int"
1322 assert rule.type_matrix["b"]['type'] == "int"
1323
1323
1324 def test_dynamic_config_manipulator(self):
1324 def test_dynamic_config_manipulator(self):
1325 from appenlight.lib.rule import Rule
1325 from appenlight.lib.rule import Rule
1326 rule_config = {
1326 rule_config = {
1327 "field": "__OR__",
1327 "field": "__OR__",
1328 "rules": [
1328 "rules": [
1329 {
1329 {
1330 "field": "__OR__",
1330 "field": "__OR__",
1331 "rules": [
1331 "rules": [
1332 {
1332 {
1333 "op": "ge",
1333 "op": "ge",
1334 "field": "a",
1334 "field": "a",
1335 "value": "10"
1335 "value": "10"
1336 }
1336 }
1337 ]
1337 ]
1338 },
1338 },
1339 {
1339 {
1340 "op": "eq",
1340 "op": "eq",
1341 "field": "b",
1341 "field": "b",
1342 "value": "500"
1342 "value": "500"
1343 }
1343 }
1344 ]
1344 ]
1345 }
1345 }
1346
1346
1347 def rule_manipulator(rule):
1347 def rule_manipulator(rule):
1348 rule.type_matrix = {
1348 rule.type_matrix = {
1349 'a': {"type": 'int',
1349 'a': {"type": 'int',
1350 "ops": ('eq', 'ne', 'ge', 'le',)},
1350 "ops": ('eq', 'ne', 'ge', 'le',)},
1351 'b': {"type": 'unicode',
1351 'b': {"type": 'unicode',
1352 "ops": ('eq', 'ne', 'ge', 'le',)},
1352 "ops": ('eq', 'ne', 'ge', 'le',)},
1353 }
1353 }
1354
1354
1355 if 'value' in rule.config:
1355 if 'value' in rule.config:
1356 if rule.config['field'] == 'a':
1356 if rule.config['field'] == 'a':
1357 rule.config['value'] = "1"
1357 rule.config['value'] = "1"
1358 elif rule.config['field'] == 'b':
1358 elif rule.config['field'] == 'b':
1359 rule.config['value'] = "2"
1359 rule.config['value'] = "2"
1360
1360
1361 rule = Rule(rule_config, {},
1361 rule = Rule(rule_config, {},
1362 config_manipulator=rule_manipulator)
1362 config_manipulator=rule_manipulator)
1363 rule.match({"a": 11,
1363 rule.match({"a": 11,
1364 "b": "55"})
1364 "b": "55"})
1365 assert rule.config['rules'][0]['rules'][0]['value'] == "1"
1365 assert rule.config['rules'][0]['rules'][0]['value'] == "1"
1366 assert rule.config['rules'][1]['value'] == "2"
1366 assert rule.config['rules'][1]['value'] == "2"
1367 assert rule.type_matrix["b"]['type'] == "unicode"
1367 assert rule.type_matrix["b"]['type'] == "unicode"
1368
1368
1369
1369
1370 @pytest.mark.usefixtures('base_app', 'with_migrations')
1370 @pytest.mark.usefixtures('base_app', 'with_migrations')
1371 class TestViewsWithForms(object):
1371 class TestViewsWithForms(object):
1372 def test_bad_csrf(self):
1372 def test_bad_csrf(self):
1373 from appenlight.forms import CSRFException
1373 from appenlight.forms import CSRFException
1374 from appenlight.views.index import register
1374 from appenlight.views.index import register
1375 post_data = {'dupa': 'dupa'}
1375 post_data = {'dupa': 'dupa'}
1376 request = testing.DummyRequest(post=post_data)
1376 request = testing.DummyRequest(post=post_data)
1377 request.POST = webob.multidict.MultiDict(request.POST)
1377 request.POST = webob.multidict.MultiDict(request.POST)
1378 with pytest.raises(CSRFException):
1378 with pytest.raises(CSRFException):
1379 register(request)
1379 register(request)
1380
1380
1381 def test_proper_csrf(self):
1381 def test_proper_csrf(self):
1382 from appenlight.views.index import register
1382 from appenlight.views.index import register
1383 request = pyramid.threadlocal.get_current_request()
1383 request = pyramid.threadlocal.get_current_request()
1384 post_data = {'dupa': 'dupa',
1384 post_data = {'dupa': 'dupa',
1385 'csrf_token': request.session.get_csrf_token()}
1385 'csrf_token': request.session.get_csrf_token()}
1386 request = testing.DummyRequest(post=post_data)
1386 request = testing.DummyRequest(post=post_data)
1387 request.POST = webob.multidict.MultiDict(request.POST)
1387 request.POST = webob.multidict.MultiDict(request.POST)
1388 result = register(request)
1388 result = register(request)
1389 assert result['form'].errors['email'][0] == 'This field is required.'
1389 assert result['form'].errors['email'][0] == 'This field is required.'
1390
1390
1391
1391
1392 @pytest.mark.usefixtures('base_app', 'with_migrations', 'default_data')
1392 @pytest.mark.usefixtures('base_app', 'with_migrations', 'default_data')
1393 class TestRegistration(object):
1393 class TestRegistration(object):
1394 def test_invalid_form(self):
1394 def test_invalid_form(self):
1395 from appenlight.views.index import register
1395 from appenlight.views.index import register
1396 request = pyramid.threadlocal.get_current_request()
1396 request = pyramid.threadlocal.get_current_request()
1397 post_data = {'user_name': '',
1397 post_data = {'user_name': '',
1398 'user_password': '',
1398 'user_password': '',
1399 'email': '',
1399 'email': '',
1400 'csrf_token': request.session.get_csrf_token()}
1400 'csrf_token': request.session.get_csrf_token()}
1401 request = testing.DummyRequest(post=post_data)
1401 request = testing.DummyRequest(post=post_data)
1402 request.POST = webob.multidict.MultiDict(request.POST)
1402 request.POST = webob.multidict.MultiDict(request.POST)
1403 result = register(request)
1403 result = register(request)
1404 assert result['form'].errors['user_name'][0] == \
1404 assert result['form'].errors['user_name'][0] == \
1405 'This field is required.'
1405 'This field is required.'
1406
1406
1407 def test_valid_form(self):
1407 def test_valid_form(self):
1408 from appenlight.views.index import register
1408 from appenlight.views.index import register
1409 from ziggurat_foundations.models.services.user import UserService
1409 from ziggurat_foundations.models.services.user import UserService
1410 request = pyramid.threadlocal.get_current_request()
1410 request = pyramid.threadlocal.get_current_request()
1411 post_data = {'user_name': 'foo',
1411 post_data = {'user_name': 'foo',
1412 'user_password': 'barr',
1412 'user_password': 'barr',
1413 'email': 'test@test.foo',
1413 'email': 'test@test.foo',
1414 'csrf_token': request.session.get_csrf_token()}
1414 'csrf_token': request.session.get_csrf_token()}
1415 request = testing.DummyRequest(post=post_data)
1415 request = testing.DummyRequest(post=post_data)
1416 request.add_flash_to_headers = mock.Mock()
1416 request.add_flash_to_headers = mock.Mock()
1417 request.POST = webob.multidict.MultiDict(request.POST)
1417 request.POST = webob.multidict.MultiDict(request.POST)
1418 assert UserService.by_user_name('foo') is None
1418 assert UserService.by_user_name('foo') is None
1419 register(request)
1419 register(request)
1420 user = UserService.by_user_name('foo')
1420 user = UserService.by_user_name('foo')
1421 assert user.user_name == 'foo'
1421 assert user.user_name == 'foo'
1422 assert len(user.user_password) == 60
1422 assert len(user.user_password) >= 60
1423
1423
1424
1424
1425 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables',
1425 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables',
1426 'default_user')
1426 'default_user')
1427 class TestApplicationCreation(object):
1427 class TestApplicationCreation(object):
1428 def test_wrong_data(self):
1428 def test_wrong_data(self):
1429 import appenlight.views.applications as applications
1429 import appenlight.views.applications as applications
1430 from ziggurat_foundations.models.services.user import UserService
1430 from ziggurat_foundations.models.services.user import UserService
1431 request = pyramid.threadlocal.get_current_request()
1431 request = pyramid.threadlocal.get_current_request()
1432 request.user = UserService.by_user_name('testuser')
1432 request.user = UserService.by_user_name('testuser')
1433 request.unsafe_json_body = {}
1433 request.unsafe_json_body = {}
1434 request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token()
1434 request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token()
1435 response = applications.application_create(request)
1435 response = applications.application_create(request)
1436 assert response.code == 422
1436 assert response.code == 422
1437
1437
1438 def test_proper_data(self):
1438 def test_proper_data(self):
1439 import appenlight.views.applications as applications
1439 import appenlight.views.applications as applications
1440 from ziggurat_foundations.models.services.user import UserService
1440 from ziggurat_foundations.models.services.user import UserService
1441
1441
1442 request = pyramid.threadlocal.get_current_request()
1442 request = pyramid.threadlocal.get_current_request()
1443 request.user = UserService.by_user_name('testuser')
1443 request.user = UserService.by_user_name('testuser')
1444 request.unsafe_json_body = {"resource_name": "app name",
1444 request.unsafe_json_body = {"resource_name": "app name",
1445 "domains": "foo"}
1445 "domains": "foo"}
1446 request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token()
1446 request.headers['X-XSRF-TOKEN'] = request.session.get_csrf_token()
1447 app_dict = applications.application_create(request)
1447 app_dict = applications.application_create(request)
1448 assert app_dict['public_key'] is not None
1448 assert app_dict['public_key'] is not None
1449 assert app_dict['api_key'] is not None
1449 assert app_dict['api_key'] is not None
1450 assert app_dict['resource_name'] == 'app name'
1450 assert app_dict['resource_name'] == 'app name'
1451 assert app_dict['owner_group_id'] is None
1451 assert app_dict['owner_group_id'] is None
1452 assert app_dict['resource_id'] is not None
1452 assert app_dict['resource_id'] is not None
1453 assert app_dict['default_grouping'] == 'url_traceback'
1453 assert app_dict['default_grouping'] == 'url_traceback'
1454 assert app_dict['possible_permissions'] == ('view', 'update_reports')
1454 assert app_dict['possible_permissions'] == ('view', 'update_reports')
1455 assert app_dict['slow_report_threshold'] == 10
1455 assert app_dict['slow_report_threshold'] == 10
1456 assert app_dict['owner_user_name'] == 'testuser'
1456 assert app_dict['owner_user_name'] == 'testuser'
1457 assert app_dict['owner_user_id'] == request.user.id
1457 assert app_dict['owner_user_id'] == request.user.id
1458 assert app_dict['domains'] is 'foo'
1458 assert app_dict['domains'] is 'foo'
1459 assert app_dict['postprocessing_rules'] == []
1459 assert app_dict['postprocessing_rules'] == []
1460 assert app_dict['error_report_threshold'] == 10
1460 assert app_dict['error_report_threshold'] == 10
1461 assert app_dict['allow_permanent_storage'] is False
1461 assert app_dict['allow_permanent_storage'] is False
1462 assert app_dict['resource_type'] == 'application'
1462 assert app_dict['resource_type'] == 'application'
1463 assert app_dict['current_permissions'] == []
1463 assert app_dict['current_permissions'] == []
1464
1464
1465
1465
1466 @pytest.mark.usefixtures('default_application')
1466 @pytest.mark.usefixtures('default_application')
1467 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
1467 @pytest.mark.usefixtures('base_app', 'with_migrations', 'clean_tables')
1468 class TestAPISentryView(object):
1468 class TestAPISentryView(object):
1469 def test_no_payload(self, default_application):
1469 def test_no_payload(self, default_application):
1470 import colander
1470 import colander
1471 from appenlight.models.services.application import ApplicationService
1471 from appenlight.models.services.application import ApplicationService
1472 from appenlight.views.api import sentry_compat
1472 from appenlight.views.api import sentry_compat
1473 from appenlight.lib.request import JSONException
1473 from appenlight.lib.request import JSONException
1474
1474
1475 context = DummyContext()
1475 context = DummyContext()
1476 context.resource = ApplicationService.by_id(1)
1476 context.resource = ApplicationService.by_id(1)
1477 request = testing.DummyRequest(
1477 request = testing.DummyRequest(
1478 headers={'Content-Type': 'application/json'})
1478 headers={'Content-Type': 'application/json'})
1479 request.unsafe_json_body = ''
1479 request.unsafe_json_body = ''
1480 request.context = context
1480 request.context = context
1481 route = mock.Mock()
1481 route = mock.Mock()
1482 route.name = 'api_sentry'
1482 route.name = 'api_sentry'
1483 request.matched_route = route
1483 request.matched_route = route
1484 with pytest.raises(JSONException):
1484 with pytest.raises(JSONException):
1485 sentry_compat(request)
1485 sentry_compat(request)
1486
1486
1487 def test_java_client_payload(self):
1487 def test_java_client_payload(self):
1488 from appenlight.views.api import sentry_compat
1488 from appenlight.views.api import sentry_compat
1489 from appenlight.models.services.application import ApplicationService
1489 from appenlight.models.services.application import ApplicationService
1490 from appenlight.models.report_group import ReportGroup
1490 from appenlight.models.report_group import ReportGroup
1491 route = mock.Mock()
1491 route = mock.Mock()
1492 route.name = 'api_sentry'
1492 route.name = 'api_sentry'
1493 request = pyramid.threadlocal.get_current_request()
1493 request = pyramid.threadlocal.get_current_request()
1494 context = DummyContext()
1494 context = DummyContext()
1495 context.resource = ApplicationService.by_id(1)
1495 context.resource = ApplicationService.by_id(1)
1496 context.resource.allow_permanent_storage = True
1496 context.resource.allow_permanent_storage = True
1497 request.context = context
1497 request.context = context
1498 request.matched_route = route
1498 request.matched_route = route
1499 request.body = b'eJy1UmFr2zAQ/S0T+7BCLOzYThp/C6xjG6SDLd/GCBf57Ki' \
1499 request.body = b'eJy1UmFr2zAQ/S0T+7BCLOzYThp/C6xjG6SDLd/GCBf57Ki' \
1500 b'RJSHJJiXkv+/UlC7p2kAZA33Ru6f33t1pz3BAHVayZhWr87' \
1500 b'RJSHJJiXkv+/UlC7p2kAZA33Ru6f33t1pz3BAHVayZhWr87' \
1501 b'JMs+I6q3MsrifFep2vc1iXM1HMpgBTNmIdeg8tEvlmJ9AGa' \
1501 b'JMs+I6q3MsrifFep2vc1iXM1HMpgBTNmIdeg8tEvlmJ9AGa' \
1502 b'fQ7goOkQoDOUmGcZpMkLZO0WGZFRadMiaHIR1EVnTMu3k3b' \
1502 b'fQ7goOkQoDOUmGcZpMkLZO0WGZFRadMiaHIR1EVnTMu3k3b' \
1503 b'oiMgqJrXpgOpOVjLLTiPkWAVhMa4jih3MAAholfWyUDAksz' \
1503 b'oiMgqJrXpgOpOVjLLTiPkWAVhMa4jih3MAAholfWyUDAksz' \
1504 b'm1iopICbg8fWH52B8VWXZVYwHrWfV/jBipD2gW2no8CFMa5' \
1504 b'm1iopICbg8fWH52B8VWXZVYwHrWfV/jBipD2gW2no8CFMa5' \
1505 b'JButCDSjoQG6mR6LgLDojPPn/7sbydL25ep34HGl+y3DiE+' \
1505 b'JButCDSjoQG6mR6LgLDojPPn/7sbydL25ep34HGl+y3DiE+' \
1506 b'lH0xXBXjMzFBsXW99SS7pWKYXRw91zqgK4BgZ4/DZVVP/cs' \
1506 b'lH0xXBXjMzFBsXW99SS7pWKYXRw91zqgK4BgZ4/DZVVP/cs' \
1507 b'3NuzSZPfAKqP2Cdj4tw7U/cKH0fEFeiWQFqE2FIHAmMPjaN' \
1507 b'3NuzSZPfAKqP2Cdj4tw7U/cKH0fEFeiWQFqE2FIHAmMPjaN' \
1508 b'Y/kHvbzY/JqdHUq9o/KxqQHkcsabX4piDuT4aK+pXG1ZNi/' \
1508 b'Y/kHvbzY/JqdHUq9o/KxqQHkcsabX4piDuT4aK+pXG1ZNi/' \
1509 b'IwOpEyruXC1LiB3vPO3BmOOxTUCIqv5LIg5H12oh9cf0l+P' \
1509 b'IwOpEyruXC1LiB3vPO3BmOOxTUCIqv5LIg5H12oh9cf0l+P' \
1510 b'MvP5P8kddgoFIEvMGzM5cRSD2aLJ6qTdHKm6nv9pPcRFba0' \
1510 b'MvP5P8kddgoFIEvMGzM5cRSD2aLJ6qTdHKm6nv9pPcRFba0' \
1511 b'Kd0eleeCFuGN+9JZ9TaXIn/V5JYMBvxXg3L6PwzSE4dkfOb' \
1511 b'Kd0eleeCFuGN+9JZ9TaXIn/V5JYMBvxXg3L6PwzSE4dkfOb' \
1512 b'w7CtfWmP85SdCs8OvA53fUV19cg=='
1512 b'w7CtfWmP85SdCs8OvA53fUV19cg=='
1513 sentry_compat(request)
1513 sentry_compat(request)
1514 query = DBSession.query(ReportGroup)
1514 query = DBSession.query(ReportGroup)
1515 report = query.first()
1515 report = query.first()
1516 assert query.count() == 1
1516 assert query.count() == 1
1517 assert report.total_reports == 1
1517 assert report.total_reports == 1
1518
1518
1519 def test_ruby_client_payload(self):
1519 def test_ruby_client_payload(self):
1520 from appenlight.views.api import sentry_compat
1520 from appenlight.views.api import sentry_compat
1521 from appenlight.models.services.application import ApplicationService
1521 from appenlight.models.services.application import ApplicationService
1522 from appenlight.models.report_group import ReportGroup
1522 from appenlight.models.report_group import ReportGroup
1523 from appenlight.tests.payload_examples import SENTRY_RUBY_ENCODED
1523 from appenlight.tests.payload_examples import SENTRY_RUBY_ENCODED
1524 route = mock.Mock()
1524 route = mock.Mock()
1525 route.name = 'api_sentry'
1525 route.name = 'api_sentry'
1526 request = testing.DummyRequest(
1526 request = testing.DummyRequest(
1527 headers={'Content-Type': 'application/octet-stream',
1527 headers={'Content-Type': 'application/octet-stream',
1528 'User-Agent': 'sentry-ruby/1.0.0',
1528 'User-Agent': 'sentry-ruby/1.0.0',
1529 'X-Sentry-Auth': 'Sentry sentry_version=5, '
1529 'X-Sentry-Auth': 'Sentry sentry_version=5, '
1530 'sentry_client=raven-ruby/1.0.0, '
1530 'sentry_client=raven-ruby/1.0.0, '
1531 'sentry_timestamp=1462378483, '
1531 'sentry_timestamp=1462378483, '
1532 'sentry_key=xxx, sentry_secret=xxx'
1532 'sentry_key=xxx, sentry_secret=xxx'
1533 })
1533 })
1534 context = DummyContext()
1534 context = DummyContext()
1535 context.resource = ApplicationService.by_id(1)
1535 context.resource = ApplicationService.by_id(1)
1536 context.resource.allow_permanent_storage = True
1536 context.resource.allow_permanent_storage = True
1537 request.context = context
1537 request.context = context
1538 request.matched_route = route
1538 request.matched_route = route
1539 request.body = SENTRY_RUBY_ENCODED
1539 request.body = SENTRY_RUBY_ENCODED
1540 sentry_compat(request)
1540 sentry_compat(request)
1541 query = DBSession.query(ReportGroup)
1541 query = DBSession.query(ReportGroup)
1542 report = query.first()
1542 report = query.first()
1543 assert query.count() == 1
1543 assert query.count() == 1
1544 assert report.total_reports == 1
1544 assert report.total_reports == 1
1545
1545
1546 def test_python_client_decoded_payload(self):
1546 def test_python_client_decoded_payload(self):
1547 from appenlight.views.api import sentry_compat
1547 from appenlight.views.api import sentry_compat
1548 from appenlight.models.services.application import ApplicationService
1548 from appenlight.models.services.application import ApplicationService
1549 from appenlight.models.report_group import ReportGroup
1549 from appenlight.models.report_group import ReportGroup
1550 from appenlight.tests.payload_examples import SENTRY_PYTHON_PAYLOAD_7
1550 from appenlight.tests.payload_examples import SENTRY_PYTHON_PAYLOAD_7
1551 route = mock.Mock()
1551 route = mock.Mock()
1552 route.name = 'api_sentry'
1552 route.name = 'api_sentry'
1553 request = pyramid.threadlocal.get_current_request()
1553 request = pyramid.threadlocal.get_current_request()
1554 context = DummyContext()
1554 context = DummyContext()
1555 context.resource = ApplicationService.by_id(1)
1555 context.resource = ApplicationService.by_id(1)
1556 context.resource.allow_permanent_storage = True
1556 context.resource.allow_permanent_storage = True
1557 request.context = context
1557 request.context = context
1558 request.matched_route = route
1558 request.matched_route = route
1559 request.body = json.dumps(SENTRY_PYTHON_PAYLOAD_7).encode('utf8')
1559 request.body = json.dumps(SENTRY_PYTHON_PAYLOAD_7).encode('utf8')
1560 sentry_compat(request)
1560 sentry_compat(request)
1561 query = DBSession.query(ReportGroup)
1561 query = DBSession.query(ReportGroup)
1562 report = query.first()
1562 report = query.first()
1563 assert query.count() == 1
1563 assert query.count() == 1
1564 assert report.total_reports == 1
1564 assert report.total_reports == 1
1565
1565
1566 def test_python_client_encoded_payload(self):
1566 def test_python_client_encoded_payload(self):
1567 from appenlight.views.api import sentry_compat
1567 from appenlight.views.api import sentry_compat
1568 from appenlight.models.services.application import ApplicationService
1568 from appenlight.models.services.application import ApplicationService
1569 from appenlight.models.report_group import ReportGroup
1569 from appenlight.models.report_group import ReportGroup
1570 from appenlight.tests.payload_examples import SENTRY_PYTHON_ENCODED
1570 from appenlight.tests.payload_examples import SENTRY_PYTHON_ENCODED
1571 route = mock.Mock()
1571 route = mock.Mock()
1572 route.name = 'api_sentry'
1572 route.name = 'api_sentry'
1573 request = testing.DummyRequest(
1573 request = testing.DummyRequest(
1574 headers={'Content-Type': 'application/octet-stream',
1574 headers={'Content-Type': 'application/octet-stream',
1575 'Content-Encoding': 'deflate',
1575 'Content-Encoding': 'deflate',
1576 'User-Agent': 'sentry-ruby/1.0.0',
1576 'User-Agent': 'sentry-ruby/1.0.0',
1577 'X-Sentry-Auth': 'Sentry sentry_version=5, '
1577 'X-Sentry-Auth': 'Sentry sentry_version=5, '
1578 'sentry_client=raven-ruby/1.0.0, '
1578 'sentry_client=raven-ruby/1.0.0, '
1579 'sentry_timestamp=1462378483, '
1579 'sentry_timestamp=1462378483, '
1580 'sentry_key=xxx, sentry_secret=xxx'
1580 'sentry_key=xxx, sentry_secret=xxx'
1581 })
1581 })
1582 context = DummyContext()
1582 context = DummyContext()
1583 context.resource = ApplicationService.by_id(1)
1583 context.resource = ApplicationService.by_id(1)
1584 context.resource.allow_permanent_storage = True
1584 context.resource.allow_permanent_storage = True
1585 request.context = context
1585 request.context = context
1586 request.matched_route = route
1586 request.matched_route = route
1587 request.body = SENTRY_PYTHON_ENCODED
1587 request.body = SENTRY_PYTHON_ENCODED
1588 sentry_compat(request)
1588 sentry_compat(request)
1589 query = DBSession.query(ReportGroup)
1589 query = DBSession.query(ReportGroup)
1590 report = query.first()
1590 report = query.first()
1591 assert query.count() == 1
1591 assert query.count() == 1
1592 assert report.total_reports == 1
1592 assert report.total_reports == 1
@@ -1,36 +1,36 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 from pyramid.view import view_config
17 from pyramid.view import view_config
18 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
18 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
19 from pyramid import security
19 from pyramid import security
20 from appenlight.models.user import User
20 from ziggurat_foundations.models.services.user import UserService
21
21
22 import logging
22 import logging
23
23
24 log = logging.getLogger(__name__)
24 log = logging.getLogger(__name__)
25
25
26
26
27 @view_config(route_name='section_view', permission='root_administration',
27 @view_config(route_name='section_view', permission='root_administration',
28 match_param=['section=admin_section', 'view=relogin_user'],
28 match_param=['section=admin_section', 'view=relogin_user'],
29 renderer='json', request_method='GET')
29 renderer='json', request_method='GET')
30 def relogin_to_user(request):
30 def relogin_to_user(request):
31 user = User.by_id(request.GET.get('user_id'))
31 user = UserService.by_id(request.GET.get('user_id'))
32 if not user:
32 if not user:
33 return HTTPNotFound()
33 return HTTPNotFound()
34 headers = security.remember(request, user.id)
34 headers = security.remember(request, user.id)
35 return HTTPFound(location=request.route_url('/'),
35 return HTTPFound(location=request.route_url('/'),
36 headers=headers)
36 headers=headers)
@@ -1,755 +1,760 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import copy
17 import copy
18 import json
18 import json
19 import logging
19 import logging
20 import six
20 import six
21
21
22 from datetime import datetime, timedelta
22 from datetime import datetime, timedelta
23
23
24 import colander
24 import colander
25 from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity
25 from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from webob.multidict import MultiDict
27 from webob.multidict import MultiDict
28 from zope.sqlalchemy import mark_changed
28 from zope.sqlalchemy import mark_changed
29 from ziggurat_foundations.permissions import ANY_PERMISSION
29 from ziggurat_foundations.permissions import ANY_PERMISSION
30
30
31 import appenlight.forms as forms
31 import appenlight.forms as forms
32 from appenlight.models import DBSession
32 from appenlight.models import DBSession
33 from appenlight.models.resource import Resource
33 from appenlight.models.resource import Resource
34 from appenlight.models.application import Application
34 from appenlight.models.application import Application
35 from appenlight.models.application_postprocess_conf import \
35 from appenlight.models.application_postprocess_conf import \
36 ApplicationPostprocessConf
36 ApplicationPostprocessConf
37 from appenlight.models.user import User
37 from ziggurat_foundations.models.services.user import UserService
38 from ziggurat_foundations.models.services.resource import ResourceService
39 from ziggurat_foundations.models.services.user_resource_permission import UserResourcePermissionService
38 from appenlight.models.user_resource_permission import UserResourcePermission
40 from appenlight.models.user_resource_permission import UserResourcePermission
39 from appenlight.models.group_resource_permission import GroupResourcePermission
41 from appenlight.models.group_resource_permission import GroupResourcePermission
40 from appenlight.models.services.application import ApplicationService
42 from appenlight.models.services.application import ApplicationService
41 from appenlight.models.services.application_postprocess_conf import \
43 from appenlight.models.services.application_postprocess_conf import \
42 ApplicationPostprocessConfService
44 ApplicationPostprocessConfService
43 from appenlight.models.services.group import GroupService
45 from appenlight.models.services.group import GroupService
44 from appenlight.models.services.group_resource_permission import \
46 from appenlight.models.services.group_resource_permission import \
45 GroupResourcePermissionService
47 GroupResourcePermissionService
46 from appenlight.models.services.request_metric import RequestMetricService
48 from appenlight.models.services.request_metric import RequestMetricService
47 from appenlight.models.services.report_group import ReportGroupService
49 from appenlight.models.services.report_group import ReportGroupService
48 from appenlight.models.services.slow_call import SlowCallService
50 from appenlight.models.services.slow_call import SlowCallService
49 from appenlight.lib import helpers as h
51 from appenlight.lib import helpers as h
50 from appenlight.lib.utils import build_filter_settings_from_query_dict
52 from appenlight.lib.utils import build_filter_settings_from_query_dict
51 from appenlight.security import RootFactory
53 from appenlight.security import RootFactory
52 from appenlight.models.report import REPORT_TYPE_MATRIX
54 from appenlight.models.report import REPORT_TYPE_MATRIX
53 from appenlight.validators import build_rule_schema
55 from appenlight.validators import build_rule_schema
54
56
55 _ = str
57 _ = str
56
58
57 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
58
60
59
61
60 def app_not_found(request, id):
62 def app_not_found(request, id):
61 """
63 """
62 Redirects on non found and sets a flash message
64 Redirects on non found and sets a flash message
63 """
65 """
64 request.session.flash(_('Application not found'), 'warning')
66 request.session.flash(_('Application not found'), 'warning')
65 return HTTPFound(
67 return HTTPFound(
66 location=request.route_url('applications', action='index'))
68 location=request.route_url('applications', action='index'))
67
69
68
70
69 @view_config(route_name='applications_no_id',
71 @view_config(route_name='applications_no_id',
70 renderer='json', request_method="GET", permission='authenticated')
72 renderer='json', request_method="GET", permission='authenticated')
71 def applications_list(request):
73 def applications_list(request):
72 """
74 """
73 Applications list
75 Applications list
74
76
75 if query params contain ?type=foo, it will list applications
77 if query params contain ?type=foo, it will list applications
76 with one of those permissions for user,
78 with one of those permissions for user,
77 otherwise only list of owned applications will
79 otherwise only list of owned applications will
78 be returned
80 be returned
79
81
80 appending ?root_list while being administration will allow to list all
82 appending ?root_list while being administration will allow to list all
81 applications in the system
83 applications in the system
82
84
83 """
85 """
84 is_root = request.has_permission('root_administration',
86 is_root = request.has_permission('root_administration',
85 RootFactory(request))
87 RootFactory(request))
86 if is_root and request.GET.get('root_list'):
88 if is_root and request.GET.get('root_list'):
87 resources = Resource.all().order_by(Resource.resource_name)
89 resources = Resource.all().order_by(Resource.resource_name)
88 resource_type = request.GET.get('resource_type', 'application')
90 resource_type = request.GET.get('resource_type', 'application')
89 if resource_type:
91 if resource_type:
90 resources = resources.filter(
92 resources = resources.filter(
91 Resource.resource_type == resource_type)
93 Resource.resource_type == resource_type)
92 else:
94 else:
93 permissions = request.params.getall('permission')
95 permissions = request.params.getall('permission')
94 if permissions:
96 if permissions:
95 resources = request.user.resources_with_perms(
97 resources = UserService.resources_with_perms(
98 request.user,
96 permissions,
99 permissions,
97 resource_types=[request.GET.get('resource_type',
100 resource_types=[request.GET.get('resource_type',
98 'application')])
101 'application')])
99 else:
102 else:
100 resources = request.user.resources.filter(
103 resources = request.user.resources.filter(
101 Application.resource_type == request.GET.get(
104 Application.resource_type == request.GET.get(
102 'resource_type',
105 'resource_type',
103 'application'))
106 'application'))
104 return [r.get_dict(include_keys=['resource_id', 'resource_name', 'domains',
107 return [r.get_dict(include_keys=['resource_id', 'resource_name', 'domains',
105 'owner_user_name', 'owner_group_name'])
108 'owner_user_name', 'owner_group_name'])
106 for
109 for
107 r in resources]
110 r in resources]
108
111
109
112
110 @view_config(route_name='applications', renderer='json',
113 @view_config(route_name='applications', renderer='json',
111 request_method="GET", permission='view')
114 request_method="GET", permission='view')
112 def application_GET(request):
115 def application_GET(request):
113 resource = request.context.resource
116 resource = request.context.resource
114 include_sensitive_info = False
117 include_sensitive_info = False
115 if request.has_permission('edit'):
118 if request.has_permission('edit'):
116 include_sensitive_info = True
119 include_sensitive_info = True
117 resource_dict = resource.get_dict(
120 resource_dict = resource.get_dict(
118 include_perms=include_sensitive_info,
121 include_perms=include_sensitive_info,
119 include_processing_rules=include_sensitive_info)
122 include_processing_rules=include_sensitive_info)
120 return resource_dict
123 return resource_dict
121
124
122
125
123 @view_config(route_name='applications_no_id', request_method="POST",
126 @view_config(route_name='applications_no_id', request_method="POST",
124 renderer='json', permission='create_resources')
127 renderer='json', permission='create_resources')
125 def application_create(request):
128 def application_create(request):
126 """
129 """
127 Creates new application instances
130 Creates new application instances
128 """
131 """
129 user = request.user
132 user = request.user
130 form = forms.ApplicationCreateForm(MultiDict(request.unsafe_json_body),
133 form = forms.ApplicationCreateForm(MultiDict(request.unsafe_json_body),
131 csrf_context=request)
134 csrf_context=request)
132 if form.validate():
135 if form.validate():
133 session = DBSession()
136 session = DBSession()
134 resource = Application()
137 resource = Application()
135 DBSession.add(resource)
138 DBSession.add(resource)
136 form.populate_obj(resource)
139 form.populate_obj(resource)
137 resource.api_key = resource.generate_api_key()
140 resource.api_key = resource.generate_api_key()
138 user.resources.append(resource)
141 user.resources.append(resource)
139 request.session.flash(_('Application created'))
142 request.session.flash(_('Application created'))
140 DBSession.flush()
143 DBSession.flush()
141 mark_changed(session)
144 mark_changed(session)
142 else:
145 else:
143 return HTTPUnprocessableEntity(body=form.errors_json)
146 return HTTPUnprocessableEntity(body=form.errors_json)
144
147
145 return resource.get_dict()
148 return resource.get_dict()
146
149
147
150
148 @view_config(route_name='applications', request_method="PATCH",
151 @view_config(route_name='applications', request_method="PATCH",
149 renderer='json', permission='edit')
152 renderer='json', permission='edit')
150 def application_update(request):
153 def application_update(request):
151 """
154 """
152 Updates main application configuration
155 Updates main application configuration
153 """
156 """
154 resource = request.context.resource
157 resource = request.context.resource
155 if not resource:
158 if not resource:
156 return app_not_found()
159 return app_not_found()
157
160
158 # disallow setting permanent storage by non-admins
161 # disallow setting permanent storage by non-admins
159 # use default/non-resource based context for this check
162 # use default/non-resource based context for this check
160 req_dict = copy.copy(request.unsafe_json_body)
163 req_dict = copy.copy(request.unsafe_json_body)
161 if not request.has_permission('root_administration', RootFactory(request)):
164 if not request.has_permission('root_administration', RootFactory(request)):
162 req_dict['allow_permanent_storage'] = ''
165 req_dict['allow_permanent_storage'] = ''
163 if not req_dict.get('uptime_url'):
166 if not req_dict.get('uptime_url'):
164 # needed cause validator is still triggered by default
167 # needed cause validator is still triggered by default
165 req_dict.pop('uptime_url', '')
168 req_dict.pop('uptime_url', '')
166 application_form = forms.ApplicationUpdateForm(MultiDict(req_dict),
169 application_form = forms.ApplicationUpdateForm(MultiDict(req_dict),
167 csrf_context=request)
170 csrf_context=request)
168 if application_form.validate():
171 if application_form.validate():
169 application_form.populate_obj(resource)
172 application_form.populate_obj(resource)
170 request.session.flash(_('Application updated'))
173 request.session.flash(_('Application updated'))
171 else:
174 else:
172 return HTTPUnprocessableEntity(body=application_form.errors_json)
175 return HTTPUnprocessableEntity(body=application_form.errors_json)
173
176
174 include_sensitive_info = False
177 include_sensitive_info = False
175 if request.has_permission('edit'):
178 if request.has_permission('edit'):
176 include_sensitive_info = True
179 include_sensitive_info = True
177 resource_dict = resource.get_dict(
180 resource_dict = resource.get_dict(
178 include_perms=include_sensitive_info,
181 include_perms=include_sensitive_info,
179 include_processing_rules=include_sensitive_info)
182 include_processing_rules=include_sensitive_info)
180 return resource_dict
183 return resource_dict
181
184
182
185
183 @view_config(route_name='applications_property', match_param='key=api_key',
186 @view_config(route_name='applications_property', match_param='key=api_key',
184 request_method="POST", renderer='json',
187 request_method="POST", renderer='json',
185 permission='delete')
188 permission='delete')
186 def application_regenerate_key(request):
189 def application_regenerate_key(request):
187 """
190 """
188 Regenerates API keys for application
191 Regenerates API keys for application
189 """
192 """
190 resource = request.context.resource
193 resource = request.context.resource
191
194
192 form = forms.CheckPasswordForm(MultiDict(request.unsafe_json_body),
195 form = forms.CheckPasswordForm(MultiDict(request.unsafe_json_body),
193 csrf_context=request)
196 csrf_context=request)
194 form.password.user = request.user
197 form.password.user = request.user
195
198
196 if form.validate():
199 if form.validate():
197 resource.api_key = resource.generate_api_key()
200 resource.api_key = resource.generate_api_key()
198 resource.public_key = resource.generate_api_key()
201 resource.public_key = resource.generate_api_key()
199 msg = 'API keys regenerated - please update your application config.'
202 msg = 'API keys regenerated - please update your application config.'
200 request.session.flash(_(msg))
203 request.session.flash(_(msg))
201 else:
204 else:
202 return HTTPUnprocessableEntity(body=form.errors_json)
205 return HTTPUnprocessableEntity(body=form.errors_json)
203
206
204 if request.has_permission('edit'):
207 if request.has_permission('edit'):
205 include_sensitive_info = True
208 include_sensitive_info = True
206 resource_dict = resource.get_dict(
209 resource_dict = resource.get_dict(
207 include_perms=include_sensitive_info,
210 include_perms=include_sensitive_info,
208 include_processing_rules=include_sensitive_info)
211 include_processing_rules=include_sensitive_info)
209 return resource_dict
212 return resource_dict
210
213
211
214
212 @view_config(route_name='applications_property',
215 @view_config(route_name='applications_property',
213 match_param='key=delete_resource',
216 match_param='key=delete_resource',
214 request_method="PATCH", renderer='json', permission='delete')
217 request_method="PATCH", renderer='json', permission='delete')
215 def application_remove(request):
218 def application_remove(request):
216 """
219 """
217 Removes application resources
220 Removes application resources
218 """
221 """
219 resource = request.context.resource
222 resource = request.context.resource
220 # we need polymorphic object here, to properly launch sqlalchemy events
223 # we need polymorphic object here, to properly launch sqlalchemy events
221 resource = ApplicationService.by_id(resource.resource_id)
224 resource = ApplicationService.by_id(resource.resource_id)
222 form = forms.CheckPasswordForm(MultiDict(request.safe_json_body or {}),
225 form = forms.CheckPasswordForm(MultiDict(request.safe_json_body or {}),
223 csrf_context=request)
226 csrf_context=request)
224 form.password.user = request.user
227 form.password.user = request.user
225 if form.validate():
228 if form.validate():
226 DBSession.delete(resource)
229 DBSession.delete(resource)
227 request.session.flash(_('Application removed'))
230 request.session.flash(_('Application removed'))
228 else:
231 else:
229 return HTTPUnprocessableEntity(body=form.errors_json)
232 return HTTPUnprocessableEntity(body=form.errors_json)
230
233
231 return True
234 return True
232
235
233
236
234 @view_config(route_name='applications_property', match_param='key=owner',
237 @view_config(route_name='applications_property', match_param='key=owner',
235 request_method="PATCH", renderer='json', permission='delete')
238 request_method="PATCH", renderer='json', permission='delete')
236 def application_ownership_transfer(request):
239 def application_ownership_transfer(request):
237 """
240 """
238 Allows application owner to transfer application ownership to other user
241 Allows application owner to transfer application ownership to other user
239 """
242 """
240 resource = request.context.resource
243 resource = request.context.resource
241 form = forms.ChangeApplicationOwnerForm(
244 form = forms.ChangeApplicationOwnerForm(
242 MultiDict(request.safe_json_body or {}), csrf_context=request)
245 MultiDict(request.safe_json_body or {}), csrf_context=request)
243 form.password.user = request.user
246 form.password.user = request.user
244 if form.validate():
247 if form.validate():
245 user = User.by_user_name(form.user_name.data)
248 user = UserService.by_user_name(form.user_name.data)
246 user.resources.append(resource)
249 user.resources.append(resource)
247 # remove integrations to not leak security data of external applications
250 # remove integrations to not leak security data of external applications
248 for integration in resource.integrations[:]:
251 for integration in resource.integrations[:]:
249 resource.integrations.remove(integration)
252 resource.integrations.remove(integration)
250 request.session.flash(_('Application transfered'))
253 request.session.flash(_('Application transfered'))
251 else:
254 else:
252 return HTTPUnprocessableEntity(body=form.errors_json)
255 return HTTPUnprocessableEntity(body=form.errors_json)
253 return True
256 return True
254
257
255
258
256 @view_config(route_name='applications_property',
259 @view_config(route_name='applications_property',
257 match_param='key=postprocessing_rules', renderer='json',
260 match_param='key=postprocessing_rules', renderer='json',
258 request_method='POST', permission='edit')
261 request_method='POST', permission='edit')
259 def applications_postprocess_POST(request):
262 def applications_postprocess_POST(request):
260 """
263 """
261 Creates new postprocessing rules for applications
264 Creates new postprocessing rules for applications
262 """
265 """
263 resource = request.context.resource
266 resource = request.context.resource
264 conf = ApplicationPostprocessConf()
267 conf = ApplicationPostprocessConf()
265 conf.do = 'postprocess'
268 conf.do = 'postprocess'
266 conf.new_value = '1'
269 conf.new_value = '1'
267 resource.postprocess_conf.append(conf)
270 resource.postprocess_conf.append(conf)
268 DBSession.flush()
271 DBSession.flush()
269 return conf.get_dict()
272 return conf.get_dict()
270
273
271
274
272 @view_config(route_name='applications_property',
275 @view_config(route_name='applications_property',
273 match_param='key=postprocessing_rules', renderer='json',
276 match_param='key=postprocessing_rules', renderer='json',
274 request_method='PATCH', permission='edit')
277 request_method='PATCH', permission='edit')
275 def applications_postprocess_PATCH(request):
278 def applications_postprocess_PATCH(request):
276 """
279 """
277 Creates new postprocessing rules for applications
280 Creates new postprocessing rules for applications
278 """
281 """
279 json_body = request.unsafe_json_body
282 json_body = request.unsafe_json_body
280
283
281 schema = build_rule_schema(json_body['rule'], REPORT_TYPE_MATRIX)
284 schema = build_rule_schema(json_body['rule'], REPORT_TYPE_MATRIX)
282 try:
285 try:
283 schema.deserialize(json_body['rule'])
286 schema.deserialize(json_body['rule'])
284 except colander.Invalid as exc:
287 except colander.Invalid as exc:
285 return HTTPUnprocessableEntity(body=json.dumps(exc.asdict()))
288 return HTTPUnprocessableEntity(body=json.dumps(exc.asdict()))
286
289
287 resource = request.context.resource
290 resource = request.context.resource
288 conf = ApplicationPostprocessConfService.by_pkey_and_resource_id(
291 conf = ApplicationPostprocessConfService.by_pkey_and_resource_id(
289 json_body['pkey'], resource.resource_id)
292 json_body['pkey'], resource.resource_id)
290 conf.rule = request.unsafe_json_body['rule']
293 conf.rule = request.unsafe_json_body['rule']
291 # for now hardcode int since we dont support anything else so far
294 # for now hardcode int since we dont support anything else so far
292 conf.new_value = int(request.unsafe_json_body['new_value'])
295 conf.new_value = int(request.unsafe_json_body['new_value'])
293 return conf.get_dict()
296 return conf.get_dict()
294
297
295
298
296 @view_config(route_name='applications_property',
299 @view_config(route_name='applications_property',
297 match_param='key=postprocessing_rules', renderer='json',
300 match_param='key=postprocessing_rules', renderer='json',
298 request_method='DELETE', permission='edit')
301 request_method='DELETE', permission='edit')
299 def applications_postprocess_DELETE(request):
302 def applications_postprocess_DELETE(request):
300 """
303 """
301 Removes application postprocessing rules
304 Removes application postprocessing rules
302 """
305 """
303 form = forms.ReactorForm(request.POST, csrf_context=request)
306 form = forms.ReactorForm(request.POST, csrf_context=request)
304 resource = request.context.resource
307 resource = request.context.resource
305 if form.validate():
308 if form.validate():
306 for postprocess_conf in resource.postprocess_conf:
309 for postprocess_conf in resource.postprocess_conf:
307 if postprocess_conf.pkey == int(request.GET['pkey']):
310 if postprocess_conf.pkey == int(request.GET['pkey']):
308 # remove rule
311 # remove rule
309 DBSession.delete(postprocess_conf)
312 DBSession.delete(postprocess_conf)
310 return True
313 return True
311
314
312
315
313 @view_config(route_name='applications_property',
316 @view_config(route_name='applications_property',
314 match_param='key=report_graphs', renderer='json',
317 match_param='key=report_graphs', renderer='json',
315 permission='view')
318 permission='view')
316 @view_config(route_name='applications_property',
319 @view_config(route_name='applications_property',
317 match_param='key=slow_report_graphs', renderer='json',
320 match_param='key=slow_report_graphs', renderer='json',
318 permission='view')
321 permission='view')
319 def get_application_report_stats(request):
322 def get_application_report_stats(request):
320 query_params = request.GET.mixed()
323 query_params = request.GET.mixed()
321 query_params['resource'] = (request.context.resource.resource_id,)
324 query_params['resource'] = (request.context.resource.resource_id,)
322
325
323 filter_settings = build_filter_settings_from_query_dict(request,
326 filter_settings = build_filter_settings_from_query_dict(request,
324 query_params)
327 query_params)
325 if not filter_settings.get('end_date'):
328 if not filter_settings.get('end_date'):
326 end_date = datetime.utcnow().replace(microsecond=0, second=0)
329 end_date = datetime.utcnow().replace(microsecond=0, second=0)
327 filter_settings['end_date'] = end_date
330 filter_settings['end_date'] = end_date
328
331
329 if not filter_settings.get('start_date'):
332 if not filter_settings.get('start_date'):
330 delta = timedelta(hours=1)
333 delta = timedelta(hours=1)
331 filter_settings['start_date'] = filter_settings['end_date'] - delta
334 filter_settings['start_date'] = filter_settings['end_date'] - delta
332
335
333 result = ReportGroupService.get_report_stats(request, filter_settings)
336 result = ReportGroupService.get_report_stats(request, filter_settings)
334 return result
337 return result
335
338
336
339
337 @view_config(route_name='applications_property',
340 @view_config(route_name='applications_property',
338 match_param='key=metrics_graphs', renderer='json',
341 match_param='key=metrics_graphs', renderer='json',
339 permission='view')
342 permission='view')
340 def metrics_graphs(request):
343 def metrics_graphs(request):
341 """
344 """
342 Handles metric dashboard graphs
345 Handles metric dashboard graphs
343 Returns information for time/tier breakdown
346 Returns information for time/tier breakdown
344 """
347 """
345 query_params = request.GET.mixed()
348 query_params = request.GET.mixed()
346 query_params['resource'] = (request.context.resource.resource_id,)
349 query_params['resource'] = (request.context.resource.resource_id,)
347
350
348 filter_settings = build_filter_settings_from_query_dict(request,
351 filter_settings = build_filter_settings_from_query_dict(request,
349 query_params)
352 query_params)
350
353
351 if not filter_settings.get('end_date'):
354 if not filter_settings.get('end_date'):
352 end_date = datetime.utcnow().replace(microsecond=0, second=0)
355 end_date = datetime.utcnow().replace(microsecond=0, second=0)
353 filter_settings['end_date'] = end_date
356 filter_settings['end_date'] = end_date
354
357
355 delta = timedelta(hours=1)
358 delta = timedelta(hours=1)
356 if not filter_settings.get('start_date'):
359 if not filter_settings.get('start_date'):
357 filter_settings['start_date'] = filter_settings['end_date'] - delta
360 filter_settings['start_date'] = filter_settings['end_date'] - delta
358 if filter_settings['end_date'] <= filter_settings['start_date']:
361 if filter_settings['end_date'] <= filter_settings['start_date']:
359 filter_settings['end_date'] = filter_settings['start_date']
362 filter_settings['end_date'] = filter_settings['start_date']
360
363
361 delta = filter_settings['end_date'] - filter_settings['start_date']
364 delta = filter_settings['end_date'] - filter_settings['start_date']
362 if delta < h.time_deltas.get('12h')['delta']:
365 if delta < h.time_deltas.get('12h')['delta']:
363 divide_by_min = 1
366 divide_by_min = 1
364 elif delta <= h.time_deltas.get('3d')['delta']:
367 elif delta <= h.time_deltas.get('3d')['delta']:
365 divide_by_min = 5.0
368 divide_by_min = 5.0
366 elif delta >= h.time_deltas.get('2w')['delta']:
369 elif delta >= h.time_deltas.get('2w')['delta']:
367 divide_by_min = 60.0 * 24
370 divide_by_min = 60.0 * 24
368 else:
371 else:
369 divide_by_min = 60.0
372 divide_by_min = 60.0
370
373
371 results = RequestMetricService.get_metrics_stats(
374 results = RequestMetricService.get_metrics_stats(
372 request, filter_settings)
375 request, filter_settings)
373 # because requests are PER SECOND / we divide 1 min stats by 60
376 # because requests are PER SECOND / we divide 1 min stats by 60
374 # requests are normalized to 1 min average
377 # requests are normalized to 1 min average
375 # results are average seconds time spent per request in specific area
378 # results are average seconds time spent per request in specific area
376 for point in results:
379 for point in results:
377 if point['requests']:
380 if point['requests']:
378 point['main'] = (point['main'] - point['sql'] -
381 point['main'] = (point['main'] - point['sql'] -
379 point['nosql'] - point['remote'] -
382 point['nosql'] - point['remote'] -
380 point['tmpl'] -
383 point['tmpl'] -
381 point['custom']) / point['requests']
384 point['custom']) / point['requests']
382 point['sql'] = point['sql'] / point['requests']
385 point['sql'] = point['sql'] / point['requests']
383 point['nosql'] = point['nosql'] / point['requests']
386 point['nosql'] = point['nosql'] / point['requests']
384 point['remote'] = point['remote'] / point['requests']
387 point['remote'] = point['remote'] / point['requests']
385 point['tmpl'] = point['tmpl'] / point['requests']
388 point['tmpl'] = point['tmpl'] / point['requests']
386 point['custom'] = point['custom'] / point['requests']
389 point['custom'] = point['custom'] / point['requests']
387 point['requests_2'] = point['requests'] / 60.0 / divide_by_min
390 point['requests_2'] = point['requests'] / 60.0 / divide_by_min
388
391
389 selected_types = ['main', 'sql', 'nosql', 'remote', 'tmpl', 'custom']
392 selected_types = ['main', 'sql', 'nosql', 'remote', 'tmpl', 'custom']
390
393
391 for point in results:
394 for point in results:
392 for stat_type in selected_types:
395 for stat_type in selected_types:
393 point[stat_type] = round(point.get(stat_type, 0), 3)
396 point[stat_type] = round(point.get(stat_type, 0), 3)
394
397
395 return results
398 return results
396
399
397
400
398 @view_config(route_name='applications_property',
401 @view_config(route_name='applications_property',
399 match_param='key=response_graphs', renderer='json',
402 match_param='key=response_graphs', renderer='json',
400 permission='view')
403 permission='view')
401 def response_graphs(request):
404 def response_graphs(request):
402 """
405 """
403 Handles dashboard infomation for avg. response time split by today,
406 Handles dashboard infomation for avg. response time split by today,
404 2 days ago and week ago
407 2 days ago and week ago
405 """
408 """
406 query_params = request.GET.mixed()
409 query_params = request.GET.mixed()
407 query_params['resource'] = (request.context.resource.resource_id,)
410 query_params['resource'] = (request.context.resource.resource_id,)
408
411
409 filter_settings = build_filter_settings_from_query_dict(request,
412 filter_settings = build_filter_settings_from_query_dict(request,
410 query_params)
413 query_params)
411
414
412 if not filter_settings.get('end_date'):
415 if not filter_settings.get('end_date'):
413 end_date = datetime.utcnow().replace(microsecond=0, second=0)
416 end_date = datetime.utcnow().replace(microsecond=0, second=0)
414 filter_settings['end_date'] = end_date
417 filter_settings['end_date'] = end_date
415
418
416 delta = timedelta(hours=1)
419 delta = timedelta(hours=1)
417 if not filter_settings.get('start_date'):
420 if not filter_settings.get('start_date'):
418 filter_settings['start_date'] = filter_settings['end_date'] - delta
421 filter_settings['start_date'] = filter_settings['end_date'] - delta
419
422
420 result_now = RequestMetricService.get_metrics_stats(
423 result_now = RequestMetricService.get_metrics_stats(
421 request, filter_settings)
424 request, filter_settings)
422
425
423 filter_settings_2d = filter_settings.copy()
426 filter_settings_2d = filter_settings.copy()
424 filter_settings_2d['start_date'] = filter_settings['start_date'] - \
427 filter_settings_2d['start_date'] = filter_settings['start_date'] - \
425 timedelta(days=2)
428 timedelta(days=2)
426 filter_settings_2d['end_date'] = filter_settings['end_date'] - \
429 filter_settings_2d['end_date'] = filter_settings['end_date'] - \
427 timedelta(days=2)
430 timedelta(days=2)
428 result_2d = RequestMetricService.get_metrics_stats(
431 result_2d = RequestMetricService.get_metrics_stats(
429 request, filter_settings_2d)
432 request, filter_settings_2d)
430
433
431 filter_settings_7d = filter_settings.copy()
434 filter_settings_7d = filter_settings.copy()
432 filter_settings_7d['start_date'] = filter_settings['start_date'] - \
435 filter_settings_7d['start_date'] = filter_settings['start_date'] - \
433 timedelta(days=7)
436 timedelta(days=7)
434 filter_settings_7d['end_date'] = filter_settings['end_date'] - \
437 filter_settings_7d['end_date'] = filter_settings['end_date'] - \
435 timedelta(days=7)
438 timedelta(days=7)
436 result_7d = RequestMetricService.get_metrics_stats(
439 result_7d = RequestMetricService.get_metrics_stats(
437 request, filter_settings_7d)
440 request, filter_settings_7d)
438
441
439 plot_data = []
442 plot_data = []
440
443
441 for item in result_now:
444 for item in result_now:
442 point = {'x': item['x'], 'today': 0, 'days_ago_2': 0,
445 point = {'x': item['x'], 'today': 0, 'days_ago_2': 0,
443 'days_ago_7': 0}
446 'days_ago_7': 0}
444 if item['requests']:
447 if item['requests']:
445 point['today'] = round(item['main'] / item['requests'], 3)
448 point['today'] = round(item['main'] / item['requests'], 3)
446 plot_data.append(point)
449 plot_data.append(point)
447
450
448 for i, item in enumerate(result_2d[:len(plot_data)]):
451 for i, item in enumerate(result_2d[:len(plot_data)]):
449 plot_data[i]['days_ago_2'] = 0
452 plot_data[i]['days_ago_2'] = 0
450 point = result_2d[i]
453 point = result_2d[i]
451 if point['requests']:
454 if point['requests']:
452 plot_data[i]['days_ago_2'] = round(point['main'] /
455 plot_data[i]['days_ago_2'] = round(point['main'] /
453 point['requests'], 3)
456 point['requests'], 3)
454
457
455 for i, item in enumerate(result_7d[:len(plot_data)]):
458 for i, item in enumerate(result_7d[:len(plot_data)]):
456 plot_data[i]['days_ago_7'] = 0
459 plot_data[i]['days_ago_7'] = 0
457 point = result_7d[i]
460 point = result_7d[i]
458 if point['requests']:
461 if point['requests']:
459 plot_data[i]['days_ago_7'] = round(point['main'] /
462 plot_data[i]['days_ago_7'] = round(point['main'] /
460 point['requests'], 3)
463 point['requests'], 3)
461
464
462 return plot_data
465 return plot_data
463
466
464
467
465 @view_config(route_name='applications_property',
468 @view_config(route_name='applications_property',
466 match_param='key=requests_graphs', renderer='json',
469 match_param='key=requests_graphs', renderer='json',
467 permission='view')
470 permission='view')
468 def requests_graphs(request):
471 def requests_graphs(request):
469 """
472 """
470 Handles dashboard infomation for avg. response time split by today,
473 Handles dashboard infomation for avg. response time split by today,
471 2 days ago and week ago
474 2 days ago and week ago
472 """
475 """
473 query_params = request.GET.mixed()
476 query_params = request.GET.mixed()
474 query_params['resource'] = (request.context.resource.resource_id,)
477 query_params['resource'] = (request.context.resource.resource_id,)
475
478
476 filter_settings = build_filter_settings_from_query_dict(request,
479 filter_settings = build_filter_settings_from_query_dict(request,
477 query_params)
480 query_params)
478
481
479 if not filter_settings.get('end_date'):
482 if not filter_settings.get('end_date'):
480 end_date = datetime.utcnow().replace(microsecond=0, second=0)
483 end_date = datetime.utcnow().replace(microsecond=0, second=0)
481 filter_settings['end_date'] = end_date
484 filter_settings['end_date'] = end_date
482
485
483 delta = timedelta(hours=1)
486 delta = timedelta(hours=1)
484 if not filter_settings.get('start_date'):
487 if not filter_settings.get('start_date'):
485 filter_settings['start_date'] = filter_settings['end_date'] - delta
488 filter_settings['start_date'] = filter_settings['end_date'] - delta
486
489
487 result_now = RequestMetricService.get_metrics_stats(
490 result_now = RequestMetricService.get_metrics_stats(
488 request, filter_settings)
491 request, filter_settings)
489
492
490 delta = filter_settings['end_date'] - filter_settings['start_date']
493 delta = filter_settings['end_date'] - filter_settings['start_date']
491 if delta < h.time_deltas.get('12h')['delta']:
494 if delta < h.time_deltas.get('12h')['delta']:
492 seconds = h.time_deltas['1m']['minutes'] * 60.0
495 seconds = h.time_deltas['1m']['minutes'] * 60.0
493 elif delta <= h.time_deltas.get('3d')['delta']:
496 elif delta <= h.time_deltas.get('3d')['delta']:
494 seconds = h.time_deltas['5m']['minutes'] * 60.0
497 seconds = h.time_deltas['5m']['minutes'] * 60.0
495 elif delta >= h.time_deltas.get('2w')['delta']:
498 elif delta >= h.time_deltas.get('2w')['delta']:
496 seconds = h.time_deltas['24h']['minutes'] * 60.0
499 seconds = h.time_deltas['24h']['minutes'] * 60.0
497 else:
500 else:
498 seconds = h.time_deltas['1h']['minutes'] * 60.0
501 seconds = h.time_deltas['1h']['minutes'] * 60.0
499
502
500 for item in result_now:
503 for item in result_now:
501 if item['requests']:
504 if item['requests']:
502 item['requests'] = round(item['requests'] / seconds, 3)
505 item['requests'] = round(item['requests'] / seconds, 3)
503 return result_now
506 return result_now
504
507
505
508
506 @view_config(route_name='applications_property',
509 @view_config(route_name='applications_property',
507 match_param='key=apdex_stats', renderer='json',
510 match_param='key=apdex_stats', renderer='json',
508 permission='view')
511 permission='view')
509 def get_apdex_stats(request):
512 def get_apdex_stats(request):
510 """
513 """
511 Returns information and calculates APDEX score per server for dashboard
514 Returns information and calculates APDEX score per server for dashboard
512 server information (upper right stats boxes)
515 server information (upper right stats boxes)
513 """
516 """
514 query_params = request.GET.mixed()
517 query_params = request.GET.mixed()
515 query_params['resource'] = (request.context.resource.resource_id,)
518 query_params['resource'] = (request.context.resource.resource_id,)
516
519
517 filter_settings = build_filter_settings_from_query_dict(request,
520 filter_settings = build_filter_settings_from_query_dict(request,
518 query_params)
521 query_params)
519 # make sure we have only one resource here to don't produce
522 # make sure we have only one resource here to don't produce
520 # weird results when we have wrong app in app selector
523 # weird results when we have wrong app in app selector
521 filter_settings['resource'] = [filter_settings['resource'][0]]
524 filter_settings['resource'] = [filter_settings['resource'][0]]
522
525
523 if not filter_settings.get('end_date'):
526 if not filter_settings.get('end_date'):
524 end_date = datetime.utcnow().replace(microsecond=0, second=0)
527 end_date = datetime.utcnow().replace(microsecond=0, second=0)
525 filter_settings['end_date'] = end_date
528 filter_settings['end_date'] = end_date
526
529
527 delta = timedelta(hours=1)
530 delta = timedelta(hours=1)
528 if not filter_settings.get('start_date'):
531 if not filter_settings.get('start_date'):
529 filter_settings['start_date'] = filter_settings['end_date'] - delta
532 filter_settings['start_date'] = filter_settings['end_date'] - delta
530
533
531 return RequestMetricService.get_apdex_stats(request, filter_settings)
534 return RequestMetricService.get_apdex_stats(request, filter_settings)
532
535
533
536
534 @view_config(route_name='applications_property', match_param='key=slow_calls',
537 @view_config(route_name='applications_property', match_param='key=slow_calls',
535 renderer='json', permission='view')
538 renderer='json', permission='view')
536 def get_slow_calls(request):
539 def get_slow_calls(request):
537 """
540 """
538 Returns information for time consuming calls in specific time interval
541 Returns information for time consuming calls in specific time interval
539 """
542 """
540 query_params = request.GET.mixed()
543 query_params = request.GET.mixed()
541 query_params['resource'] = (request.context.resource.resource_id,)
544 query_params['resource'] = (request.context.resource.resource_id,)
542
545
543 filter_settings = build_filter_settings_from_query_dict(request,
546 filter_settings = build_filter_settings_from_query_dict(request,
544 query_params)
547 query_params)
545
548
546 if not filter_settings.get('end_date'):
549 if not filter_settings.get('end_date'):
547 end_date = datetime.utcnow().replace(microsecond=0, second=0)
550 end_date = datetime.utcnow().replace(microsecond=0, second=0)
548 filter_settings['end_date'] = end_date
551 filter_settings['end_date'] = end_date
549
552
550 delta = timedelta(hours=1)
553 delta = timedelta(hours=1)
551 if not filter_settings.get('start_date'):
554 if not filter_settings.get('start_date'):
552 filter_settings['start_date'] = filter_settings['end_date'] - delta
555 filter_settings['start_date'] = filter_settings['end_date'] - delta
553
556
554 return SlowCallService.get_time_consuming_calls(request, filter_settings)
557 return SlowCallService.get_time_consuming_calls(request, filter_settings)
555
558
556
559
557 @view_config(route_name='applications_property',
560 @view_config(route_name='applications_property',
558 match_param='key=requests_breakdown',
561 match_param='key=requests_breakdown',
559 renderer='json', permission='view')
562 renderer='json', permission='view')
560 def get_requests_breakdown(request):
563 def get_requests_breakdown(request):
561 """
564 """
562 Used on dashboard to get information which views are most used in
565 Used on dashboard to get information which views are most used in
563 a time interval
566 a time interval
564 """
567 """
565 query_params = request.GET.mixed()
568 query_params = request.GET.mixed()
566 query_params['resource'] = (request.context.resource.resource_id,)
569 query_params['resource'] = (request.context.resource.resource_id,)
567
570
568 filter_settings = build_filter_settings_from_query_dict(request,
571 filter_settings = build_filter_settings_from_query_dict(request,
569 query_params)
572 query_params)
570 if not filter_settings.get('end_date'):
573 if not filter_settings.get('end_date'):
571 end_date = datetime.utcnow().replace(microsecond=0, second=0)
574 end_date = datetime.utcnow().replace(microsecond=0, second=0)
572 filter_settings['end_date'] = end_date
575 filter_settings['end_date'] = end_date
573
576
574 if not filter_settings.get('start_date'):
577 if not filter_settings.get('start_date'):
575 delta = timedelta(hours=1)
578 delta = timedelta(hours=1)
576 filter_settings['start_date'] = filter_settings['end_date'] - delta
579 filter_settings['start_date'] = filter_settings['end_date'] - delta
577
580
578 series = RequestMetricService.get_requests_breakdown(
581 series = RequestMetricService.get_requests_breakdown(
579 request, filter_settings)
582 request, filter_settings)
580
583
581 results = []
584 results = []
582 for row in series:
585 for row in series:
583 d_row = {'avg_response': round(row['main'] / row['requests'], 3),
586 d_row = {'avg_response': round(row['main'] / row['requests'], 3),
584 'requests': row['requests'],
587 'requests': row['requests'],
585 'main': row['main'],
588 'main': row['main'],
586 'view_name': row['key'],
589 'view_name': row['key'],
587 'latest_details': row['latest_details'],
590 'latest_details': row['latest_details'],
588 'percentage': round(row['percentage'] * 100, 1)}
591 'percentage': round(row['percentage'] * 100, 1)}
589
592
590 results.append(d_row)
593 results.append(d_row)
591
594
592 return results
595 return results
593
596
594
597
595 @view_config(route_name='applications_property',
598 @view_config(route_name='applications_property',
596 match_param='key=trending_reports', renderer='json',
599 match_param='key=trending_reports', renderer='json',
597 permission='view')
600 permission='view')
598 def trending_reports(request):
601 def trending_reports(request):
599 """
602 """
600 Returns exception/slow reports trending for specific time interval
603 Returns exception/slow reports trending for specific time interval
601 """
604 """
602 query_params = request.GET.mixed().copy()
605 query_params = request.GET.mixed().copy()
603 # pop report type to rewrite it to tag later
606 # pop report type to rewrite it to tag later
604 report_type = query_params.pop('report_type', None)
607 report_type = query_params.pop('report_type', None)
605 if report_type:
608 if report_type:
606 query_params['type'] = report_type
609 query_params['type'] = report_type
607
610
608 query_params['resource'] = (request.context.resource.resource_id,)
611 query_params['resource'] = (request.context.resource.resource_id,)
609
612
610 filter_settings = build_filter_settings_from_query_dict(request,
613 filter_settings = build_filter_settings_from_query_dict(request,
611 query_params)
614 query_params)
612
615
613 if not filter_settings.get('end_date'):
616 if not filter_settings.get('end_date'):
614 end_date = datetime.utcnow().replace(microsecond=0, second=0)
617 end_date = datetime.utcnow().replace(microsecond=0, second=0)
615 filter_settings['end_date'] = end_date
618 filter_settings['end_date'] = end_date
616
619
617 if not filter_settings.get('start_date'):
620 if not filter_settings.get('start_date'):
618 delta = timedelta(hours=1)
621 delta = timedelta(hours=1)
619 filter_settings['start_date'] = filter_settings['end_date'] - delta
622 filter_settings['start_date'] = filter_settings['end_date'] - delta
620
623
621 results = ReportGroupService.get_trending(request, filter_settings)
624 results = ReportGroupService.get_trending(request, filter_settings)
622
625
623 trending = []
626 trending = []
624 for occurences, group in results:
627 for occurences, group in results:
625 report_group = group.get_dict(request)
628 report_group = group.get_dict(request)
626 # show the occurences in time range instead of global ones
629 # show the occurences in time range instead of global ones
627 report_group['occurences'] = occurences
630 report_group['occurences'] = occurences
628 trending.append(report_group)
631 trending.append(report_group)
629
632
630 return trending
633 return trending
631
634
632
635
633 @view_config(route_name='applications_property',
636 @view_config(route_name='applications_property',
634 match_param='key=integrations',
637 match_param='key=integrations',
635 renderer='json', permission='view')
638 renderer='json', permission='view')
636 def integrations(request):
639 def integrations(request):
637 """
640 """
638 Integration list for given application
641 Integration list for given application
639 """
642 """
640 application = request.context.resource
643 application = request.context.resource
641 return {'resource': application}
644 return {'resource': application}
642
645
643
646
644 @view_config(route_name='applications_property',
647 @view_config(route_name='applications_property',
645 match_param='key=user_permissions', renderer='json',
648 match_param='key=user_permissions', renderer='json',
646 permission='owner', request_method='POST')
649 permission='owner', request_method='POST')
647 def user_resource_permission_create(request):
650 def user_resource_permission_create(request):
648 """
651 """
649 Set new permissions for user for a resource
652 Set new permissions for user for a resource
650 """
653 """
651 resource = request.context.resource
654 resource = request.context.resource
652 user_name = request.unsafe_json_body.get('user_name')
655 user_name = request.unsafe_json_body.get('user_name')
653 user = User.by_user_name(user_name)
656 user = UserService.by_user_name(user_name)
654 if not user:
657 if not user:
655 user = User.by_email(user_name)
658 user = UserService.by_email(user_name)
656 if not user:
659 if not user:
657 return False
660 return False
658
661
659 for perm_name in request.unsafe_json_body.get('permissions', []):
662 for perm_name in request.unsafe_json_body.get('permissions', []):
660 permission = UserResourcePermission.by_resource_user_and_perm(
663 permission = UserResourcePermissionService.by_resource_user_and_perm(
661 user.id, perm_name, resource.resource_id)
664 user.id, perm_name, resource.resource_id)
662 if not permission:
665 if not permission:
663 permission = UserResourcePermission(perm_name=perm_name,
666 permission = UserResourcePermission(perm_name=perm_name,
664 user_id=user.id)
667 user_id=user.id)
665 resource.user_permissions.append(permission)
668 resource.user_permissions.append(permission)
666 DBSession.flush()
669 DBSession.flush()
667 perms = [p.perm_name for p in resource.perms_for_user(user)
670 perms = [p.perm_name for p in ResourceService.perms_for_user(resource, user)
668 if p.type == 'user']
671 if p.type == 'user']
669 result = {'user_name': user.user_name,
672 result = {'user_name': user.user_name,
670 'permissions': list(set(perms))}
673 'permissions': list(set(perms))}
671 return result
674 return result
672
675
673
676
674 @view_config(route_name='applications_property',
677 @view_config(route_name='applications_property',
675 match_param='key=user_permissions', renderer='json',
678 match_param='key=user_permissions', renderer='json',
676 permission='owner', request_method='DELETE')
679 permission='owner', request_method='DELETE')
677 def user_resource_permission_delete(request):
680 def user_resource_permission_delete(request):
678 """
681 """
679 Removes user permission from specific resource
682 Removes user permission from specific resource
680 """
683 """
681 resource = request.context.resource
684 resource = request.context.resource
682
685
683 user = User.by_user_name(request.GET.get('user_name'))
686 user = UserService.by_user_name(request.GET.get('user_name'))
684 if not user:
687 if not user:
685 return False
688 return False
686
689
687 for perm_name in request.GET.getall('permissions'):
690 for perm_name in request.GET.getall('permissions'):
688 permission = UserResourcePermission.by_resource_user_and_perm(
691 permission = UserResourcePermissionService.by_resource_user_and_perm(
689 user.id, perm_name, resource.resource_id)
692 user.id, perm_name, resource.resource_id)
690 resource.user_permissions.remove(permission)
693 resource.user_permissions.remove(permission)
691 DBSession.flush()
694 DBSession.flush()
692 perms = [p.perm_name for p in resource.perms_for_user(user)
695 perms = [p.perm_name for p in ResourceService.perms_for_user(resource, user)
693 if p.type == 'user']
696 if p.type == 'user']
694 result = {'user_name': user.user_name,
697 result = {'user_name': user.user_name,
695 'permissions': list(set(perms))}
698 'permissions': list(set(perms))}
696 return result
699 return result
697
700
698
701
699 @view_config(route_name='applications_property',
702 @view_config(route_name='applications_property',
700 match_param='key=group_permissions', renderer='json',
703 match_param='key=group_permissions', renderer='json',
701 permission='owner', request_method='POST')
704 permission='owner', request_method='POST')
702 def group_resource_permission_create(request):
705 def group_resource_permission_create(request):
703 """
706 """
704 Set new permissions for group for a resource
707 Set new permissions for group for a resource
705 """
708 """
706 resource = request.context.resource
709 resource = request.context.resource
707 group = GroupService.by_id(request.unsafe_json_body.get('group_id'))
710 group = GroupService.by_id(request.unsafe_json_body.get('group_id'))
708 if not group:
711 if not group:
709 return False
712 return False
710
713
711 for perm_name in request.unsafe_json_body.get('permissions', []):
714 for perm_name in request.unsafe_json_body.get('permissions', []):
712 permission = GroupResourcePermissionService.by_resource_group_and_perm(
715 permission = GroupResourcePermissionService.by_resource_group_and_perm(
713 group.id, perm_name, resource.resource_id)
716 group.id, perm_name, resource.resource_id)
714 if not permission:
717 if not permission:
715 permission = GroupResourcePermission(perm_name=perm_name,
718 permission = GroupResourcePermission(perm_name=perm_name,
716 group_id=group.id)
719 group_id=group.id)
717 resource.group_permissions.append(permission)
720 resource.group_permissions.append(permission)
718 DBSession.flush()
721 DBSession.flush()
719 perm_tuples = resource.groups_for_perm(
722 perm_tuples = ResourceService.groups_for_perm(
723 resource,
720 ANY_PERMISSION,
724 ANY_PERMISSION,
721 limit_group_permissions=True,
725 limit_group_permissions=True,
722 group_ids=[group.id])
726 group_ids=[group.id])
723 perms = [p.perm_name for p in perm_tuples if p.type == 'group']
727 perms = [p.perm_name for p in perm_tuples if p.type == 'group']
724 result = {'group': group.get_dict(),
728 result = {'group': group.get_dict(),
725 'permissions': list(set(perms))}
729 'permissions': list(set(perms))}
726 return result
730 return result
727
731
728
732
729 @view_config(route_name='applications_property',
733 @view_config(route_name='applications_property',
730 match_param='key=group_permissions', renderer='json',
734 match_param='key=group_permissions', renderer='json',
731 permission='owner', request_method='DELETE')
735 permission='owner', request_method='DELETE')
732 def group_resource_permission_delete(request):
736 def group_resource_permission_delete(request):
733 """
737 """
734 Removes group permission from specific resource
738 Removes group permission from specific resource
735 """
739 """
736 form = forms.ReactorForm(request.POST, csrf_context=request)
740 form = forms.ReactorForm(request.POST, csrf_context=request)
737 form.validate()
741 form.validate()
738 resource = request.context.resource
742 resource = request.context.resource
739 group = GroupService.by_id(request.GET.get('group_id'))
743 group = GroupService.by_id(request.GET.get('group_id'))
740 if not group:
744 if not group:
741 return False
745 return False
742
746
743 for perm_name in request.GET.getall('permissions'):
747 for perm_name in request.GET.getall('permissions'):
744 permission = GroupResourcePermissionService.by_resource_group_and_perm(
748 permission = GroupResourcePermissionService.by_resource_group_and_perm(
745 group.id, perm_name, resource.resource_id)
749 group.id, perm_name, resource.resource_id)
746 resource.group_permissions.remove(permission)
750 resource.group_permissions.remove(permission)
747 DBSession.flush()
751 DBSession.flush()
748 perm_tuples = resource.groups_for_perm(
752 perm_tuples = ResourceService.groups_for_perm(
753 resource,
749 ANY_PERMISSION,
754 ANY_PERMISSION,
750 limit_group_permissions=True,
755 limit_group_permissions=True,
751 group_ids=[group.id])
756 group_ids=[group.id])
752 perms = [p.perm_name for p in perm_tuples if p.type == 'group']
757 perms = [p.perm_name for p in perm_tuples if p.type == 'group']
753 result = {'group': group.get_dict(),
758 result = {'group': group.get_dict(),
754 'permissions': list(set(perms))}
759 'permissions': list(set(perms))}
755 return result
760 return result
@@ -1,59 +1,60 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 from appenlight.lib.helpers import gen_pagination_headers
17 from appenlight.lib.helpers import gen_pagination_headers
18 from appenlight.models.services.event import EventService
18 from appenlight.models.services.event import EventService
19 from pyramid.view import view_config
19 from pyramid.view import view_config
20 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound
20 from pyramid.httpexceptions import HTTPBadRequest, HTTPNotFound
21 from ziggurat_foundations.models.services.user import UserService
21
22
22
23
23 @view_config(route_name='events_no_id',
24 @view_config(route_name='events_no_id',
24 renderer='json', permission='authenticated')
25 renderer='json', permission='authenticated')
25 def fetch_events(request):
26 def fetch_events(request):
26 """
27 """
27 Returns list of log entries from Elasticsearch
28 Returns list of log entries from Elasticsearch
28 """
29 """
29 event_paginator = EventService.get_paginator(
30 event_paginator = EventService.get_paginator(
30 user=request.user,
31 user=request.user,
31 page=1,
32 page=1,
32 items_per_page=100
33 items_per_page=100
33 )
34 )
34 headers = gen_pagination_headers(request, event_paginator)
35 headers = gen_pagination_headers(request, event_paginator)
35 request.response.headers.update(headers)
36 request.response.headers.update(headers)
36
37
37 return [ev.get_dict() for ev in event_paginator.items]
38 return [ev.get_dict() for ev in event_paginator.items]
38
39
39
40
40 @view_config(route_name='events', renderer='json', request_method='PATCH',
41 @view_config(route_name='events', renderer='json', request_method='PATCH',
41 permission='authenticated')
42 permission='authenticated')
42 def event_PATCH(request):
43 def event_PATCH(request):
43 resources = request.user.resources_with_perms(
44 resources = UserService.resources_with_perms(
44 ['view'], resource_types=request.registry.resource_types)
45 request.user, ['view'], resource_types=request.registry.resource_types)
45 event = EventService.for_resource(
46 event = EventService.for_resource(
46 [r.resource_id for r in resources],
47 [r.resource_id for r in resources],
47 event_id=request.matchdict['event_id']).first()
48 event_id=request.matchdict['event_id']).first()
48 if not event:
49 if not event:
49 return HTTPNotFound()
50 return HTTPNotFound()
50 allowed_keys = ['status']
51 allowed_keys = ['status']
51 for k, v in request.unsafe_json_body.items():
52 for k, v in request.unsafe_json_body.items():
52 if k in allowed_keys:
53 if k in allowed_keys:
53 if k == 'status':
54 if k == 'status':
54 event.close()
55 event.close()
55 else:
56 else:
56 setattr(event, k, v)
57 setattr(event, k, v)
57 else:
58 else:
58 return HTTPBadRequest()
59 return HTTPBadRequest()
59 return event.get_dict()
60 return event.get_dict()
@@ -1,191 +1,192 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import logging
17 import logging
18
18
19 from pyramid.view import view_config
19 from pyramid.view import view_config
20 from pyramid.httpexceptions import HTTPUnprocessableEntity, HTTPNotFound
20 from pyramid.httpexceptions import HTTPUnprocessableEntity, HTTPNotFound
21
21
22 from ziggurat_foundations.models.services.user import UserService
22 from appenlight.lib.utils import permission_tuple_to_dict
23 from appenlight.lib.utils import permission_tuple_to_dict
23 from appenlight.models.services.config import ConfigService
24 from appenlight.models.services.config import ConfigService
24 from appenlight.models.group import Group
25 from appenlight.models.group import Group
25 from appenlight.models.services.group import GroupService
26 from appenlight.models.services.group import GroupService
26 from appenlight.models.user import User
27 from appenlight.models.user import User
27 from appenlight.models import DBSession
28 from appenlight.models import DBSession
28 from appenlight import forms
29 from appenlight import forms
29 from webob.multidict import MultiDict
30 from webob.multidict import MultiDict
30
31
31 log = logging.getLogger(__name__)
32 log = logging.getLogger(__name__)
32
33
33 _ = str
34 _ = str
34
35
35
36
36 @view_config(route_name='groups_no_id', renderer='json',
37 @view_config(route_name='groups_no_id', renderer='json',
37 request_method="GET", permission='authenticated')
38 request_method="GET", permission='authenticated')
38 def groups_list(request):
39 def groups_list(request):
39 """
40 """
40 Returns groups list
41 Returns groups list
41 """
42 """
42 groups = Group.all().order_by(Group.group_name)
43 groups = Group.all().order_by(Group.group_name)
43 list_groups = ConfigService.by_key_and_section(
44 list_groups = ConfigService.by_key_and_section(
44 'list_groups_to_non_admins', 'global')
45 'list_groups_to_non_admins', 'global')
45 if list_groups.value or request.has_permission('root_administration'):
46 if list_groups.value or request.has_permission('root_administration'):
46 return [g.get_dict() for g in groups]
47 return [g.get_dict() for g in groups]
47 else:
48 else:
48 return []
49 return []
49
50
50
51
51 @view_config(route_name='groups_no_id', renderer='json',
52 @view_config(route_name='groups_no_id', renderer='json',
52 request_method="POST", permission='root_administration')
53 request_method="POST", permission='root_administration')
53 def groups_create(request):
54 def groups_create(request):
54 """
55 """
55 Returns groups list
56 Returns groups list
56 """
57 """
57 form = forms.GroupCreateForm(
58 form = forms.GroupCreateForm(
58 MultiDict(request.safe_json_body or {}), csrf_context=request)
59 MultiDict(request.safe_json_body or {}), csrf_context=request)
59 if form.validate():
60 if form.validate():
60 log.info('registering group')
61 log.info('registering group')
61 group = Group()
62 group = Group()
62 # insert new group here
63 # insert new group here
63 DBSession.add(group)
64 DBSession.add(group)
64 form.populate_obj(group)
65 form.populate_obj(group)
65 request.session.flash(_('Group created'))
66 request.session.flash(_('Group created'))
66 DBSession.flush()
67 DBSession.flush()
67 return group.get_dict(include_perms=True)
68 return group.get_dict(include_perms=True)
68 else:
69 else:
69 return HTTPUnprocessableEntity(body=form.errors_json)
70 return HTTPUnprocessableEntity(body=form.errors_json)
70
71
71
72
72 @view_config(route_name='groups', renderer='json',
73 @view_config(route_name='groups', renderer='json',
73 request_method="DELETE", permission='root_administration')
74 request_method="DELETE", permission='root_administration')
74 def groups_DELETE(request):
75 def groups_DELETE(request):
75 """
76 """
76 Removes a groups permanently from db
77 Removes a groups permanently from db
77 """
78 """
78 msg = _('You cannot remove administrator group from the system')
79 msg = _('You cannot remove administrator group from the system')
79 group = GroupService.by_id(request.matchdict.get('group_id'))
80 group = GroupService.by_id(request.matchdict.get('group_id'))
80 if group:
81 if group:
81 if group.id == 1:
82 if group.id == 1:
82 request.session.flash(msg, 'warning')
83 request.session.flash(msg, 'warning')
83 else:
84 else:
84 DBSession.delete(group)
85 DBSession.delete(group)
85 request.session.flash(_('Group removed'))
86 request.session.flash(_('Group removed'))
86 return True
87 return True
87 request.response.status = 422
88 request.response.status = 422
88 return False
89 return False
89
90
90
91
91 @view_config(route_name='groups', renderer='json',
92 @view_config(route_name='groups', renderer='json',
92 request_method="GET", permission='root_administration')
93 request_method="GET", permission='root_administration')
93 @view_config(route_name='groups', renderer='json',
94 @view_config(route_name='groups', renderer='json',
94 request_method="PATCH", permission='root_administration')
95 request_method="PATCH", permission='root_administration')
95 def group_update(request):
96 def group_update(request):
96 """
97 """
97 Updates group object
98 Updates group object
98 """
99 """
99 group = GroupService.by_id(request.matchdict.get('group_id'))
100 group = GroupService.by_id(request.matchdict.get('group_id'))
100 if not group:
101 if not group:
101 return HTTPNotFound()
102 return HTTPNotFound()
102
103
103 if request.method == 'PATCH':
104 if request.method == 'PATCH':
104 form = forms.GroupCreateForm(
105 form = forms.GroupCreateForm(
105 MultiDict(request.unsafe_json_body), csrf_context=request)
106 MultiDict(request.unsafe_json_body), csrf_context=request)
106 form._modified_group = group
107 form._modified_group = group
107 if form.validate():
108 if form.validate():
108 form.populate_obj(group)
109 form.populate_obj(group)
109 else:
110 else:
110 return HTTPUnprocessableEntity(body=form.errors_json)
111 return HTTPUnprocessableEntity(body=form.errors_json)
111 return group.get_dict(include_perms=True)
112 return group.get_dict(include_perms=True)
112
113
113
114
114 @view_config(route_name='groups_property',
115 @view_config(route_name='groups_property',
115 match_param='key=resource_permissions',
116 match_param='key=resource_permissions',
116 renderer='json', permission='root_administration')
117 renderer='json', permission='root_administration')
117 def groups_resource_permissions_list(request):
118 def groups_resource_permissions_list(request):
118 """
119 """
119 Get list of permissions assigned to specific resources
120 Get list of permissions assigned to specific resources
120 """
121 """
121 group = GroupService.by_id(request.matchdict.get('group_id'))
122 group = GroupService.by_id(request.matchdict.get('group_id'))
122 if not group:
123 if not group:
123 return HTTPNotFound()
124 return HTTPNotFound()
124 return [permission_tuple_to_dict(perm) for perm in
125 return [permission_tuple_to_dict(perm) for perm in
125 group.resources_with_possible_perms()]
126 GroupService.resources_with_possible_perms(group)]
126
127
127
128
128 @view_config(route_name='groups_property',
129 @view_config(route_name='groups_property',
129 match_param='key=users', request_method="GET",
130 match_param='key=users', request_method="GET",
130 renderer='json', permission='root_administration')
131 renderer='json', permission='root_administration')
131 def groups_users_list(request):
132 def groups_users_list(request):
132 """
133 """
133 Get list of permissions assigned to specific resources
134 Get list of permissions assigned to specific resources
134 """
135 """
135 group = GroupService.by_id(request.matchdict.get('group_id'))
136 group = GroupService.by_id(request.matchdict.get('group_id'))
136 if not group:
137 if not group:
137 return HTTPNotFound()
138 return HTTPNotFound()
138 props = ['user_name', 'id', 'first_name', 'last_name', 'email',
139 props = ['user_name', 'id', 'first_name', 'last_name', 'email',
139 'last_login_date', 'status']
140 'last_login_date', 'status']
140 users_dicts = []
141 users_dicts = []
141 for user in group.users:
142 for user in group.users:
142 u_dict = user.get_dict(include_keys=props)
143 u_dict = user.get_dict(include_keys=props)
143 u_dict['gravatar_url'] = user.gravatar_url(s=20)
144 u_dict['gravatar_url'] = UserService.gravatar_url(user, s=20)
144 users_dicts.append(u_dict)
145 users_dicts.append(u_dict)
145 return users_dicts
146 return users_dicts
146
147
147
148
148 @view_config(route_name='groups_property',
149 @view_config(route_name='groups_property',
149 match_param='key=users', request_method="DELETE",
150 match_param='key=users', request_method="DELETE",
150 renderer='json', permission='root_administration')
151 renderer='json', permission='root_administration')
151 def groups_users_remove(request):
152 def groups_users_remove(request):
152 """
153 """
153 Get list of permissions assigned to specific resources
154 Get list of permissions assigned to specific resources
154 """
155 """
155 group = GroupService.by_id(request.matchdict.get('group_id'))
156 group = GroupService.by_id(request.matchdict.get('group_id'))
156 user = User.by_user_name(request.GET.get('user_name'))
157 user = UserService.by_user_name(request.GET.get('user_name'))
157 if not group or not user:
158 if not group or not user:
158 return HTTPNotFound()
159 return HTTPNotFound()
159 if len(group.users) > 1:
160 if len(group.users) > 1:
160 group.users.remove(user)
161 group.users.remove(user)
161 msg = "User removed from group"
162 msg = "User removed from group"
162 request.session.flash(msg)
163 request.session.flash(msg)
163 group.member_count = group.users_dynamic.count()
164 group.member_count = group.users_dynamic.count()
164 return True
165 return True
165 msg = "Administrator group needs to contain at least one user"
166 msg = "Administrator group needs to contain at least one user"
166 request.session.flash(msg, 'warning')
167 request.session.flash(msg, 'warning')
167 return False
168 return False
168
169
169
170
170 @view_config(route_name='groups_property',
171 @view_config(route_name='groups_property',
171 match_param='key=users', request_method="POST",
172 match_param='key=users', request_method="POST",
172 renderer='json', permission='root_administration')
173 renderer='json', permission='root_administration')
173 def groups_users_add(request):
174 def groups_users_add(request):
174 """
175 """
175 Get list of permissions assigned to specific resources
176 Get list of permissions assigned to specific resources
176 """
177 """
177 group = GroupService.by_id(request.matchdict.get('group_id'))
178 group = GroupService.by_id(request.matchdict.get('group_id'))
178 user = User.by_user_name(request.unsafe_json_body.get('user_name'))
179 user = UserService.by_user_name(request.unsafe_json_body.get('user_name'))
179 if not user:
180 if not user:
180 user = User.by_email(request.unsafe_json_body.get('user_name'))
181 user = UserService.by_email(request.unsafe_json_body.get('user_name'))
181
182
182 if not group or not user:
183 if not group or not user:
183 return HTTPNotFound()
184 return HTTPNotFound()
184 if user not in group.users:
185 if user not in group.users:
185 group.users.append(user)
186 group.users.append(user)
186 group.member_count = group.users_dynamic.count()
187 group.member_count = group.users_dynamic.count()
187 props = ['user_name', 'id', 'first_name', 'last_name', 'email',
188 props = ['user_name', 'id', 'first_name', 'last_name', 'email',
188 'last_login_date', 'status']
189 'last_login_date', 'status']
189 u_dict = user.get_dict(include_keys=props)
190 u_dict = user.get_dict(include_keys=props)
190 u_dict['gravatar_url'] = user.gravatar_url(s=20)
191 u_dict['gravatar_url'] = UserService.gravatar_url(user, s=20)
191 return u_dict
192 return u_dict
@@ -1,241 +1,242 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import datetime
17 import datetime
18 import logging
18 import logging
19 import uuid
19 import uuid
20
20
21 import pyramid.security as security
21 import pyramid.security as security
22
22
23 from pyramid.view import view_config
23 from pyramid.view import view_config
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.response import Response
25 from pyramid.response import Response
26 from pyramid.security import NO_PERMISSION_REQUIRED
26 from pyramid.security import NO_PERMISSION_REQUIRED
27 from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignInSuccess
27 from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignInSuccess
28 from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignInBadAuth
28 from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignInBadAuth
29 from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignOut
29 from ziggurat_foundations.ext.pyramid.sign_in import ZigguratSignOut
30 from ziggurat_foundations.models.services.user import UserService
30
31
31 from appenlight.lib.social import handle_social_data
32 from appenlight.lib.social import handle_social_data
32 from appenlight.models import DBSession
33 from appenlight.models import DBSession
33 from appenlight.models.user import User
34 from appenlight.models.user import User
34 from appenlight.models.services.user import UserService
35 from appenlight.models.services.user import UserService
35 from appenlight.subscribers import _
36 from appenlight.subscribers import _
36 from appenlight import forms
37 from appenlight import forms
37 from webob.multidict import MultiDict
38 from webob.multidict import MultiDict
38
39
39 log = logging.getLogger(__name__)
40 log = logging.getLogger(__name__)
40
41
41
42
42 @view_config(context=ZigguratSignInSuccess, permission=NO_PERMISSION_REQUIRED)
43 @view_config(context=ZigguratSignInSuccess, permission=NO_PERMISSION_REQUIRED)
43 def sign_in(request):
44 def sign_in(request):
44 """
45 """
45 Performs sign in by sending proper user identification headers
46 Performs sign in by sending proper user identification headers
46 Regenerates CSRF token
47 Regenerates CSRF token
47 """
48 """
48 user = request.context.user
49 user = request.context.user
49 if user.status == 1:
50 if user.status == 1:
50 request.session.new_csrf_token()
51 request.session.new_csrf_token()
51 user.last_login_date = datetime.datetime.utcnow()
52 user.last_login_date = datetime.datetime.utcnow()
52 social_data = request.session.get('zigg.social_auth')
53 social_data = request.session.get('zigg.social_auth')
53 if social_data:
54 if social_data:
54 handle_social_data(request, user, social_data)
55 handle_social_data(request, user, social_data)
55 else:
56 else:
56 request.session.flash(_('Account got disabled'))
57 request.session.flash(_('Account got disabled'))
57
58
58 if request.context.came_from != '/':
59 if request.context.came_from != '/':
59 return HTTPFound(location=request.context.came_from,
60 return HTTPFound(location=request.context.came_from,
60 headers=request.context.headers)
61 headers=request.context.headers)
61 else:
62 else:
62 return HTTPFound(location=request.route_url('/'),
63 return HTTPFound(location=request.route_url('/'),
63 headers=request.context.headers)
64 headers=request.context.headers)
64
65
65
66
66 @view_config(context=ZigguratSignInBadAuth, permission=NO_PERMISSION_REQUIRED)
67 @view_config(context=ZigguratSignInBadAuth, permission=NO_PERMISSION_REQUIRED)
67 def bad_auth(request):
68 def bad_auth(request):
68 """
69 """
69 Handles incorrect login flow
70 Handles incorrect login flow
70 """
71 """
71 request.session.flash(_('Incorrect username or password'), 'warning')
72 request.session.flash(_('Incorrect username or password'), 'warning')
72 return HTTPFound(location=request.route_url('register'),
73 return HTTPFound(location=request.route_url('register'),
73 headers=request.context.headers)
74 headers=request.context.headers)
74
75
75
76
76 @view_config(context=ZigguratSignOut, permission=NO_PERMISSION_REQUIRED)
77 @view_config(context=ZigguratSignOut, permission=NO_PERMISSION_REQUIRED)
77 def sign_out(request):
78 def sign_out(request):
78 """
79 """
79 Removes user identification cookie
80 Removes user identification cookie
80 """
81 """
81 return HTTPFound(location=request.route_url('register'),
82 return HTTPFound(location=request.route_url('register'),
82 headers=request.context.headers)
83 headers=request.context.headers)
83
84
84
85
85 @view_config(route_name='lost_password',
86 @view_config(route_name='lost_password',
86 renderer='appenlight:templates/user/lost_password.jinja2',
87 renderer='appenlight:templates/user/lost_password.jinja2',
87 permission=NO_PERMISSION_REQUIRED)
88 permission=NO_PERMISSION_REQUIRED)
88 def lost_password(request):
89 def lost_password(request):
89 """
90 """
90 Presents lost password page - sends password reset link to
91 Presents lost password page - sends password reset link to
91 specified email address.
92 specified email address.
92 This link is valid only for 10 minutes
93 This link is valid only for 10 minutes
93 """
94 """
94 form = forms.LostPasswordForm(request.POST, csrf_context=request)
95 form = forms.LostPasswordForm(request.POST, csrf_context=request)
95 if request.method == 'POST' and form.validate():
96 if request.method == 'POST' and form.validate():
96 user = User.by_email(form.email.data)
97 user = UserService.by_email(form.email.data)
97 if user:
98 if user:
98 user.regenerate_security_code()
99 UserService.regenerate_security_code(user)
99 user.security_code_date = datetime.datetime.utcnow()
100 user.security_code_date = datetime.datetime.utcnow()
100 email_vars = {
101 email_vars = {
101 'user': user,
102 'user': user,
102 'request': request,
103 'request': request,
103 'email_title': "AppEnlight :: New password request"
104 'email_title': "AppEnlight :: New password request"
104 }
105 }
105 UserService.send_email(
106 UserService.send_email(
106 request, recipients=[user.email],
107 request, recipients=[user.email],
107 variables=email_vars,
108 variables=email_vars,
108 template='/email_templates/lost_password.jinja2')
109 template='/email_templates/lost_password.jinja2')
109 msg = 'Password reset email had been sent. ' \
110 msg = 'Password reset email had been sent. ' \
110 'Please check your mailbox for further instructions.'
111 'Please check your mailbox for further instructions.'
111 request.session.flash(_(msg))
112 request.session.flash(_(msg))
112 return HTTPFound(location=request.route_url('lost_password'))
113 return HTTPFound(location=request.route_url('lost_password'))
113 return {"form": form}
114 return {"form": form}
114
115
115
116
116 @view_config(route_name='lost_password_generate',
117 @view_config(route_name='lost_password_generate',
117 permission=NO_PERMISSION_REQUIRED,
118 permission=NO_PERMISSION_REQUIRED,
118 renderer='appenlight:templates/user/lost_password_generate.jinja2')
119 renderer='appenlight:templates/user/lost_password_generate.jinja2')
119 def lost_password_generate(request):
120 def lost_password_generate(request):
120 """
121 """
121 Shows new password form - perform time check and set new password for user
122 Shows new password form - perform time check and set new password for user
122 """
123 """
123 user = User.by_user_name_and_security_code(
124 user = UserService.by_user_name_and_security_code(
124 request.GET.get('user_name'), request.GET.get('security_code'))
125 request.GET.get('user_name'), request.GET.get('security_code'))
125 if user:
126 if user:
126 delta = datetime.datetime.utcnow() - user.security_code_date
127 delta = datetime.datetime.utcnow() - user.security_code_date
127
128
128 if user and delta.total_seconds() < 600:
129 if user and delta.total_seconds() < 600:
129 form = forms.NewPasswordForm(request.POST, csrf_context=request)
130 form = forms.NewPasswordForm(request.POST, csrf_context=request)
130 if request.method == "POST" and form.validate():
131 if request.method == "POST" and form.validate():
131 user.set_password(form.new_password.data)
132 UserService.set_password(user, form.new_password.data)
132 request.session.flash(_('You can sign in with your new password.'))
133 request.session.flash(_('You can sign in with your new password.'))
133 return HTTPFound(location=request.route_url('register'))
134 return HTTPFound(location=request.route_url('register'))
134 else:
135 else:
135 return {"form": form}
136 return {"form": form}
136 else:
137 else:
137 return Response('Security code expired')
138 return Response('Security code expired')
138
139
139
140
140 @view_config(route_name='register',
141 @view_config(route_name='register',
141 renderer='appenlight:templates/user/register.jinja2',
142 renderer='appenlight:templates/user/register.jinja2',
142 permission=NO_PERMISSION_REQUIRED)
143 permission=NO_PERMISSION_REQUIRED)
143 def register(request):
144 def register(request):
144 """
145 """
145 Render register page with form
146 Render register page with form
146 Also handles oAuth flow for registration
147 Also handles oAuth flow for registration
147 """
148 """
148 login_url = request.route_url('ziggurat.routes.sign_in')
149 login_url = request.route_url('ziggurat.routes.sign_in')
149 if request.query_string:
150 if request.query_string:
150 query_string = '?%s' % request.query_string
151 query_string = '?%s' % request.query_string
151 else:
152 else:
152 query_string = ''
153 query_string = ''
153 referrer = '%s%s' % (request.path, query_string)
154 referrer = '%s%s' % (request.path, query_string)
154
155
155 if referrer in [login_url, '/register', '/register?sign_in=1']:
156 if referrer in [login_url, '/register', '/register?sign_in=1']:
156 referrer = '/' # never use the login form itself as came_from
157 referrer = '/' # never use the login form itself as came_from
157 sign_in_form = forms.SignInForm(
158 sign_in_form = forms.SignInForm(
158 came_from=request.params.get('came_from', referrer),
159 came_from=request.params.get('came_from', referrer),
159 csrf_context=request)
160 csrf_context=request)
160
161
161 # populate form from oAuth session data returned by authomatic
162 # populate form from oAuth session data returned by authomatic
162 social_data = request.session.get('zigg.social_auth')
163 social_data = request.session.get('zigg.social_auth')
163 if request.method != 'POST' and social_data:
164 if request.method != 'POST' and social_data:
164 log.debug(social_data)
165 log.debug(social_data)
165 user_name = social_data['user'].get('user_name', '').split('@')[0]
166 user_name = social_data['user'].get('user_name', '').split('@')[0]
166 form_data = {
167 form_data = {
167 'user_name': user_name,
168 'user_name': user_name,
168 'email': social_data['user'].get('email')
169 'email': social_data['user'].get('email')
169 }
170 }
170 form_data['user_password'] = str(uuid.uuid4())
171 form_data['user_password'] = str(uuid.uuid4())
171 form = forms.UserRegisterForm(MultiDict(form_data),
172 form = forms.UserRegisterForm(MultiDict(form_data),
172 csrf_context=request)
173 csrf_context=request)
173 form.user_password.widget.hide_value = False
174 form.user_password.widget.hide_value = False
174 else:
175 else:
175 form = forms.UserRegisterForm(request.POST, csrf_context=request)
176 form = forms.UserRegisterForm(request.POST, csrf_context=request)
176 if request.method == 'POST' and form.validate():
177 if request.method == 'POST' and form.validate():
177 log.info('registering user')
178 log.info('registering user')
178 # insert new user here
179 # insert new user here
179 if request.registry.settings['appenlight.disable_registration']:
180 if request.registry.settings['appenlight.disable_registration']:
180 request.session.flash(_('Registration is currently disabled.'))
181 request.session.flash(_('Registration is currently disabled.'))
181 return HTTPFound(location=request.route_url('/'))
182 return HTTPFound(location=request.route_url('/'))
182
183
183 new_user = User()
184 new_user = User()
184 DBSession.add(new_user)
185 DBSession.add(new_user)
185 form.populate_obj(new_user)
186 form.populate_obj(new_user)
186 new_user.regenerate_security_code()
187 UserService.regenerate_security_code(new_user)
187 new_user.status = 1
188 new_user.status = 1
188 new_user.set_password(new_user.user_password)
189 UserService.set_password(new_user, new_user.user_password)
189 new_user.registration_ip = request.environ.get('REMOTE_ADDR')
190 new_user.registration_ip = request.environ.get('REMOTE_ADDR')
190
191
191 if social_data:
192 if social_data:
192 handle_social_data(request, new_user, social_data)
193 handle_social_data(request, new_user, social_data)
193
194
194 email_vars = {'user': new_user,
195 email_vars = {'user': new_user,
195 'request': request,
196 'request': request,
196 'email_title': "AppEnlight :: Start information"}
197 'email_title': "AppEnlight :: Start information"}
197 UserService.send_email(
198 UserService.send_email(
198 request, recipients=[new_user.email], variables=email_vars,
199 request, recipients=[new_user.email], variables=email_vars,
199 template='/email_templates/registered.jinja2')
200 template='/email_templates/registered.jinja2')
200 request.session.flash(_('You have successfully registered.'))
201 request.session.flash(_('You have successfully registered.'))
201 DBSession.flush()
202 DBSession.flush()
202 headers = security.remember(request, new_user.id)
203 headers = security.remember(request, new_user.id)
203 return HTTPFound(location=request.route_url('/'),
204 return HTTPFound(location=request.route_url('/'),
204 headers=headers)
205 headers=headers)
205 settings = request.registry.settings
206 settings = request.registry.settings
206 social_plugins = {}
207 social_plugins = {}
207 if settings.get('authomatic.pr.twitter.key', ''):
208 if settings.get('authomatic.pr.twitter.key', ''):
208 social_plugins['twitter'] = True
209 social_plugins['twitter'] = True
209 if settings.get('authomatic.pr.google.key', ''):
210 if settings.get('authomatic.pr.google.key', ''):
210 social_plugins['google'] = True
211 social_plugins['google'] = True
211 if settings.get('authomatic.pr.github.key', ''):
212 if settings.get('authomatic.pr.github.key', ''):
212 social_plugins['github'] = True
213 social_plugins['github'] = True
213 if settings.get('authomatic.pr.bitbucket.key', ''):
214 if settings.get('authomatic.pr.bitbucket.key', ''):
214 social_plugins['bitbucket'] = True
215 social_plugins['bitbucket'] = True
215
216
216 return {
217 return {
217 "form": form,
218 "form": form,
218 "sign_in_form": sign_in_form,
219 "sign_in_form": sign_in_form,
219 "social_plugins": social_plugins
220 "social_plugins": social_plugins
220 }
221 }
221
222
222
223
223 @view_config(route_name='/',
224 @view_config(route_name='/',
224 renderer='appenlight:templates/app.jinja2',
225 renderer='appenlight:templates/app.jinja2',
225 permission=NO_PERMISSION_REQUIRED)
226 permission=NO_PERMISSION_REQUIRED)
226 @view_config(route_name='angular_app_ui',
227 @view_config(route_name='angular_app_ui',
227 renderer='appenlight:templates/app.jinja2',
228 renderer='appenlight:templates/app.jinja2',
228 permission=NO_PERMISSION_REQUIRED)
229 permission=NO_PERMISSION_REQUIRED)
229 @view_config(route_name='angular_app_ui_ix',
230 @view_config(route_name='angular_app_ui_ix',
230 renderer='appenlight:templates/app.jinja2',
231 renderer='appenlight:templates/app.jinja2',
231 permission=NO_PERMISSION_REQUIRED)
232 permission=NO_PERMISSION_REQUIRED)
232 def app_main_index(request):
233 def app_main_index(request):
233 """
234 """
234 Render dashoard/report browser page page along with:
235 Render dashoard/report browser page page along with:
235 - flash messages
236 - flash messages
236 - application list
237 - application list
237 - assigned reports
238 - assigned reports
238 - latest events
239 - latest events
239 (those last two come from subscribers.py that sets global renderer variables)
240 (those last two come from subscribers.py that sets global renderer variables)
240 """
241 """
241 return {}
242 return {}
@@ -1,294 +1,297 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import logging
17 import logging
18
18
19 from datetime import datetime, timedelta
19 from datetime import datetime, timedelta
20 from pyramid.view import view_config
20 from pyramid.view import view_config
21 from pyramid.httpexceptions import HTTPUnprocessableEntity
21 from pyramid.httpexceptions import HTTPUnprocessableEntity
22
22
23 from ziggurat_foundations.models.services.resource import ResourceService
24 from ziggurat_foundations.models.services.user import UserService
25
23 from appenlight.models import DBSession
26 from appenlight.models import DBSession
24 from appenlight.models.user import User
27 from appenlight.models.user import User
25 from appenlight.models.report_comment import ReportComment
28 from appenlight.models.report_comment import ReportComment
26 from appenlight.models.report_assignment import ReportAssignment
29 from appenlight.models.report_assignment import ReportAssignment
27 from appenlight.models.services.user import UserService
30 from appenlight.models.services.user import UserService
28 from appenlight.models.services.report_group import ReportGroupService
31 from appenlight.models.services.report_group import ReportGroupService
29 from appenlight import forms
32 from appenlight import forms
30 from appenlight.lib.enums import ReportType
33 from appenlight.lib.enums import ReportType
31 from appenlight.lib.helpers import gen_pagination_headers
34 from appenlight.lib.helpers import gen_pagination_headers
32 from appenlight.lib.utils import build_filter_settings_from_query_dict
35 from appenlight.lib.utils import build_filter_settings_from_query_dict
33 from appenlight.validators import ReportSearchSchema, TagListSchema, \
36 from appenlight.validators import ReportSearchSchema, TagListSchema, \
34 accepted_search_params
37 accepted_search_params
35 from webob.multidict import MultiDict
38 from webob.multidict import MultiDict
36
39
37 _ = str
40 _ = str
38
41
39 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
40
43
41 section_filters_key = 'appenlight:reports:filter:%s'
44 section_filters_key = 'appenlight:reports:filter:%s'
42
45
43
46
44 @view_config(route_name='reports', renderer='json', permission='authenticated')
47 @view_config(route_name='reports', renderer='json', permission='authenticated')
45 @view_config(route_name='slow_reports', renderer='json',
48 @view_config(route_name='slow_reports', renderer='json',
46 permission='authenticated')
49 permission='authenticated')
47 def index(request):
50 def index(request):
48 """
51 """
49 Returns list of report groups based on user search query
52 Returns list of report groups based on user search query
50 """
53 """
51 if request.user:
54 if request.user:
52 request.user.last_login_date = datetime.utcnow()
55 request.user.last_login_date = datetime.utcnow()
53
56
54 applications = request.user.resources_with_perms(
57 applications = UserService.resources_with_perms(
55 ['view'], resource_types=['application'])
58 request.user, ['view'], resource_types=['application'])
56
59
57 search_params = request.GET.mixed()
60 search_params = request.GET.mixed()
58
61
59 all_possible_app_ids = set([app.resource_id for app in applications])
62 all_possible_app_ids = set([app.resource_id for app in applications])
60 schema = ReportSearchSchema().bind(resources=all_possible_app_ids)
63 schema = ReportSearchSchema().bind(resources=all_possible_app_ids)
61 tag_schema = TagListSchema()
64 tag_schema = TagListSchema()
62 filter_settings = schema.deserialize(search_params)
65 filter_settings = schema.deserialize(search_params)
63 tag_list = [{"name": k, "value": v} for k, v in filter_settings.items()
66 tag_list = [{"name": k, "value": v} for k, v in filter_settings.items()
64 if k not in accepted_search_params]
67 if k not in accepted_search_params]
65 tags = tag_schema.deserialize(tag_list)
68 tags = tag_schema.deserialize(tag_list)
66 filter_settings['tags'] = tags
69 filter_settings['tags'] = tags
67 if request.matched_route.name == 'slow_reports':
70 if request.matched_route.name == 'slow_reports':
68 filter_settings['report_type'] = [ReportType.slow]
71 filter_settings['report_type'] = [ReportType.slow]
69 else:
72 else:
70 filter_settings['report_type'] = [ReportType.error]
73 filter_settings['report_type'] = [ReportType.error]
71
74
72 reports_paginator = ReportGroupService.get_paginator_by_app_ids(
75 reports_paginator = ReportGroupService.get_paginator_by_app_ids(
73 app_ids=filter_settings['resource'],
76 app_ids=filter_settings['resource'],
74 page=filter_settings['page'],
77 page=filter_settings['page'],
75 filter_settings=filter_settings
78 filter_settings=filter_settings
76 )
79 )
77 reports = []
80 reports = []
78 include_keys = ('id', 'http_status', 'report_type', 'resource_name',
81 include_keys = ('id', 'http_status', 'report_type', 'resource_name',
79 'front_url', 'resource_id', 'error', 'url_path', 'tags',
82 'front_url', 'resource_id', 'error', 'url_path', 'tags',
80 'duration')
83 'duration')
81 for report in reports_paginator.sa_items:
84 for report in reports_paginator.sa_items:
82 reports.append(report.get_dict(request, include_keys=include_keys))
85 reports.append(report.get_dict(request, include_keys=include_keys))
83 headers = gen_pagination_headers(request, reports_paginator)
86 headers = gen_pagination_headers(request, reports_paginator)
84 request.response.headers.update(headers)
87 request.response.headers.update(headers)
85 return reports
88 return reports
86
89
87
90
88 @view_config(route_name='report_groups', renderer='json', permission='view',
91 @view_config(route_name='report_groups', renderer='json', permission='view',
89 request_method="GET")
92 request_method="GET")
90 def view_report(request):
93 def view_report(request):
91 """
94 """
92 Show individual detailed report group along with latest report
95 Show individual detailed report group along with latest report
93 """
96 """
94 report_group = request.context.report_group
97 report_group = request.context.report_group
95 if not report_group.read:
98 if not report_group.read:
96 report_group.read = True
99 report_group.read = True
97
100
98 report_id = request.params.get('reportId', request.params.get('report_id'))
101 report_id = request.params.get('reportId', request.params.get('report_id'))
99 report_dict = report_group.get_report(report_id).get_dict(request,
102 report_dict = report_group.get_report(report_id).get_dict(request,
100 details=True)
103 details=True)
101 # disallow browsing other occurences by anonymous
104 # disallow browsing other occurences by anonymous
102 if not request.user:
105 if not request.user:
103 report_dict.pop('group_next_report', None)
106 report_dict.pop('group_next_report', None)
104 report_dict.pop('group_previous_report', None)
107 report_dict.pop('group_previous_report', None)
105 return report_dict
108 return report_dict
106
109
107
110
108 @view_config(route_name='report_groups', renderer='json',
111 @view_config(route_name='report_groups', renderer='json',
109 permission='update_reports', request_method='DELETE')
112 permission='update_reports', request_method='DELETE')
110 def remove(request):
113 def remove(request):
111 """
114 """
112 Used to remove reourt groups from database
115 Used to remove reourt groups from database
113 """
116 """
114 report = request.context.report_group
117 report = request.context.report_group
115 form = forms.ReactorForm(request.POST, csrf_context=request)
118 form = forms.ReactorForm(request.POST, csrf_context=request)
116 form.validate()
119 form.validate()
117 DBSession.delete(report)
120 DBSession.delete(report)
118 return True
121 return True
119
122
120
123
121 @view_config(route_name='report_groups_property', match_param='key=comments',
124 @view_config(route_name='report_groups_property', match_param='key=comments',
122 renderer='json', permission='view', request_method="POST")
125 renderer='json', permission='view', request_method="POST")
123 def comment_create(request):
126 def comment_create(request):
124 """
127 """
125 Creates user comments for report group, sends email notifications
128 Creates user comments for report group, sends email notifications
126 of said comments
129 of said comments
127 """
130 """
128 report_group = request.context.report_group
131 report_group = request.context.report_group
129 application = request.context.resource
132 application = request.context.resource
130 form = forms.CommentForm(MultiDict(request.unsafe_json_body),
133 form = forms.CommentForm(MultiDict(request.unsafe_json_body),
131 csrf_context=request)
134 csrf_context=request)
132 if request.method == 'POST' and form.validate():
135 if request.method == 'POST' and form.validate():
133 comment = ReportComment(owner_id=request.user.id,
136 comment = ReportComment(owner_id=request.user.id,
134 report_time=report_group.first_timestamp)
137 report_time=report_group.first_timestamp)
135 form.populate_obj(comment)
138 form.populate_obj(comment)
136 report_group.comments.append(comment)
139 report_group.comments.append(comment)
137 perm_list = application.users_for_perm('view')
140 perm_list = ResourceService.users_for_perm(application, 'view')
138 uids_to_notify = []
141 uids_to_notify = []
139 users_to_notify = []
142 users_to_notify = []
140 for perm in perm_list:
143 for perm in perm_list:
141 user = perm.user
144 user = perm.user
142 if ('@{}'.format(user.user_name) in comment.body and
145 if ('@{}'.format(user.user_name) in comment.body and
143 user.id not in uids_to_notify):
146 user.id not in uids_to_notify):
144 uids_to_notify.append(user.id)
147 uids_to_notify.append(user.id)
145 users_to_notify.append(user)
148 users_to_notify.append(user)
146
149
147 commenters = ReportGroupService.users_commenting(
150 commenters = ReportGroupService.users_commenting(
148 report_group, exclude_user_id=request.user.id)
151 report_group, exclude_user_id=request.user.id)
149 for user in commenters:
152 for user in commenters:
150 if user.id not in uids_to_notify:
153 if user.id not in uids_to_notify:
151 uids_to_notify.append(user.id)
154 uids_to_notify.append(user.id)
152 users_to_notify.append(user)
155 users_to_notify.append(user)
153
156
154 for user in users_to_notify:
157 for user in users_to_notify:
155 email_vars = {'user': user,
158 email_vars = {'user': user,
156 'commenting_user': request.user,
159 'commenting_user': request.user,
157 'request': request,
160 'request': request,
158 'application': application,
161 'application': application,
159 'report_group': report_group,
162 'report_group': report_group,
160 'comment': comment,
163 'comment': comment,
161 'email_title': "AppEnlight :: New comment"}
164 'email_title': "AppEnlight :: New comment"}
162 UserService.send_email(
165 UserService.send_email(
163 request,
166 request,
164 recipients=[user.email],
167 recipients=[user.email],
165 variables=email_vars,
168 variables=email_vars,
166 template='/email_templates/new_comment_report.jinja2')
169 template='/email_templates/new_comment_report.jinja2')
167 request.session.flash(_('Your comment was created'))
170 request.session.flash(_('Your comment was created'))
168 return comment.get_dict()
171 return comment.get_dict()
169 else:
172 else:
170 return form.errors
173 return form.errors
171
174
172
175
173 @view_config(route_name='report_groups_property',
176 @view_config(route_name='report_groups_property',
174 match_param='key=assigned_users', renderer='json',
177 match_param='key=assigned_users', renderer='json',
175 permission='update_reports', request_method="GET")
178 permission='update_reports', request_method="GET")
176 def assigned_users(request):
179 def assigned_users(request):
177 """
180 """
178 Returns list of users a specific report group is assigned for review
181 Returns list of users a specific report group is assigned for review
179 """
182 """
180 report_group = request.context.report_group
183 report_group = request.context.report_group
181 application = request.context.resource
184 application = request.context.resource
182 users = set([p.user for p in application.users_for_perm('view')])
185 users = set([p.user for p in ResourceService.users_for_perm(application, 'view')])
183 currently_assigned = [u.user_name for u in report_group.assigned_users]
186 currently_assigned = [u.user_name for u in report_group.assigned_users]
184 user_status = {'assigned': [], 'unassigned': []}
187 user_status = {'assigned': [], 'unassigned': []}
185 # handle users
188 # handle users
186 for user in users:
189 for user in users:
187 user_dict = {'user_name': user.user_name,
190 user_dict = {'user_name': user.user_name,
188 'gravatar_url': user.gravatar_url(),
191 'gravatar_url': UserService.gravatar_url(user),
189 'name': '%s %s' % (user.first_name, user.last_name,)}
192 'name': '%s %s' % (user.first_name, user.last_name,)}
190 if user.user_name in currently_assigned:
193 if user.user_name in currently_assigned:
191 user_status['assigned'].append(user_dict)
194 user_status['assigned'].append(user_dict)
192 elif user_dict not in user_status['unassigned']:
195 elif user_dict not in user_status['unassigned']:
193 user_status['unassigned'].append(user_dict)
196 user_status['unassigned'].append(user_dict)
194 return user_status
197 return user_status
195
198
196
199
197 @view_config(route_name='report_groups_property',
200 @view_config(route_name='report_groups_property',
198 match_param='key=assigned_users', renderer='json',
201 match_param='key=assigned_users', renderer='json',
199 permission='update_reports', request_method="PATCH")
202 permission='update_reports', request_method="PATCH")
200 def assign_users(request):
203 def assign_users(request):
201 """
204 """
202 Assigns specific report group to user for review - send email notification
205 Assigns specific report group to user for review - send email notification
203 """
206 """
204 report_group = request.context.report_group
207 report_group = request.context.report_group
205 application = request.context.resource
208 application = request.context.resource
206 currently_assigned = [u.user_name for u in report_group.assigned_users]
209 currently_assigned = [u.user_name for u in report_group.assigned_users]
207 new_assigns = request.unsafe_json_body
210 new_assigns = request.unsafe_json_body
208
211
209 # first unassign old users
212 # first unassign old users
210 for user_name in new_assigns['unassigned']:
213 for user_name in new_assigns['unassigned']:
211 if user_name in currently_assigned:
214 if user_name in currently_assigned:
212 user = User.by_user_name(user_name)
215 user = UserService.by_user_name(user_name)
213 report_group.assigned_users.remove(user)
216 report_group.assigned_users.remove(user)
214 comment = ReportComment(owner_id=request.user.id,
217 comment = ReportComment(owner_id=request.user.id,
215 report_time=report_group.first_timestamp)
218 report_time=report_group.first_timestamp)
216 comment.body = 'Unassigned group from @%s' % user_name
219 comment.body = 'Unassigned group from @%s' % user_name
217 report_group.comments.append(comment)
220 report_group.comments.append(comment)
218
221
219 # assign new users
222 # assign new users
220 for user_name in new_assigns['assigned']:
223 for user_name in new_assigns['assigned']:
221 if user_name not in currently_assigned:
224 if user_name not in currently_assigned:
222 user = User.by_user_name(user_name)
225 user = UserService.by_user_name(user_name)
223 if user in report_group.assigned_users:
226 if user in report_group.assigned_users:
224 report_group.assigned_users.remove(user)
227 report_group.assigned_users.remove(user)
225 DBSession.flush()
228 DBSession.flush()
226 assignment = ReportAssignment(
229 assignment = ReportAssignment(
227 owner_id=user.id,
230 owner_id=user.id,
228 report_time=report_group.first_timestamp,
231 report_time=report_group.first_timestamp,
229 group_id=report_group.id)
232 group_id=report_group.id)
230 DBSession.add(assignment)
233 DBSession.add(assignment)
231
234
232 comment = ReportComment(owner_id=request.user.id,
235 comment = ReportComment(owner_id=request.user.id,
233 report_time=report_group.first_timestamp)
236 report_time=report_group.first_timestamp)
234 comment.body = 'Assigned report_group to @%s' % user_name
237 comment.body = 'Assigned report_group to @%s' % user_name
235 report_group.comments.append(comment)
238 report_group.comments.append(comment)
236
239
237 email_vars = {'user': user,
240 email_vars = {'user': user,
238 'request': request,
241 'request': request,
239 'application': application,
242 'application': application,
240 'report_group': report_group,
243 'report_group': report_group,
241 'email_title': "AppEnlight :: Assigned Report"}
244 'email_title': "AppEnlight :: Assigned Report"}
242 UserService.send_email(request, recipients=[user.email],
245 UserService.send_email(request, recipients=[user.email],
243 variables=email_vars,
246 variables=email_vars,
244 template='/email_templates/assigned_report.jinja2')
247 template='/email_templates/assigned_report.jinja2')
245
248
246 return True
249 return True
247
250
248
251
249 @view_config(route_name='report_groups_property', match_param='key=history',
252 @view_config(route_name='report_groups_property', match_param='key=history',
250 renderer='json', permission='view')
253 renderer='json', permission='view')
251 def history(request):
254 def history(request):
252 """ Separate error graph or similar graph"""
255 """ Separate error graph or similar graph"""
253 report_group = request.context.report_group
256 report_group = request.context.report_group
254 query_params = request.GET.mixed()
257 query_params = request.GET.mixed()
255 query_params['resource'] = (report_group.resource_id,)
258 query_params['resource'] = (report_group.resource_id,)
256
259
257 filter_settings = build_filter_settings_from_query_dict(request,
260 filter_settings = build_filter_settings_from_query_dict(request,
258 query_params)
261 query_params)
259 if not filter_settings.get('end_date'):
262 if not filter_settings.get('end_date'):
260 end_date = datetime.utcnow().replace(microsecond=0, second=0)
263 end_date = datetime.utcnow().replace(microsecond=0, second=0)
261 filter_settings['end_date'] = end_date
264 filter_settings['end_date'] = end_date
262
265
263 if not filter_settings.get('start_date'):
266 if not filter_settings.get('start_date'):
264 delta = timedelta(days=30)
267 delta = timedelta(days=30)
265 filter_settings['start_date'] = filter_settings['end_date'] - delta
268 filter_settings['start_date'] = filter_settings['end_date'] - delta
266
269
267 filter_settings['group_id'] = report_group.id
270 filter_settings['group_id'] = report_group.id
268
271
269 result = ReportGroupService.get_report_stats(request, filter_settings)
272 result = ReportGroupService.get_report_stats(request, filter_settings)
270
273
271 plot_data = []
274 plot_data = []
272 for row in result:
275 for row in result:
273 point = {
276 point = {
274 'x': row['x'],
277 'x': row['x'],
275 'reports': row['report'] + row['slow_report'] + row['not_found']}
278 'reports': row['report'] + row['slow_report'] + row['not_found']}
276 plot_data.append(point)
279 plot_data.append(point)
277
280
278 return plot_data
281 return plot_data
279
282
280
283
281 @view_config(route_name='report_groups', renderer='json',
284 @view_config(route_name='report_groups', renderer='json',
282 permission='update_reports', request_method="PATCH")
285 permission='update_reports', request_method="PATCH")
283 def report_groups_PATCH(request):
286 def report_groups_PATCH(request):
284 """
287 """
285 Used to update the report group fixed status
288 Used to update the report group fixed status
286 """
289 """
287 report_group = request.context.report_group
290 report_group = request.context.report_group
288 allowed_keys = ['public', 'fixed']
291 allowed_keys = ['public', 'fixed']
289 for k, v in request.unsafe_json_body.items():
292 for k, v in request.unsafe_json_body.items():
290 if k in allowed_keys:
293 if k in allowed_keys:
291 setattr(report_group, k, v)
294 setattr(report_group, k, v)
292 else:
295 else:
293 return HTTPUnprocessableEntity()
296 return HTTPUnprocessableEntity()
294 return report_group.get_dict(request)
297 return report_group.get_dict(request)
@@ -1,444 +1,446 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import copy
17 import copy
18 import logging
18 import logging
19 import datetime
19 import datetime
20 import time
20 import time
21 import random
21 import random
22 import redis
22 import redis
23 import six
23 import six
24 import pyramid.renderers
24 import pyramid.renderers
25 import requests
25 import requests
26
27 from ziggurat_foundations.models.services.user import UserService
28
26 import appenlight.celery.tasks
29 import appenlight.celery.tasks
27 from pyramid.view import view_config
30 from pyramid.view import view_config
28 from pyramid_mailer.message import Message
31 from pyramid_mailer.message import Message
29 from appenlight_client.timing import time_trace
32 from appenlight_client.timing import time_trace
30 from appenlight.models import DBSession, Datastores
33 from appenlight.models import DBSession, Datastores
31 from appenlight.models.user import User
34 from appenlight.models.user import User
32 from appenlight.models.report_group import ReportGroup
35 from appenlight.models.report_group import ReportGroup
33 from appenlight.models.event import Event
36 from appenlight.models.event import Event
34 from appenlight.models.services.report_group import ReportGroupService
37 from appenlight.models.services.report_group import ReportGroupService
35 from appenlight.models.services.event import EventService
38 from appenlight.models.services.event import EventService
36 from appenlight.lib.enums import ReportType
39 from appenlight.lib.enums import ReportType
37
40
38 log = logging.getLogger(__name__)
41 log = logging.getLogger(__name__)
39
42
40 GLOBAL_REQ = None
43 GLOBAL_REQ = None
41
44
42
45
43 @view_config(route_name='test', match_param='action=mail',
46 @view_config(route_name='test', match_param='action=mail',
44 renderer='string', permission='root_administration')
47 renderer='string', permission='root_administration')
45 def mail(request):
48 def mail(request):
46 """
49 """
47 Test email communication
50 Test email communication
48 """
51 """
49 request.environ['HTTP_HOST'] = 'appenlight.com'
52 request.environ['HTTP_HOST'] = 'appenlight.com'
50 request.environ['wsgi.url_scheme'] = 'https'
53 request.environ['wsgi.url_scheme'] = 'https'
51 renderer_vars = {"title": "You have just registered on AppEnlight",
54 renderer_vars = {"title": "You have just registered on AppEnlight",
52 "username": "test",
55 "username": "test",
53 "email": "grzegżółka",
56 "email": "grzegżółka",
54 'firstname': 'dupa'}
57 'firstname': 'dupa'}
55 # return vars
58 # return vars
56 html = pyramid.renderers.render('/email_templates/registered.jinja2',
59 html = pyramid.renderers.render('/email_templates/registered.jinja2',
57 renderer_vars,
60 renderer_vars,
58 request=request)
61 request=request)
59 message = Message(subject="hello world %s" % random.randint(1, 9999),
62 message = Message(subject="hello world %s" % random.randint(1, 9999),
60 sender="info@appenlight.com",
63 sender="info@appenlight.com",
61 recipients=["ergo14@gmail.com"],
64 recipients=["ergo14@gmail.com"],
62 html=html)
65 html=html)
63 request.registry.mailer.send(message)
66 request.registry.mailer.send(message)
64 return html
67 return html
65 return vars
68 return vars
66
69
67
70
68 @view_config(route_name='test', match_param='action=alerting',
71 @view_config(route_name='test', match_param='action=alerting',
69 renderer='appenlight:templates/tests/alerting.jinja2',
72 renderer='appenlight:templates/tests/alerting.jinja2',
70 permission='root_administration')
73 permission='root_administration')
71 def alerting_test(request):
74 def alerting_test(request):
72 """
75 """
73 Allows to test send data on various registered alerting channels
76 Allows to test send data on various registered alerting channels
74 """
77 """
75 applications = request.user.resources_with_perms(
78 applications = UserService.resources_with_perms(request.user, ['view'], resource_types=['application'])
76 ['view'], resource_types=['application'])
77 # what we can select in total
79 # what we can select in total
78 all_possible_app_ids = [app.resource_id for app in applications]
80 all_possible_app_ids = [app.resource_id for app in applications]
79 resource = applications[0]
81 resource = applications[0]
80
82
81 alert_channels = []
83 alert_channels = []
82 for channel in request.user.alert_channels:
84 for channel in request.user.alert_channels:
83 alert_channels.append(channel.get_dict())
85 alert_channels.append(channel.get_dict())
84
86
85 cname = request.params.get('channel_name')
87 cname = request.params.get('channel_name')
86 cvalue = request.params.get('channel_value')
88 cvalue = request.params.get('channel_value')
87 event_name = request.params.get('event_name')
89 event_name = request.params.get('event_name')
88 if cname and cvalue:
90 if cname and cvalue:
89 for channel in request.user.alert_channels:
91 for channel in request.user.alert_channels:
90 if (channel.channel_value == cvalue and
92 if (channel.channel_value == cvalue and
91 channel.channel_name == cname):
93 channel.channel_name == cname):
92 break
94 break
93 if event_name in ['error_report_alert', 'slow_report_alert']:
95 if event_name in ['error_report_alert', 'slow_report_alert']:
94 # opened
96 # opened
95 new_event = Event(resource_id=resource.resource_id,
97 new_event = Event(resource_id=resource.resource_id,
96 event_type=Event.types[event_name],
98 event_type=Event.types[event_name],
97 start_date=datetime.datetime.utcnow(),
99 start_date=datetime.datetime.utcnow(),
98 status=Event.statuses['active'],
100 status=Event.statuses['active'],
99 values={'reports': 5,
101 values={'reports': 5,
100 'threshold': 10}
102 'threshold': 10}
101 )
103 )
102 channel.notify_alert(resource=resource,
104 channel.notify_alert(resource=resource,
103 event=new_event,
105 event=new_event,
104 user=request.user,
106 user=request.user,
105 request=request)
107 request=request)
106
108
107 # closed
109 # closed
108 ev_type = Event.types[event_name.replace('open', 'close')]
110 ev_type = Event.types[event_name.replace('open', 'close')]
109 new_event = Event(resource_id=resource.resource_id,
111 new_event = Event(resource_id=resource.resource_id,
110 event_type=ev_type,
112 event_type=ev_type,
111 start_date=datetime.datetime.utcnow(),
113 start_date=datetime.datetime.utcnow(),
112 status=Event.statuses['closed'],
114 status=Event.statuses['closed'],
113 values={'reports': 5,
115 values={'reports': 5,
114 'threshold': 10})
116 'threshold': 10})
115 channel.notify_alert(resource=resource,
117 channel.notify_alert(resource=resource,
116 event=new_event,
118 event=new_event,
117 user=request.user,
119 user=request.user,
118 request=request)
120 request=request)
119 elif event_name == 'notify_reports':
121 elif event_name == 'notify_reports':
120 report = ReportGroupService.by_app_ids(all_possible_app_ids) \
122 report = ReportGroupService.by_app_ids(all_possible_app_ids) \
121 .filter(ReportGroup.report_type == ReportType.error).first()
123 .filter(ReportGroup.report_type == ReportType.error).first()
122 confirmed_reports = [(5, report), (1, report)]
124 confirmed_reports = [(5, report), (1, report)]
123 channel.notify_reports(resource=resource,
125 channel.notify_reports(resource=resource,
124 user=request.user,
126 user=request.user,
125 request=request,
127 request=request,
126 since_when=datetime.datetime.utcnow(),
128 since_when=datetime.datetime.utcnow(),
127 reports=confirmed_reports)
129 reports=confirmed_reports)
128 confirmed_reports = [(5, report)]
130 confirmed_reports = [(5, report)]
129 channel.notify_reports(resource=resource,
131 channel.notify_reports(resource=resource,
130 user=request.user,
132 user=request.user,
131 request=request,
133 request=request,
132 since_when=datetime.datetime.utcnow(),
134 since_when=datetime.datetime.utcnow(),
133 reports=confirmed_reports)
135 reports=confirmed_reports)
134 elif event_name == 'notify_uptime':
136 elif event_name == 'notify_uptime':
135 new_event = Event(resource_id=resource.resource_id,
137 new_event = Event(resource_id=resource.resource_id,
136 event_type=Event.types['uptime_alert'],
138 event_type=Event.types['uptime_alert'],
137 start_date=datetime.datetime.utcnow(),
139 start_date=datetime.datetime.utcnow(),
138 status=Event.statuses['active'],
140 status=Event.statuses['active'],
139 values={"status_code": 500,
141 values={"status_code": 500,
140 "tries": 2,
142 "tries": 2,
141 "response_time": 0})
143 "response_time": 0})
142 channel.notify_uptime_alert(resource=resource,
144 channel.notify_uptime_alert(resource=resource,
143 event=new_event,
145 event=new_event,
144 user=request.user,
146 user=request.user,
145 request=request)
147 request=request)
146 elif event_name == 'chart_alert':
148 elif event_name == 'chart_alert':
147 event = EventService.by_type_and_status(
149 event = EventService.by_type_and_status(
148 event_types=(Event.types['chart_alert'],),
150 event_types=(Event.types['chart_alert'],),
149 status_types=(Event.statuses['active'],)).first()
151 status_types=(Event.statuses['active'],)).first()
150 channel.notify_chart_alert(resource=event.resource,
152 channel.notify_chart_alert(resource=event.resource,
151 event=event,
153 event=event,
152 user=request.user,
154 user=request.user,
153 request=request)
155 request=request)
154 elif event_name == 'daily_digest':
156 elif event_name == 'daily_digest':
155 since_when = datetime.datetime.utcnow() - datetime.timedelta(
157 since_when = datetime.datetime.utcnow() - datetime.timedelta(
156 hours=8)
158 hours=8)
157 filter_settings = {'resource': [resource.resource_id],
159 filter_settings = {'resource': [resource.resource_id],
158 'tags': [{'name': 'type',
160 'tags': [{'name': 'type',
159 'value': ['error'], 'op': None}],
161 'value': ['error'], 'op': None}],
160 'type': 'error', 'start_date': since_when}
162 'type': 'error', 'start_date': since_when}
161
163
162 reports = ReportGroupService.get_trending(
164 reports = ReportGroupService.get_trending(
163 request, filter_settings=filter_settings, limit=50)
165 request, filter_settings=filter_settings, limit=50)
164 channel.send_digest(resource=resource,
166 channel.send_digest(resource=resource,
165 user=request.user,
167 user=request.user,
166 request=request,
168 request=request,
167 since_when=datetime.datetime.utcnow(),
169 since_when=datetime.datetime.utcnow(),
168 reports=reports)
170 reports=reports)
169
171
170 return {'alert_channels': alert_channels,
172 return {'alert_channels': alert_channels,
171 'applications': dict([(app.resource_id, app.resource_name)
173 'applications': dict([(app.resource_id, app.resource_name)
172 for app in applications.all()])}
174 for app in applications.all()])}
173
175
174
176
175 @view_config(route_name='test', match_param='action=error',
177 @view_config(route_name='test', match_param='action=error',
176 renderer='string', permission='root_administration')
178 renderer='string', permission='root_administration')
177 def error(request):
179 def error(request):
178 """
180 """
179 Raises an internal error with some test data for testing purposes
181 Raises an internal error with some test data for testing purposes
180 """
182 """
181 request.environ['appenlight.message'] = 'test message'
183 request.environ['appenlight.message'] = 'test message'
182 request.environ['appenlight.extra']['dupa'] = 'dupa'
184 request.environ['appenlight.extra']['dupa'] = 'dupa'
183 request.environ['appenlight.extra']['message'] = 'message'
185 request.environ['appenlight.extra']['message'] = 'message'
184 request.environ['appenlight.tags']['action'] = 'test_error'
186 request.environ['appenlight.tags']['action'] = 'test_error'
185 request.environ['appenlight.tags']['count'] = 5
187 request.environ['appenlight.tags']['count'] = 5
186 log.debug(chr(960))
188 log.debug(chr(960))
187 log.debug('debug')
189 log.debug('debug')
188 log.info(chr(960))
190 log.info(chr(960))
189 log.info('INFO')
191 log.info('INFO')
190 log.warning('warning')
192 log.warning('warning')
191
193
192 @time_trace(name='error.foobar', min_duration=0.1)
194 @time_trace(name='error.foobar', min_duration=0.1)
193 def fooobar():
195 def fooobar():
194 time.sleep(0.12)
196 time.sleep(0.12)
195 return 1
197 return 1
196
198
197 fooobar()
199 fooobar()
198
200
199 def foobar(somearg):
201 def foobar(somearg):
200 raise Exception('test')
202 raise Exception('test')
201
203
202 client = redis.StrictRedis()
204 client = redis.StrictRedis()
203 client.setex('testval', 10, 'foo')
205 client.setex('testval', 10, 'foo')
204 request.environ['appenlight.force_send'] = 1
206 request.environ['appenlight.force_send'] = 1
205
207
206 # stats, result = get_local_storage(local_timing).get_thread_stats()
208 # stats, result = get_local_storage(local_timing).get_thread_stats()
207 # import pprint
209 # import pprint
208 # pprint.pprint(stats)
210 # pprint.pprint(stats)
209 # pprint.pprint(result)
211 # pprint.pprint(result)
210 # print 'entries', len(result)
212 # print 'entries', len(result)
211 request.environ['appenlight.username'] = 'ErgO'
213 request.environ['appenlight.username'] = 'ErgO'
212 raise Exception(chr(960) + '%s' % random.randint(1, 5))
214 raise Exception(chr(960) + '%s' % random.randint(1, 5))
213 return {}
215 return {}
214
216
215
217
216 @view_config(route_name='test', match_param='action=task',
218 @view_config(route_name='test', match_param='action=task',
217 renderer='string', permission='root_administration')
219 renderer='string', permission='root_administration')
218 def test_task(request):
220 def test_task(request):
219 """
221 """
220 Test erroneous celery task
222 Test erroneous celery task
221 """
223 """
222 import appenlight.celery.tasks
224 import appenlight.celery.tasks
223
225
224 appenlight.celery.tasks.test_exception_task.delay()
226 appenlight.celery.tasks.test_exception_task.delay()
225 return 'task sent'
227 return 'task sent'
226
228
227
229
228 @view_config(route_name='test', match_param='action=task_retry',
230 @view_config(route_name='test', match_param='action=task_retry',
229 renderer='string', permission='root_administration')
231 renderer='string', permission='root_administration')
230 def test_task_retry(request):
232 def test_task_retry(request):
231 """
233 """
232 Test erroneous celery task
234 Test erroneous celery task
233 """
235 """
234 import appenlight.celery.tasks
236 import appenlight.celery.tasks
235
237
236 appenlight.celery.tasks.test_retry_exception_task.delay()
238 appenlight.celery.tasks.test_retry_exception_task.delay()
237 return 'task sent'
239 return 'task sent'
238
240
239
241
240 @view_config(route_name='test', match_param='action=celery_emails',
242 @view_config(route_name='test', match_param='action=celery_emails',
241 renderer='string', permission='root_administration')
243 renderer='string', permission='root_administration')
242 def test_celery_emails(request):
244 def test_celery_emails(request):
243 import appenlight.celery.tasks
245 import appenlight.celery.tasks
244 appenlight.celery.tasks.alerting.delay()
246 appenlight.celery.tasks.alerting.delay()
245 return 'task sent'
247 return 'task sent'
246
248
247
249
248 @view_config(route_name='test', match_param='action=daily_digest',
250 @view_config(route_name='test', match_param='action=daily_digest',
249 renderer='string', permission='root_administration')
251 renderer='string', permission='root_administration')
250 def test_celery_daily_digest(request):
252 def test_celery_daily_digest(request):
251 import appenlight.celery.tasks
253 import appenlight.celery.tasks
252 appenlight.celery.tasks.daily_digest.delay()
254 appenlight.celery.tasks.daily_digest.delay()
253 return 'task sent'
255 return 'task sent'
254
256
255
257
256 @view_config(route_name='test', match_param='action=celery_alerting',
258 @view_config(route_name='test', match_param='action=celery_alerting',
257 renderer='string', permission='root_administration')
259 renderer='string', permission='root_administration')
258 def test_celery_alerting(request):
260 def test_celery_alerting(request):
259 import appenlight.celery.tasks
261 import appenlight.celery.tasks
260 appenlight.celery.tasks.alerting()
262 appenlight.celery.tasks.alerting()
261 return 'task sent'
263 return 'task sent'
262
264
263
265
264 @view_config(route_name='test', match_param='action=logging',
266 @view_config(route_name='test', match_param='action=logging',
265 renderer='string', permission='root_administration')
267 renderer='string', permission='root_administration')
266 def logs(request):
268 def logs(request):
267 """
269 """
268 Test some in-app logging
270 Test some in-app logging
269 """
271 """
270 log.debug(chr(960))
272 log.debug(chr(960))
271 log.debug('debug')
273 log.debug('debug')
272 log.info(chr(960))
274 log.info(chr(960))
273 log.info('INFO')
275 log.info('INFO')
274 log.warning('Matched GET /\xc4\x85\xc5\xbc\xc4\x87'
276 log.warning('Matched GET /\xc4\x85\xc5\xbc\xc4\x87'
275 '\xc4\x99\xc4\x99\xc4\x85/summary')
277 '\xc4\x99\xc4\x99\xc4\x85/summary')
276 log.warning('XXXXMatched GET /\xc4\x85\xc5\xbc\xc4'
278 log.warning('XXXXMatched GET /\xc4\x85\xc5\xbc\xc4'
277 '\x87\xc4\x99\xc4\x99\xc4\x85/summary')
279 '\x87\xc4\x99\xc4\x99\xc4\x85/summary')
278 log.warning('DUPA /ążćęęą')
280 log.warning('DUPA /ążćęęą')
279 log.warning("g\u017ceg\u017c\u00f3\u0142ka")
281 log.warning("g\u017ceg\u017c\u00f3\u0142ka")
280 log.error('TEST Lorem ipsum2',
282 log.error('TEST Lorem ipsum2',
281 extra={'user': 'ergo', 'commit': 'sog8ds0g7sdih12hh1j512h5k'})
283 extra={'user': 'ergo', 'commit': 'sog8ds0g7sdih12hh1j512h5k'})
282 log.fatal('TEST Lorem ipsum3')
284 log.fatal('TEST Lorem ipsum3')
283 log.warning('TEST Lorem ipsum',
285 log.warning('TEST Lorem ipsum',
284 extra={"action": 'purchase',
286 extra={"action": 'purchase',
285 "price": random.random() * 100,
287 "price": random.random() * 100,
286 "quantity": random.randint(1, 99)})
288 "quantity": random.randint(1, 99)})
287 log.warning('test_pkey',
289 log.warning('test_pkey',
288 extra={"action": 'test_pkey', "price": random.random() * 100,
290 extra={"action": 'test_pkey', "price": random.random() * 100,
289 'ae_primary_key': 1,
291 'ae_primary_key': 1,
290 "quantity": random.randint(1, 99)})
292 "quantity": random.randint(1, 99)})
291 log.warning('test_pkey2',
293 log.warning('test_pkey2',
292 extra={"action": 'test_pkey', "price": random.random() * 100,
294 extra={"action": 'test_pkey', "price": random.random() * 100,
293 'ae_primary_key': 'b',
295 'ae_primary_key': 'b',
294 'ae_permanent': 't',
296 'ae_permanent': 't',
295 "quantity": random.randint(1, 99)})
297 "quantity": random.randint(1, 99)})
296 log.warning('test_pkey3',
298 log.warning('test_pkey3',
297 extra={"action": 'test_pkey', "price": random.random() * 100,
299 extra={"action": 'test_pkey', "price": random.random() * 100,
298 'ae_primary_key': 1,
300 'ae_primary_key': 1,
299 "quantity": random.randint(1, 99)})
301 "quantity": random.randint(1, 99)})
300 log.warning('test_pkey4',
302 log.warning('test_pkey4',
301 extra={"action": 'test_pkey', "price": random.random() * 100,
303 extra={"action": 'test_pkey', "price": random.random() * 100,
302 'ae_primary_key': 'b',
304 'ae_primary_key': 'b',
303 'ae_permanent': True,
305 'ae_permanent': True,
304 "quantity": random.randint(1, 99)})
306 "quantity": random.randint(1, 99)})
305 request.environ['appenlight.force_send'] = 1
307 request.environ['appenlight.force_send'] = 1
306 return {}
308 return {}
307
309
308
310
309 @view_config(route_name='test', match_param='action=transaction',
311 @view_config(route_name='test', match_param='action=transaction',
310 renderer='string', permission='root_administration')
312 renderer='string', permission='root_administration')
311 def transaction_test(request):
313 def transaction_test(request):
312 """
314 """
313 Test transactions
315 Test transactions
314 """
316 """
315 try:
317 try:
316 result = DBSession.execute("SELECT 1/0")
318 result = DBSession.execute("SELECT 1/0")
317 except:
319 except:
318 request.tm.abort()
320 request.tm.abort()
319 result = DBSession.execute("SELECT 1")
321 result = DBSession.execute("SELECT 1")
320 return 'OK'
322 return 'OK'
321
323
322
324
323 @view_config(route_name='test', match_param='action=slow_request',
325 @view_config(route_name='test', match_param='action=slow_request',
324 renderer='string', permission='root_administration')
326 renderer='string', permission='root_administration')
325 def slow_request(request):
327 def slow_request(request):
326 """
328 """
327 Test a request that has some slow entries - including nested calls
329 Test a request that has some slow entries - including nested calls
328 """
330 """
329 users = DBSession.query(User).all()
331 users = DBSession.query(User).all()
330 import random
332 import random
331 some_val = random.random()
333 some_val = random.random()
332 import threading
334 import threading
333 t_id = id(threading.currentThread())
335 t_id = id(threading.currentThread())
334 log.warning('slow_log %s %s ' % (some_val, t_id))
336 log.warning('slow_log %s %s ' % (some_val, t_id))
335 log.critical('tid %s' % t_id)
337 log.critical('tid %s' % t_id)
336
338
337 @time_trace(name='baz_func %s' % some_val, min_duration=0.1)
339 @time_trace(name='baz_func %s' % some_val, min_duration=0.1)
338 def baz(arg):
340 def baz(arg):
339 time.sleep(0.32)
341 time.sleep(0.32)
340 return arg
342 return arg
341
343
342 requests.get('http://ubuntu.com')
344 requests.get('http://ubuntu.com')
343
345
344 @time_trace(name='foo_func %s %s' % (some_val, t_id), min_duration=0.1)
346 @time_trace(name='foo_func %s %s' % (some_val, t_id), min_duration=0.1)
345 def foo(arg):
347 def foo(arg):
346 time.sleep(0.52)
348 time.sleep(0.52)
347 log.warning('foo_func %s %s' % (some_val, t_id))
349 log.warning('foo_func %s %s' % (some_val, t_id))
348 requests.get('http://ubuntu.com?test=%s' % some_val)
350 requests.get('http://ubuntu.com?test=%s' % some_val)
349 return bar(arg)
351 return bar(arg)
350
352
351 @time_trace(name='bar_func %s %s' % (some_val, t_id), min_duration=0.1)
353 @time_trace(name='bar_func %s %s' % (some_val, t_id), min_duration=0.1)
352 def bar(arg):
354 def bar(arg):
353 log.warning('bar_func %s %s' % (some_val, t_id))
355 log.warning('bar_func %s %s' % (some_val, t_id))
354 time.sleep(1.52)
356 time.sleep(1.52)
355 baz(arg)
357 baz(arg)
356 baz(arg)
358 baz(arg)
357 return baz(arg)
359 return baz(arg)
358
360
359 foo('a')
361 foo('a')
360 return {}
362 return {}
361
363
362
364
363 @view_config(route_name='test', match_param='action=styling',
365 @view_config(route_name='test', match_param='action=styling',
364 renderer='appenlight:templates/tests/styling.jinja2',
366 renderer='appenlight:templates/tests/styling.jinja2',
365 permission='__no_permission_required__')
367 permission='__no_permission_required__')
366 def styling(request):
368 def styling(request):
367 """
369 """
368 Some styling test page
370 Some styling test page
369 """
371 """
370 _ = str
372 _ = str
371 request.session.flash(_(
373 request.session.flash(_(
372 'Your password got updated. '
374 'Your password got updated. '
373 'Next time log in with your new credentials.'))
375 'Next time log in with your new credentials.'))
374 request.session.flash(_(
376 request.session.flash(_(
375 'Something went wrong when we '
377 'Something went wrong when we '
376 'tried to authorize you via external provider'),
378 'tried to authorize you via external provider'),
377 'warning')
379 'warning')
378 request.session.flash(_(
380 request.session.flash(_(
379 'Unfortunately there was a problem '
381 'Unfortunately there was a problem '
380 'processing your payment, please try again later.'),
382 'processing your payment, please try again later.'),
381 'error')
383 'error')
382 return {}
384 return {}
383
385
384
386
385 @view_config(route_name='test', match_param='action=js_error',
387 @view_config(route_name='test', match_param='action=js_error',
386 renderer='appenlight:templates/tests/js_error.jinja2',
388 renderer='appenlight:templates/tests/js_error.jinja2',
387 permission='__no_permission_required__')
389 permission='__no_permission_required__')
388 def js(request):
390 def js(request):
389 """
391 """
390 Used for testing javasctipt client for error catching
392 Used for testing javasctipt client for error catching
391 """
393 """
392 return {}
394 return {}
393
395
394
396
395 @view_config(route_name='test', match_param='action=js_log',
397 @view_config(route_name='test', match_param='action=js_log',
396 renderer='appenlight:templates/tests/js_log.jinja2',
398 renderer='appenlight:templates/tests/js_log.jinja2',
397 permission='__no_permission_required__')
399 permission='__no_permission_required__')
398 def js_log(request):
400 def js_log(request):
399 """
401 """
400 Used for testing javasctipt client for logging
402 Used for testing javasctipt client for logging
401 """
403 """
402 return {}
404 return {}
403
405
404
406
405 @view_config(route_name='test', match_param='action=log_requests',
407 @view_config(route_name='test', match_param='action=log_requests',
406 renderer='string',
408 renderer='string',
407 permission='__no_permission_required__')
409 permission='__no_permission_required__')
408 def log_requests(request):
410 def log_requests(request):
409 """
411 """
410 Util view for printing json requests
412 Util view for printing json requests
411 """
413 """
412 return {}
414 return {}
413
415
414
416
415 @view_config(route_name='test', match_param='action=url', renderer='string',
417 @view_config(route_name='test', match_param='action=url', renderer='string',
416 permission='__no_permission_required__')
418 permission='__no_permission_required__')
417 def log_requests(request):
419 def log_requests(request):
418 """
420 """
419 I have no fucking clue why I needed that ;-)
421 I have no fucking clue why I needed that ;-)
420 """
422 """
421 return request.route_url('reports', _app_url='https://appenlight.com')
423 return request.route_url('reports', _app_url='https://appenlight.com')
422
424
423
425
424 class TestClass(object):
426 class TestClass(object):
425 """
427 """
426 Used to test if class-based view name resolution works correctly
428 Used to test if class-based view name resolution works correctly
427 """
429 """
428
430
429 def __init__(self, request):
431 def __init__(self, request):
430 self.request = request
432 self.request = request
431
433
432 @view_config(route_name='test', match_param='action=test_a',
434 @view_config(route_name='test', match_param='action=test_a',
433 renderer='string', permission='root_administration')
435 renderer='string', permission='root_administration')
434 @view_config(route_name='test', match_param='action=test_c',
436 @view_config(route_name='test', match_param='action=test_c',
435 renderer='string', permission='root_administration')
437 renderer='string', permission='root_administration')
436 @view_config(route_name='test', match_param='action=test_d',
438 @view_config(route_name='test', match_param='action=test_d',
437 renderer='string', permission='root_administration')
439 renderer='string', permission='root_administration')
438 def test_a(self):
440 def test_a(self):
439 return 'ok'
441 return 'ok'
440
442
441 @view_config(route_name='test', match_param='action=test_b',
443 @view_config(route_name='test', match_param='action=test_b',
442 renderer='string', permission='root_administration')
444 renderer='string', permission='root_administration')
443 def test_b(self):
445 def test_b(self):
444 return 'ok'
446 return 'ok'
@@ -1,678 +1,678 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
3 # Copyright 2010 - 2017 RhodeCode GmbH and the AppEnlight project authors
4 #
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
7 # You may obtain a copy of the License at
8 #
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
10 #
11 # Unless required by applicable law or agreed to in writing, software
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
15 # limitations under the License.
16
16
17 import colander
17 import colander
18 import datetime
18 import datetime
19 import json
19 import json
20 import logging
20 import logging
21 import uuid
21 import uuid
22 import pyramid.security as security
22 import pyramid.security as security
23 import appenlight.lib.helpers as h
23 import appenlight.lib.helpers as h
24
24
25 from authomatic.adapters import WebObAdapter
25 from authomatic.adapters import WebObAdapter
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity
27 from pyramid.httpexceptions import HTTPFound, HTTPUnprocessableEntity
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest
28 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest
29 from pyramid.security import NO_PERMISSION_REQUIRED
29 from pyramid.security import NO_PERMISSION_REQUIRED
30 from ziggurat_foundations.models.services.external_identity import \
30 from ziggurat_foundations.models.services.external_identity import \
31 ExternalIdentityService
31 ExternalIdentityService
32 from ziggurat_foundations.models.services.user import UserService
32
33
33 from appenlight.lib import generate_random_string
34 from appenlight.lib import generate_random_string
34 from appenlight.lib.social import handle_social_data
35 from appenlight.lib.social import handle_social_data
35 from appenlight.lib.utils import channelstream_request, add_cors_headers, \
36 from appenlight.lib.utils import channelstream_request, add_cors_headers, \
36 permission_tuple_to_dict
37 permission_tuple_to_dict
37 from appenlight.models import DBSession
38 from appenlight.models import DBSession
38 from appenlight.models.alert_channels.email import EmailAlertChannel
39 from appenlight.models.alert_channels.email import EmailAlertChannel
39 from appenlight.models.alert_channel_action import AlertChannelAction
40 from appenlight.models.alert_channel_action import AlertChannelAction
40 from appenlight.models.services.alert_channel import AlertChannelService
41 from appenlight.models.services.alert_channel import AlertChannelService
41 from appenlight.models.services.alert_channel_action import \
42 from appenlight.models.services.alert_channel_action import \
42 AlertChannelActionService
43 AlertChannelActionService
43 from appenlight.models.auth_token import AuthToken
44 from appenlight.models.auth_token import AuthToken
44 from appenlight.models.report import REPORT_TYPE_MATRIX
45 from appenlight.models.report import REPORT_TYPE_MATRIX
45 from appenlight.models.user import User
46 from appenlight.models.user import User
46 from appenlight.models.services.user import UserService
47 from appenlight.models.services.user import UserService
47 from appenlight.subscribers import _
48 from appenlight.subscribers import _
48 from appenlight.validators import build_rule_schema
49 from appenlight.validators import build_rule_schema
49 from appenlight import forms
50 from appenlight import forms
50 from webob.multidict import MultiDict
51 from webob.multidict import MultiDict
51
52
52 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
53
54
54
55
55 @view_config(route_name='users_no_id', renderer='json',
56 @view_config(route_name='users_no_id', renderer='json',
56 request_method="GET", permission='root_administration')
57 request_method="GET", permission='root_administration')
57 def users_list(request):
58 def users_list(request):
58 """
59 """
59 Returns users list
60 Returns users list
60 """
61 """
61 props = ['user_name', 'id', 'first_name', 'last_name', 'email',
62 props = ['user_name', 'id', 'first_name', 'last_name', 'email',
62 'last_login_date', 'status']
63 'last_login_date', 'status']
63 users = UserService.all()
64 users = UserService.all()
64 users_dicts = []
65 users_dicts = []
65 for user in users:
66 for user in users:
66 u_dict = user.get_dict(include_keys=props)
67 u_dict = user.get_dict(include_keys=props)
67 u_dict['gravatar_url'] = user.gravatar_url(s=20)
68 u_dict['gravatar_url'] = UserService.gravatar_url(user, s=20)
68 users_dicts.append(u_dict)
69 users_dicts.append(u_dict)
69 return users_dicts
70 return users_dicts
70
71
71
72
72 @view_config(route_name='users_no_id', renderer='json',
73 @view_config(route_name='users_no_id', renderer='json',
73 request_method="POST", permission='root_administration')
74 request_method="POST", permission='root_administration')
74 def users_create(request):
75 def users_create(request):
75 """
76 """
76 Returns users list
77 Returns users list
77 """
78 """
78 form = forms.UserCreateForm(MultiDict(request.safe_json_body or {}),
79 form = forms.UserCreateForm(MultiDict(request.safe_json_body or {}),
79 csrf_context=request)
80 csrf_context=request)
80 if form.validate():
81 if form.validate():
81 log.info('registering user')
82 log.info('registering user')
82 # probably not needed in the future since this requires root anyways
83 # probably not needed in the future since this requires root anyways
83 # lets keep this here in case we lower view permission in the future
84 # lets keep this here in case we lower view permission in the future
84 # if request.registry.settings['appenlight.disable_registration']:
85 # if request.registry.settings['appenlight.disable_registration']:
85 # return HTTPUnprocessableEntity(body={'error': 'Registration is currently disabled.'})
86 # return HTTPUnprocessableEntity(body={'error': 'Registration is currently disabled.'})
86 user = User()
87 user = User()
87 # insert new user here
88 # insert new user here
88 DBSession.add(user)
89 DBSession.add(user)
89 form.populate_obj(user)
90 form.populate_obj(user)
90 user.regenerate_security_code()
91 UserService.regenerate_security_code(user)
91 user.set_password(user.user_password)
92 UserService.set_password(user, user.user_password)
92 user.status = 1 if form.status.data else 0
93 user.status = 1 if form.status.data else 0
93 request.session.flash(_('User created'))
94 request.session.flash(_('User created'))
94 DBSession.flush()
95 DBSession.flush()
95 return user.get_dict(exclude_keys=['security_code_date', 'notes',
96 return user.get_dict(exclude_keys=['security_code_date', 'notes',
96 'security_code', 'user_password'])
97 'security_code', 'user_password'])
97 else:
98 else:
98 return HTTPUnprocessableEntity(body=form.errors_json)
99 return HTTPUnprocessableEntity(body=form.errors_json)
99
100
100
101
101 @view_config(route_name='users', renderer='json',
102 @view_config(route_name='users', renderer='json',
102 request_method="GET", permission='root_administration')
103 request_method="GET", permission='root_administration')
103 @view_config(route_name='users', renderer='json',
104 @view_config(route_name='users', renderer='json',
104 request_method="PATCH", permission='root_administration')
105 request_method="PATCH", permission='root_administration')
105 def users_update(request):
106 def users_update(request):
106 """
107 """
107 Updates user object
108 Updates user object
108 """
109 """
109 user = User.by_id(request.matchdict.get('user_id'))
110 user = UserService.by_id(request.matchdict.get('user_id'))
110 if not user:
111 if not user:
111 return HTTPNotFound()
112 return HTTPNotFound()
112 post_data = request.safe_json_body or {}
113 post_data = request.safe_json_body or {}
113 if request.method == 'PATCH':
114 if request.method == 'PATCH':
114 form = forms.UserUpdateForm(MultiDict(post_data),
115 form = forms.UserUpdateForm(MultiDict(post_data),
115 csrf_context=request)
116 csrf_context=request)
116 if form.validate():
117 if form.validate():
117 form.populate_obj(user, ignore_none=True)
118 form.populate_obj(user, ignore_none=True)
118 if form.user_password.data:
119 if form.user_password.data:
119 user.set_password(user.user_password)
120 UserService.set_password(user, user.user_password)
120 if form.status.data:
121 if form.status.data:
121 user.status = 1
122 user.status = 1
122 else:
123 else:
123 user.status = 0
124 user.status = 0
124 else:
125 else:
125 return HTTPUnprocessableEntity(body=form.errors_json)
126 return HTTPUnprocessableEntity(body=form.errors_json)
126 return user.get_dict(exclude_keys=['security_code_date', 'notes',
127 return user.get_dict(exclude_keys=['security_code_date', 'notes',
127 'security_code', 'user_password'])
128 'security_code', 'user_password'])
128
129
129
130
130 @view_config(route_name='users_property',
131 @view_config(route_name='users_property',
131 match_param='key=resource_permissions',
132 match_param='key=resource_permissions',
132 renderer='json', permission='authenticated')
133 renderer='json', permission='authenticated')
133 def users_resource_permissions_list(request):
134 def users_resource_permissions_list(request):
134 """
135 """
135 Get list of permissions assigned to specific resources
136 Get list of permissions assigned to specific resources
136 """
137 """
137 user = User.by_id(request.matchdict.get('user_id'))
138 user = UserService.by_id(request.matchdict.get('user_id'))
138 if not user:
139 if not user:
139 return HTTPNotFound()
140 return HTTPNotFound()
140 return [permission_tuple_to_dict(perm) for perm in
141 return [permission_tuple_to_dict(perm) for perm in
141 user.resources_with_possible_perms()]
142 UserService.resources_with_possible_perms(user)]
142
143
143
144
144 @view_config(route_name='users', renderer='json',
145 @view_config(route_name='users', renderer='json',
145 request_method="DELETE", permission='root_administration')
146 request_method="DELETE", permission='root_administration')
146 def users_DELETE(request):
147 def users_DELETE(request):
147 """
148 """
148 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
149 operation there will be at least one admin left
150 operation there will be at least one admin left
150 """
151 """
151 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')
152 user = User.by_id(request.matchdict.get('user_id'))
153 user = UserService.by_id(request.matchdict.get('user_id'))
153 if user:
154 if user:
154 users = User.users_for_perms(['root_administration']).all()
155 users = UserService.users_for_perms(['root_administration']).all()
155 if len(users) < 2 and user.id == users[0].id:
156 if len(users) < 2 and user.id == users[0].id:
156 request.session.flash(msg, 'warning')
157 request.session.flash(msg, 'warning')
157 else:
158 else:
158 DBSession.delete(user)
159 DBSession.delete(user)
159 request.session.flash(_('User removed'))
160 request.session.flash(_('User removed'))
160 return True
161 return True
161 request.response.status = 422
162 request.response.status = 422
162 return False
163 return False
163
164
164
165
165 @view_config(route_name='users_self', renderer='json',
166 @view_config(route_name='users_self', renderer='json',
166 request_method="GET", permission='authenticated')
167 request_method="GET", permission='authenticated')
167 @view_config(route_name='users_self', renderer='json',
168 @view_config(route_name='users_self', renderer='json',
168 request_method="PATCH", permission='authenticated')
169 request_method="PATCH", permission='authenticated')
169 def users_self(request):
170 def users_self(request):
170 """
171 """
171 Updates user personal information
172 Updates user personal information
172 """
173 """
173
174
174 if request.method == 'PATCH':
175 if request.method == 'PATCH':
175 form = forms.gen_user_profile_form()(
176 form = forms.gen_user_profile_form()(
176 MultiDict(request.unsafe_json_body),
177 MultiDict(request.unsafe_json_body),
177 csrf_context=request)
178 csrf_context=request)
178 if form.validate():
179 if form.validate():
179 form.populate_obj(request.user)
180 form.populate_obj(request.user)
180 request.session.flash(_('Your profile got updated.'))
181 request.session.flash(_('Your profile got updated.'))
181 else:
182 else:
182 return HTTPUnprocessableEntity(body=form.errors_json)
183 return HTTPUnprocessableEntity(body=form.errors_json)
183 return request.user.get_dict(
184 return request.user.get_dict(
184 exclude_keys=['security_code_date', 'notes', 'security_code',
185 exclude_keys=['security_code_date', 'notes', 'security_code',
185 'user_password'],
186 'user_password'],
186 extended_info=True)
187 extended_info=True)
187
188
188
189
189 @view_config(route_name='users_self_property',
190 @view_config(route_name='users_self_property',
190 match_param='key=external_identities', renderer='json',
191 match_param='key=external_identities', renderer='json',
191 request_method='GET', permission='authenticated')
192 request_method='GET', permission='authenticated')
192 def users_external_identies(request):
193 def users_external_identies(request):
193 user = request.user
194 user = request.user
194 identities = [{'provider': ident.provider_name,
195 identities = [{'provider': ident.provider_name,
195 'id': ident.external_user_name} for ident
196 'id': ident.external_user_name} for ident
196 in user.external_identities.all()]
197 in user.external_identities.all()]
197 return identities
198 return identities
198
199
199
200
200 @view_config(route_name='users_self_property',
201 @view_config(route_name='users_self_property',
201 match_param='key=external_identities', renderer='json',
202 match_param='key=external_identities', renderer='json',
202 request_method='DELETE', permission='authenticated')
203 request_method='DELETE', permission='authenticated')
203 def users_external_identies_DELETE(request):
204 def users_external_identies_DELETE(request):
204 """
205 """
205 Unbinds external identities(google,twitter etc.) from user account
206 Unbinds external identities(google,twitter etc.) from user account
206 """
207 """
207 user = request.user
208 user = request.user
208 for identity in user.external_identities.all():
209 for identity in user.external_identities.all():
209 log.info('found identity %s' % identity)
210 log.info('found identity %s' % identity)
210 if (identity.provider_name == request.params.get('provider') and
211 if (identity.provider_name == request.params.get('provider') and
211 identity.external_user_name == request.params.get('id')):
212 identity.external_user_name == request.params.get('id')):
212 log.info('remove identity %s' % identity)
213 log.info('remove identity %s' % identity)
213 DBSession.delete(identity)
214 DBSession.delete(identity)
214 return True
215 return True
215 return False
216 return False
216
217
217
218
218 @view_config(route_name='users_self_property',
219 @view_config(route_name='users_self_property',
219 match_param='key=password', renderer='json',
220 match_param='key=password', renderer='json',
220 request_method='PATCH', permission='authenticated')
221 request_method='PATCH', permission='authenticated')
221 def users_password(request):
222 def users_password(request):
222 """
223 """
223 Sets new password for user account
224 Sets new password for user account
224 """
225 """
225 user = request.user
226 user = request.user
226 form = forms.ChangePasswordForm(MultiDict(request.unsafe_json_body),
227 form = forms.ChangePasswordForm(MultiDict(request.unsafe_json_body),
227 csrf_context=request)
228 csrf_context=request)
228 form.old_password.user = user
229 form.old_password.user = user
229 if form.validate():
230 if form.validate():
230 user.regenerate_security_code()
231 UserService.regenerate_security_code(user)
231 user.set_password(form.new_password.data)
232 UserService.set_password(user, form.new_password.data)
232 msg = 'Your password got updated. ' \
233 msg = 'Your password got updated. ' \
233 'Next time log in with your new credentials.'
234 'Next time log in with your new credentials.'
234 request.session.flash(_(msg))
235 request.session.flash(_(msg))
235 return True
236 return True
236 else:
237 else:
237 return HTTPUnprocessableEntity(body=form.errors_json)
238 return HTTPUnprocessableEntity(body=form.errors_json)
238 return False
239 return False
239
240
240
241
241 @view_config(route_name='users_self_property', match_param='key=websocket',
242 @view_config(route_name='users_self_property', match_param='key=websocket',
242 renderer='json', permission='authenticated')
243 renderer='json', permission='authenticated')
243 def users_websocket(request):
244 def users_websocket(request):
244 """
245 """
245 Handle authorization of users trying to connect
246 Handle authorization of users trying to connect
246 """
247 """
247 # handle preflight request
248 # handle preflight request
248 user = request.user
249 user = request.user
249 if request.method == 'OPTIONS':
250 if request.method == 'OPTIONS':
250 res = request.response.body('OK')
251 res = request.response.body('OK')
251 add_cors_headers(res)
252 add_cors_headers(res)
252 return res
253 return res
253 applications = user.resources_with_perms(
254 applications = UserService.resources_with_perms(user, ['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 = channelstream_request(
261 response = channelstream_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': "AppEnlight :: "
399 'email_title': "AppEnlight :: "
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.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 = UserService.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 UserService.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 = UserService.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 = UserService.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 = UserService.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
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
This diff has been collapsed as it changes many lines, (1082 lines changed) Show them Hide them
General Comments 0
You need to be logged in to leave comments. Login now