##// END OF EJS Templates
audit-logs: use stricter limit on how much data the commits key can hold....
marcink -
r1964:70ea4c96 default
parent child Browse files
Show More
@@ -1,425 +1,425 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of hooks run by RhodeCode Enterprise
24 24 """
25 25
26 26 import os
27 27 import collections
28 28 import logging
29 29
30 30 import rhodecode
31 31 from rhodecode import events
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib.utils2 import safe_str
35 35 from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError
36 36 from rhodecode.model.db import Repository, User
37 37
38 38 log = logging.getLogger(__name__)
39 39
40 40
41 41 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
42 42
43 43
44 44 def is_shadow_repo(extras):
45 45 """
46 46 Returns ``True`` if this is an action executed against a shadow repository.
47 47 """
48 48 return extras['is_shadow_repo']
49 49
50 50
51 51 def _get_scm_size(alias, root_path):
52 52
53 53 if not alias.startswith('.'):
54 54 alias += '.'
55 55
56 56 size_scm, size_root = 0, 0
57 57 for path, unused_dirs, files in os.walk(safe_str(root_path)):
58 58 if path.find(alias) != -1:
59 59 for f in files:
60 60 try:
61 61 size_scm += os.path.getsize(os.path.join(path, f))
62 62 except OSError:
63 63 pass
64 64 else:
65 65 for f in files:
66 66 try:
67 67 size_root += os.path.getsize(os.path.join(path, f))
68 68 except OSError:
69 69 pass
70 70
71 71 size_scm_f = h.format_byte_size_binary(size_scm)
72 72 size_root_f = h.format_byte_size_binary(size_root)
73 73 size_total_f = h.format_byte_size_binary(size_root + size_scm)
74 74
75 75 return size_scm_f, size_root_f, size_total_f
76 76
77 77
78 78 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
79 79 def repo_size(extras):
80 80 """Present size of repository after push."""
81 81 repo = Repository.get_by_repo_name(extras.repository)
82 82 vcs_part = safe_str(u'.%s' % repo.repo_type)
83 83 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
84 84 repo.repo_full_path)
85 85 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
86 86 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
87 87 return HookResponse(0, msg)
88 88
89 89
90 90 def pre_push(extras):
91 91 """
92 92 Hook executed before pushing code.
93 93
94 94 It bans pushing when the repository is locked.
95 95 """
96 96
97 97 usr = User.get_by_username(extras.username)
98 98 output = ''
99 99 if extras.locked_by[0] and usr.user_id != int(extras.locked_by[0]):
100 100 locked_by = User.get(extras.locked_by[0]).username
101 101 reason = extras.locked_by[2]
102 102 # this exception is interpreted in git/hg middlewares and based
103 103 # on that proper return code is server to client
104 104 _http_ret = HTTPLockedRC(
105 105 _locked_by_explanation(extras.repository, locked_by, reason))
106 106 if str(_http_ret.code).startswith('2'):
107 107 # 2xx Codes don't raise exceptions
108 108 output = _http_ret.title
109 109 else:
110 110 raise _http_ret
111 111
112 112 # Propagate to external components. This is done after checking the
113 113 # lock, for consistent behavior.
114 114 if not is_shadow_repo(extras):
115 115 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
116 116 events.trigger(events.RepoPrePushEvent(
117 117 repo_name=extras.repository, extras=extras))
118 118
119 119 return HookResponse(0, output)
120 120
121 121
122 122 def pre_pull(extras):
123 123 """
124 124 Hook executed before pulling the code.
125 125
126 126 It bans pulling when the repository is locked.
127 127 """
128 128
129 129 output = ''
130 130 if extras.locked_by[0]:
131 131 locked_by = User.get(extras.locked_by[0]).username
132 132 reason = extras.locked_by[2]
133 133 # this exception is interpreted in git/hg middlewares and based
134 134 # on that proper return code is server to client
135 135 _http_ret = HTTPLockedRC(
136 136 _locked_by_explanation(extras.repository, locked_by, reason))
137 137 if str(_http_ret.code).startswith('2'):
138 138 # 2xx Codes don't raise exceptions
139 139 output = _http_ret.title
140 140 else:
141 141 raise _http_ret
142 142
143 143 # Propagate to external components. This is done after checking the
144 144 # lock, for consistent behavior.
145 145 if not is_shadow_repo(extras):
146 146 pre_pull_extension(**extras)
147 147 events.trigger(events.RepoPrePullEvent(
148 148 repo_name=extras.repository, extras=extras))
149 149
150 150 return HookResponse(0, output)
151 151
152 152
153 153 def post_pull(extras):
154 154 """Hook executed after client pulls the code."""
155 155
156 156 audit_user = audit_logger.UserWrap(
157 157 username=extras.username,
158 158 ip_addr=extras.ip)
159 159 repo = audit_logger.RepoWrap(repo_name=extras.repository)
160 160 audit_logger.store(
161 161 'user.pull', action_data={
162 162 'user_agent': extras.user_agent},
163 163 user=audit_user, repo=repo, commit=True)
164 164
165 165 # Propagate to external components.
166 166 if not is_shadow_repo(extras):
167 167 post_pull_extension(**extras)
168 168 events.trigger(events.RepoPullEvent(
169 169 repo_name=extras.repository, extras=extras))
170 170
171 171 output = ''
172 172 # make lock is a tri state False, True, None. We only make lock on True
173 173 if extras.make_lock is True and not is_shadow_repo(extras):
174 174 user = User.get_by_username(extras.username)
175 175 Repository.lock(Repository.get_by_repo_name(extras.repository),
176 176 user.user_id,
177 177 lock_reason=Repository.LOCK_PULL)
178 178 msg = 'Made lock on repo `%s`' % (extras.repository,)
179 179 output += msg
180 180
181 181 if extras.locked_by[0]:
182 182 locked_by = User.get(extras.locked_by[0]).username
183 183 reason = extras.locked_by[2]
184 184 _http_ret = HTTPLockedRC(
185 185 _locked_by_explanation(extras.repository, locked_by, reason))
186 186 if str(_http_ret.code).startswith('2'):
187 187 # 2xx Codes don't raise exceptions
188 188 output += _http_ret.title
189 189
190 190 return HookResponse(0, output)
191 191
192 192
193 193 def post_push(extras):
194 194 """Hook executed after user pushes to the repository."""
195 195 commit_ids = extras.commit_ids
196 196
197 197 # log the push call
198 198 audit_user = audit_logger.UserWrap(
199 199 username=extras.username, ip_addr=extras.ip)
200 200 repo = audit_logger.RepoWrap(repo_name=extras.repository)
201 201 audit_logger.store(
202 202 'user.push', action_data={
203 203 'user_agent': extras.user_agent,
204 'commit_ids': commit_ids[:10000]},
204 'commit_ids': commit_ids[:400]},
205 205 user=audit_user, repo=repo, commit=True)
206 206
207 207 # Propagate to external components.
208 208 if not is_shadow_repo(extras):
209 209 post_push_extension(
210 210 repo_store_path=Repository.base_path(),
211 211 pushed_revs=commit_ids,
212 212 **extras)
213 213 events.trigger(events.RepoPushEvent(
214 214 repo_name=extras.repository,
215 215 pushed_commit_ids=commit_ids,
216 216 extras=extras))
217 217
218 218 output = ''
219 219 # make lock is a tri state False, True, None. We only release lock on False
220 220 if extras.make_lock is False and not is_shadow_repo(extras):
221 221 Repository.unlock(Repository.get_by_repo_name(extras.repository))
222 222 msg = 'Released lock on repo `%s`\n' % extras.repository
223 223 output += msg
224 224
225 225 if extras.locked_by[0]:
226 226 locked_by = User.get(extras.locked_by[0]).username
227 227 reason = extras.locked_by[2]
228 228 _http_ret = HTTPLockedRC(
229 229 _locked_by_explanation(extras.repository, locked_by, reason))
230 230 # TODO: johbo: if not?
231 231 if str(_http_ret.code).startswith('2'):
232 232 # 2xx Codes don't raise exceptions
233 233 output += _http_ret.title
234 234
235 235 if extras.new_refs:
236 236 tmpl = \
237 237 extras.server_url + '/' + \
238 238 extras.repository + \
239 239 "/pull-request/new?{ref_type}={ref_name}"
240 240 for branch_name in extras.new_refs['branches']:
241 241 output += 'RhodeCode: open pull request link: {}\n'.format(
242 242 tmpl.format(ref_type='branch', ref_name=branch_name))
243 243
244 244 for book_name in extras.new_refs['bookmarks']:
245 245 output += 'RhodeCode: open pull request link: {}\n'.format(
246 246 tmpl.format(ref_type='bookmark', ref_name=book_name))
247 247
248 248 output += 'RhodeCode: push completed\n'
249 249 return HookResponse(0, output)
250 250
251 251
252 252 def _locked_by_explanation(repo_name, user_name, reason):
253 253 message = (
254 254 'Repository `%s` locked by user `%s`. Reason:`%s`'
255 255 % (repo_name, user_name, reason))
256 256 return message
257 257
258 258
259 259 def check_allowed_create_user(user_dict, created_by, **kwargs):
260 260 # pre create hooks
261 261 if pre_create_user.is_active():
262 262 allowed, reason = pre_create_user(created_by=created_by, **user_dict)
263 263 if not allowed:
264 264 raise UserCreationError(reason)
265 265
266 266
267 267 class ExtensionCallback(object):
268 268 """
269 269 Forwards a given call to rcextensions, sanitizes keyword arguments.
270 270
271 271 Does check if there is an extension active for that hook. If it is
272 272 there, it will forward all `kwargs_keys` keyword arguments to the
273 273 extension callback.
274 274 """
275 275
276 276 def __init__(self, hook_name, kwargs_keys):
277 277 self._hook_name = hook_name
278 278 self._kwargs_keys = set(kwargs_keys)
279 279
280 280 def __call__(self, *args, **kwargs):
281 281 log.debug('Calling extension callback for %s', self._hook_name)
282 282
283 283 kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys)
284 284 # backward compat for removed api_key for old hooks. THis was it works
285 285 # with older rcextensions that require api_key present
286 286 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
287 287 kwargs_to_pass['api_key'] = '_DEPRECATED_'
288 288
289 289 callback = self._get_callback()
290 290 if callback:
291 291 return callback(**kwargs_to_pass)
292 292 else:
293 293 log.debug('extensions callback not found skipping...')
294 294
295 295 def is_active(self):
296 296 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
297 297
298 298 def _get_callback(self):
299 299 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
300 300
301 301
302 302 pre_pull_extension = ExtensionCallback(
303 303 hook_name='PRE_PULL_HOOK',
304 304 kwargs_keys=(
305 305 'server_url', 'config', 'scm', 'username', 'ip', 'action',
306 306 'repository'))
307 307
308 308
309 309 post_pull_extension = ExtensionCallback(
310 310 hook_name='PULL_HOOK',
311 311 kwargs_keys=(
312 312 'server_url', 'config', 'scm', 'username', 'ip', 'action',
313 313 'repository'))
314 314
315 315
316 316 pre_push_extension = ExtensionCallback(
317 317 hook_name='PRE_PUSH_HOOK',
318 318 kwargs_keys=(
319 319 'server_url', 'config', 'scm', 'username', 'ip', 'action',
320 320 'repository', 'repo_store_path', 'commit_ids'))
321 321
322 322
323 323 post_push_extension = ExtensionCallback(
324 324 hook_name='PUSH_HOOK',
325 325 kwargs_keys=(
326 326 'server_url', 'config', 'scm', 'username', 'ip', 'action',
327 327 'repository', 'repo_store_path', 'pushed_revs'))
328 328
329 329
330 330 pre_create_user = ExtensionCallback(
331 331 hook_name='PRE_CREATE_USER_HOOK',
332 332 kwargs_keys=(
333 333 'username', 'password', 'email', 'firstname', 'lastname', 'active',
334 334 'admin', 'created_by'))
335 335
336 336
337 337 log_create_pull_request = ExtensionCallback(
338 338 hook_name='CREATE_PULL_REQUEST',
339 339 kwargs_keys=(
340 340 'server_url', 'config', 'scm', 'username', 'ip', 'action',
341 341 'repository', 'pull_request_id', 'url', 'title', 'description',
342 342 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
343 343 'mergeable', 'source', 'target', 'author', 'reviewers'))
344 344
345 345
346 346 log_merge_pull_request = ExtensionCallback(
347 347 hook_name='MERGE_PULL_REQUEST',
348 348 kwargs_keys=(
349 349 'server_url', 'config', 'scm', 'username', 'ip', 'action',
350 350 'repository', 'pull_request_id', 'url', 'title', 'description',
351 351 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
352 352 'mergeable', 'source', 'target', 'author', 'reviewers'))
353 353
354 354
355 355 log_close_pull_request = ExtensionCallback(
356 356 hook_name='CLOSE_PULL_REQUEST',
357 357 kwargs_keys=(
358 358 'server_url', 'config', 'scm', 'username', 'ip', 'action',
359 359 'repository', 'pull_request_id', 'url', 'title', 'description',
360 360 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
361 361 'mergeable', 'source', 'target', 'author', 'reviewers'))
362 362
363 363
364 364 log_review_pull_request = ExtensionCallback(
365 365 hook_name='REVIEW_PULL_REQUEST',
366 366 kwargs_keys=(
367 367 'server_url', 'config', 'scm', 'username', 'ip', 'action',
368 368 'repository', 'pull_request_id', 'url', 'title', 'description',
369 369 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
370 370 'mergeable', 'source', 'target', 'author', 'reviewers'))
371 371
372 372
373 373 log_update_pull_request = ExtensionCallback(
374 374 hook_name='UPDATE_PULL_REQUEST',
375 375 kwargs_keys=(
376 376 'server_url', 'config', 'scm', 'username', 'ip', 'action',
377 377 'repository', 'pull_request_id', 'url', 'title', 'description',
378 378 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
379 379 'mergeable', 'source', 'target', 'author', 'reviewers'))
380 380
381 381
382 382 log_create_user = ExtensionCallback(
383 383 hook_name='CREATE_USER_HOOK',
384 384 kwargs_keys=(
385 385 'username', 'full_name_or_username', 'full_contact', 'user_id',
386 386 'name', 'firstname', 'short_contact', 'admin', 'lastname',
387 387 'ip_addresses', 'extern_type', 'extern_name',
388 388 'email', 'api_keys', 'last_login',
389 389 'full_name', 'active', 'password', 'emails',
390 390 'inherit_default_permissions', 'created_by', 'created_on'))
391 391
392 392
393 393 log_delete_user = ExtensionCallback(
394 394 hook_name='DELETE_USER_HOOK',
395 395 kwargs_keys=(
396 396 'username', 'full_name_or_username', 'full_contact', 'user_id',
397 397 'name', 'firstname', 'short_contact', 'admin', 'lastname',
398 398 'ip_addresses',
399 399 'email', 'last_login',
400 400 'full_name', 'active', 'password', 'emails',
401 401 'inherit_default_permissions', 'deleted_by'))
402 402
403 403
404 404 log_create_repository = ExtensionCallback(
405 405 hook_name='CREATE_REPO_HOOK',
406 406 kwargs_keys=(
407 407 'repo_name', 'repo_type', 'description', 'private', 'created_on',
408 408 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
409 409 'clone_uri', 'fork_id', 'group_id', 'created_by'))
410 410
411 411
412 412 log_delete_repository = ExtensionCallback(
413 413 hook_name='DELETE_REPO_HOOK',
414 414 kwargs_keys=(
415 415 'repo_name', 'repo_type', 'description', 'private', 'created_on',
416 416 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
417 417 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
418 418
419 419
420 420 log_create_repository_group = ExtensionCallback(
421 421 hook_name='CREATE_REPO_GROUP_HOOK',
422 422 kwargs_keys=(
423 423 'group_name', 'group_parent_id', 'group_description',
424 424 'group_id', 'user_id', 'created_by', 'created_on',
425 425 'enable_locking'))
@@ -1,141 +1,141 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import mock
22 22 import pytest
23 23 from rhodecode.model.db import Session, UserLog
24 24 from rhodecode.lib import hooks_base, utils2
25 25
26 26
27 27 def test_post_push_truncates_commits(user_regular, repo_stub):
28 28 extras = {
29 29 'ip': '127.0.0.1',
30 30 'username': user_regular.username,
31 31 'action': 'push_local',
32 32 'repository': repo_stub.repo_name,
33 33 'scm': 'git',
34 34 'config': '',
35 35 'server_url': 'http://example.com',
36 36 'make_lock': None,
37 37 'user_agent': 'some-client',
38 38 'locked_by': [None],
39 39 'commit_ids': ['abcde12345' * 4] * 30000,
40 40 'is_shadow_repo': False,
41 41 }
42 42 extras = utils2.AttributeDict(extras)
43 43
44 44 hooks_base.post_push(extras)
45 45
46 46 # Calculate appropriate action string here
47 commit_ids = extras.commit_ids[:10000]
47 commit_ids = extras.commit_ids[:400]
48 48
49 49 entry = UserLog.query().order_by('-user_log_id').first()
50 50 assert entry.action == 'user.push'
51 51 assert entry.action_data['commit_ids'] == commit_ids
52 52 Session().delete(entry)
53 53 Session().commit()
54 54
55 55
56 56 def assert_called_with_mock(callable_, expected_mock_name):
57 57 mock_obj = callable_.call_args[0][0]
58 58 mock_name = mock_obj._mock_new_parent._mock_new_name
59 59 assert mock_name == expected_mock_name
60 60
61 61
62 62 @pytest.fixture
63 63 def hook_extras(user_regular, repo_stub):
64 64 extras = utils2.AttributeDict({
65 65 'ip': '127.0.0.1',
66 66 'username': user_regular.username,
67 67 'action': 'push',
68 68 'repository': repo_stub.repo_name,
69 69 'scm': '',
70 70 'config': '',
71 71 'server_url': 'http://example.com',
72 72 'make_lock': None,
73 73 'user_agent': 'some-client',
74 74 'locked_by': [None],
75 75 'commit_ids': [],
76 76 'is_shadow_repo': False,
77 77 })
78 78 return extras
79 79
80 80
81 81 @pytest.mark.parametrize('func, extension, event', [
82 82 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
83 83 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
84 84 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
85 85 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
86 86 ])
87 87 def test_hooks_propagate(func, extension, event, hook_extras):
88 88 """
89 89 Tests that our hook code propagates to rhodecode extensions and triggers
90 90 the appropriate event.
91 91 """
92 92 extension_mock = mock.Mock()
93 93 events_mock = mock.Mock()
94 94 patches = {
95 95 'Repository': mock.Mock(),
96 96 'events': events_mock,
97 97 extension: extension_mock,
98 98 }
99 99
100 100 # Clear shadow repo flag.
101 101 hook_extras.is_shadow_repo = False
102 102
103 103 # Execute hook function.
104 104 with mock.patch.multiple(hooks_base, **patches):
105 105 func(hook_extras)
106 106
107 107 # Assert that extensions are called and event was fired.
108 108 extension_mock.called_once()
109 109 assert_called_with_mock(events_mock.trigger, event)
110 110
111 111
112 112 @pytest.mark.parametrize('func, extension, event', [
113 113 (hooks_base.pre_push, 'pre_push_extension', 'RepoPrePushEvent'),
114 114 (hooks_base.post_push, 'post_pull_extension', 'RepoPushEvent'),
115 115 (hooks_base.pre_pull, 'pre_pull_extension', 'RepoPrePullEvent'),
116 116 (hooks_base.post_pull, 'post_push_extension', 'RepoPullEvent'),
117 117 ])
118 118 def test_hooks_propagates_not_on_shadow(func, extension, event, hook_extras):
119 119 """
120 120 If hooks are called by a request to a shadow repo we only want to run our
121 121 internal hooks code but not external ones like rhodecode extensions or
122 122 trigger an event.
123 123 """
124 124 extension_mock = mock.Mock()
125 125 events_mock = mock.Mock()
126 126 patches = {
127 127 'Repository': mock.Mock(),
128 128 'events': events_mock,
129 129 extension: extension_mock,
130 130 }
131 131
132 132 # Set shadow repo flag.
133 133 hook_extras.is_shadow_repo = True
134 134
135 135 # Execute hook function.
136 136 with mock.patch.multiple(hooks_base, **patches):
137 137 func(hook_extras)
138 138
139 139 # Assert that extensions are *not* called and event was *not* fired.
140 140 assert not extension_mock.called
141 141 assert not events_mock.trigger.called
General Comments 0
You need to be logged in to leave comments. Login now