##// END OF EJS Templates
audit-logs: implemented pull request and comment events.
marcink -
r1807:83e09901 default
parent child Browse files
Show More
@@ -47,13 +47,12 b' class TestClosePullRequest(object):'
47 'closed': True,
47 'closed': True,
48 }
48 }
49 assert_ok(id_, expected, response.body)
49 assert_ok(id_, expected, response.body)
50 action = 'user_closed_pull_request:%d' % pull_request_id
51 journal = UserLog.query()\
50 journal = UserLog.query()\
52 .filter(UserLog.user_id == author)\
51 .filter(UserLog.user_id == author) \
52 .order_by('user_log_id') \
53 .filter(UserLog.repository_id == repo)\
53 .filter(UserLog.repository_id == repo)\
54 .filter(UserLog.action == action)\
55 .all()
54 .all()
56 assert len(journal) == 1
55 assert journal[-1].action == 'repo.pull_request.close'
57
56
58 @pytest.mark.backends("git", "hg")
57 @pytest.mark.backends("git", "hg")
59 def test_api_close_pull_request_already_closed_error(self, pr_util):
58 def test_api_close_pull_request_already_closed_error(self, pr_util):
@@ -62,13 +62,12 b' class TestCommentPullRequest(object):'
62 }
62 }
63 assert_ok(id_, expected, response.body)
63 assert_ok(id_, expected, response.body)
64
64
65 action = 'user_commented_pull_request:%d' % pull_request_id
66 journal = UserLog.query()\
65 journal = UserLog.query()\
67 .filter(UserLog.user_id == author)\
66 .filter(UserLog.user_id == author)\
68 .filter(UserLog.repository_id == repo)\
67 .filter(UserLog.repository_id == repo) \
69 .filter(UserLog.action == action)\
68 .order_by('user_log_id') \
70 .all()
69 .all()
71 assert len(journal) == 2
70 assert journal[-1].action == 'repo.pull_request.comment.create'
72
71
73 @pytest.mark.backends("git", "hg")
72 @pytest.mark.backends("git", "hg")
74 def test_api_comment_pull_request_change_status(
73 def test_api_comment_pull_request_change_status(
@@ -33,7 +33,7 b' pytestmark = pytest.mark.backends("git",'
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequest(object):
34 class TestGetPullRequest(object):
35
35
36 def test_api_get_pull_request(self, pr_util, http_host_stub, http_host_only_stub):
36 def test_api_get_pull_request(self, pr_util, http_host_only_stub):
37 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38 pull_request = pr_util.create_pull_request(mergeable=True)
38 pull_request = pr_util.create_pull_request(mergeable=True)
39 id_, params = build_data(
39 id_, params = build_data(
@@ -52,7 +52,7 b' class TestGetPullRequest(object):'
52 pull_request_id=pull_request.pull_request_id, qualified=True))
52 pull_request_id=pull_request.pull_request_id, qualified=True))
53
53
54 pr_url = safe_unicode(
54 pr_url = safe_unicode(
55 url_obj.with_netloc(http_host_stub))
55 url_obj.with_netloc(http_host_only_stub))
56 source_url = safe_unicode(
56 source_url = safe_unicode(
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
58 target_url = safe_unicode(
58 target_url = safe_unicode(
@@ -95,13 +95,13 b' class TestMergePullRequest(object):'
95
95
96 assert_ok(id_, expected, response.body)
96 assert_ok(id_, expected, response.body)
97
97
98 action = 'user_merged_pull_request:%d' % (pull_request_id, )
99 journal = UserLog.query()\
98 journal = UserLog.query()\
100 .filter(UserLog.user_id == author)\
99 .filter(UserLog.user_id == author)\
101 .filter(UserLog.repository_id == repo)\
100 .filter(UserLog.repository_id == repo) \
102 .filter(UserLog.action == action)\
101 .order_by('user_log_id') \
103 .all()
102 .all()
104 assert len(journal) == 1
103 assert journal[-2].action == 'repo.pull_request.merge'
104 assert journal[-1].action == 'repo.pull_request.close'
105
105
106 id_, params = build_data(
106 id_, params = build_data(
107 self.apikey, 'merge_pull_request',
107 self.apikey, 'merge_pull_request',
@@ -33,7 +33,7 b' class TestUpdatePullRequest(object):'
33
33
34 @pytest.mark.backends("git", "hg")
34 @pytest.mark.backends("git", "hg")
35 def test_api_update_pull_request_title_or_description(
35 def test_api_update_pull_request_title_or_description(
36 self, pr_util, silence_action_logger, no_notifications):
36 self, pr_util, no_notifications):
37 pull_request = pr_util.create_pull_request()
37 pull_request = pr_util.create_pull_request()
38
38
39 id_, params = build_data(
39 id_, params = build_data(
@@ -61,7 +61,7 b' class TestUpdatePullRequest(object):'
61
61
62 @pytest.mark.backends("git", "hg")
62 @pytest.mark.backends("git", "hg")
63 def test_api_try_update_closed_pull_request(
63 def test_api_try_update_closed_pull_request(
64 self, pr_util, silence_action_logger, no_notifications):
64 self, pr_util, no_notifications):
65 pull_request = pr_util.create_pull_request()
65 pull_request = pr_util.create_pull_request()
66 PullRequestModel().close_pull_request(
66 PullRequestModel().close_pull_request(
67 pull_request, TEST_USER_ADMIN_LOGIN)
67 pull_request, TEST_USER_ADMIN_LOGIN)
@@ -78,8 +78,7 b' class TestUpdatePullRequest(object):'
78 assert_error(id_, expected, response.body)
78 assert_error(id_, expected, response.body)
79
79
80 @pytest.mark.backends("git", "hg")
80 @pytest.mark.backends("git", "hg")
81 def test_api_update_update_commits(
81 def test_api_update_update_commits(self, pr_util, no_notifications):
82 self, pr_util, silence_action_logger, no_notifications):
83 commits = [
82 commits = [
84 {'message': 'a'},
83 {'message': 'a'},
85 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
84 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
@@ -119,7 +118,7 b' class TestUpdatePullRequest(object):'
119
118
120 @pytest.mark.backends("git", "hg")
119 @pytest.mark.backends("git", "hg")
121 def test_api_update_change_reviewers(
120 def test_api_update_change_reviewers(
122 self, user_util, pr_util, silence_action_logger, no_notifications):
121 self, user_util, pr_util, no_notifications):
123 a = user_util.create_user()
122 a = user_util.create_user()
124 b = user_util.create_user()
123 b = user_util.create_user()
125 c = user_util.create_user()
124 c = user_util.create_user()
@@ -669,7 +669,7 b' def update_pull_request('
669 if title or description:
669 if title or description:
670 PullRequestModel().edit(
670 PullRequestModel().edit(
671 pull_request, title or pull_request.title,
671 pull_request, title or pull_request.title,
672 description or pull_request.description)
672 description or pull_request.description, apiuser)
673 Session().commit()
673 Session().commit()
674
674
675 commit_changes = {"added": [], "common": [], "removed": []}
675 commit_changes = {"added": [], "common": [], "removed": []}
@@ -683,7 +683,7 b' def update_pull_request('
683 reviewers_changes = {"added": [], "removed": []}
683 reviewers_changes = {"added": [], "removed": []}
684 if reviewers:
684 if reviewers:
685 added_reviewers, removed_reviewers = \
685 added_reviewers, removed_reviewers = \
686 PullRequestModel().update_reviewers(pull_request, reviewers)
686 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
687
687
688 reviewers_changes['added'] = sorted(
688 reviewers_changes['added'] = sorted(
689 [get_user_or_error(n).username for n in added_reviewers])
689 [get_user_or_error(n).username for n in added_reviewers])
@@ -628,8 +628,8 b' class UsersController(BaseController):'
628
628
629 ip_id = request.POST.get('del_ip_id')
629 ip_id = request.POST.get('del_ip_id')
630 user_model = UserModel()
630 user_model = UserModel()
631 user_data = c.user.get_api_data()
631 ip = UserIpMap.query().get(ip_id).ip_addr
632 ip = UserIpMap.query().get(ip_id).ip_addr
632 user_data = c.user.get_api_data()
633 user_model.delete_extra_ip(user_id, ip_id)
633 user_model.delete_extra_ip(user_id, ip_id)
634 audit_logger.store_web(
634 audit_logger.store_web(
635 'user.edit.ip.delete',
635 'user.edit.ip.delete',
@@ -440,7 +440,7 b' class ChangesetController(BaseRepoContro'
440 owner = (comment.author.user_id == c.rhodecode_user.user_id)
440 owner = (comment.author.user_id == c.rhodecode_user.user_id)
441 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
441 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
442 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
442 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
443 CommentsModel().delete(comment=comment)
443 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
444 Session().commit()
444 Session().commit()
445 return True
445 return True
446 else:
446 else:
@@ -38,7 +38,7 b' from rhodecode.lib import diffs, helpers'
38 from rhodecode.lib import audit_logger
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.codeblocks import (
39 from rhodecode.lib.codeblocks import (
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
40 filenode_as_lines_tokens, filenode_as_annotated_lines_tokens)
41 from rhodecode.lib.utils import jsonify, action_logger
41 from rhodecode.lib.utils import jsonify
42 from rhodecode.lib.utils2 import (
42 from rhodecode.lib.utils2 import (
43 convert_line_endings, detect_mode, safe_str, str2bool)
43 convert_line_endings, detect_mode, safe_str, str2bool)
44 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
@@ -317,7 +317,7 b' class PullrequestsController(BaseRepoCon'
317 try:
317 try:
318 PullRequestModel().edit(
318 PullRequestModel().edit(
319 pull_request, request.POST.get('title'),
319 pull_request, request.POST.get('title'),
320 request.POST.get('description'))
320 request.POST.get('description'), c.rhodecode_user)
321 except ValueError:
321 except ValueError:
322 msg = _(u'Cannot update closed pull requests.')
322 msg = _(u'Cannot update closed pull requests.')
323 h.flash(msg, category='error')
323 h.flash(msg, category='error')
@@ -456,7 +456,8 b' class PullrequestsController(BaseRepoCon'
456 h.flash(e, category='error')
456 h.flash(e, category='error')
457 return
457 return
458
458
459 PullRequestModel().update_reviewers(pull_request_id, reviewers)
459 PullRequestModel().update_reviewers(
460 pull_request_id, reviewers, c.rhodecode_user)
460 h.flash(_('Pull request reviewers updated.'), category='success')
461 h.flash(_('Pull request reviewers updated.'), category='success')
461 Session().commit()
462 Session().commit()
462
463
@@ -476,7 +477,7 b' class PullrequestsController(BaseRepoCon'
476
477
477 # only owner can delete it !
478 # only owner can delete it !
478 if allowed_to_delete:
479 if allowed_to_delete:
479 PullRequestModel().delete(pull_request)
480 PullRequestModel().delete(pull_request, c.rhodecode_user)
480 Session().commit()
481 Session().commit()
481 h.flash(_('Successfully deleted pull request'),
482 h.flash(_('Successfully deleted pull request'),
482 category='success')
483 category='success')
@@ -997,7 +998,7 b' class PullrequestsController(BaseRepoCon'
997 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
998 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
998 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
999 if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner:
999 old_calculated_status = co.pull_request.calculated_review_status()
1000 old_calculated_status = co.pull_request.calculated_review_status()
1000 CommentsModel().delete(comment=co)
1001 CommentsModel().delete(comment=co, user=c.rhodecode_user)
1001 Session().commit()
1002 Session().commit()
1002 calculated_status = co.pull_request.calculated_review_status()
1003 calculated_status = co.pull_request.calculated_review_status()
1003 if old_calculated_status != calculated_status:
1004 if old_calculated_status != calculated_status:
@@ -28,7 +28,7 b' from rhodecode.model.db import User, Use'
28 log = logging.getLogger(__name__)
28 log = logging.getLogger(__name__)
29
29
30 # action as key, and expected action_data as value
30 # action as key, and expected action_data as value
31 ACTIONS = {
31 ACTIONS_V1 = {
32 'user.login.success': {'user_agent': ''},
32 'user.login.success': {'user_agent': ''},
33 'user.login.failure': {'user_agent': ''},
33 'user.login.failure': {'user_agent': ''},
34 'user.logout': {'user_agent': ''},
34 'user.logout': {'user_agent': ''},
@@ -64,11 +64,28 b' ACTIONS = {'
64 'repo.commit.strip': {},
64 'repo.commit.strip': {},
65 'repo.archive.download': {},
65 'repo.archive.download': {},
66
66
67 'repo.pull_request.create': '',
68 'repo.pull_request.edit': '',
69 'repo.pull_request.delete': '',
70 'repo.pull_request.close': '',
71 'repo.pull_request.merge': '',
72 'repo.pull_request.vote': '',
73 'repo.pull_request.comment.create': '',
74 'repo.pull_request.comment.delete': '',
75
76 'repo.pull_request.reviewer.add': '',
77 'repo.pull_request.reviewer.delete': '',
78
79 'repo.commit.comment.create': '',
80 'repo.commit.comment.delete': '',
81 'repo.commit.vote': '',
82
67 'repo_group.create': {'data': {}},
83 'repo_group.create': {'data': {}},
68 'repo_group.edit': {'old_data': {}},
84 'repo_group.edit': {'old_data': {}},
69 'repo_group.edit.permissions': {},
85 'repo_group.edit.permissions': {},
70 'repo_group.delete': {'old_data': {}},
86 'repo_group.delete': {'old_data': {}},
71 }
87 }
88 ACTIONS = ACTIONS_V1
72
89
73 SOURCE_WEB = 'source_web'
90 SOURCE_WEB = 'source_web'
74 SOURCE_API = 'source_api'
91 SOURCE_API = 'source_api'
@@ -139,68 +139,6 b' def get_user_group_slug(request):'
139 return _group
139 return _group
140
140
141
141
142 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
143 """
144 Action logger for various actions made by users
145
146 :param user: user that made this action, can be a unique username string or
147 object containing user_id attribute
148 :param action: action to log, should be on of predefined unique actions for
149 easy translations
150 :param repo: string name of repository or object containing repo_id,
151 that action was made on
152 :param ipaddr: optional ip address from what the action was made
153 :param sa: optional sqlalchemy session
154
155 """
156
157 if not sa:
158 sa = meta.Session()
159 # if we don't get explicit IP address try to get one from registered user
160 # in tmpl context var
161 if not ipaddr:
162 ipaddr = getattr(get_current_rhodecode_user(), 'ip_addr', '')
163
164 try:
165 if getattr(user, 'user_id', None):
166 user_obj = User.get(user.user_id)
167 elif isinstance(user, basestring):
168 user_obj = User.get_by_username(user)
169 else:
170 raise Exception('You have to provide a user object or a username')
171
172 if getattr(repo, 'repo_id', None):
173 repo_obj = Repository.get(repo.repo_id)
174 repo_name = repo_obj.repo_name
175 elif isinstance(repo, basestring):
176 repo_name = repo.lstrip('/')
177 repo_obj = Repository.get_by_repo_name(repo_name)
178 else:
179 repo_obj = None
180 repo_name = ''
181
182 user_log = UserLog()
183 user_log.user_id = user_obj.user_id
184 user_log.username = user_obj.username
185 action = safe_unicode(action)
186 user_log.action = action[:1200000]
187
188 user_log.repository = repo_obj
189 user_log.repository_name = repo_name
190
191 user_log.action_date = datetime.datetime.now()
192 user_log.user_ip = ipaddr
193 sa.add(user_log)
194
195 log.info('Logging action:`%s` on repo:`%s` by user:%s ip:%s',
196 action, safe_unicode(repo), user_obj, ipaddr)
197 if commit:
198 sa.commit()
199 except Exception:
200 log.error(traceback.format_exc())
201 raise
202
203
204 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
142 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
205 """
143 """
206 Scans given path for repos and return (name,(type,path)) tuple
144 Scans given path for repos and return (name,(type,path)) tuple
@@ -34,8 +34,8 b' from sqlalchemy.sql.expression import nu'
34 from sqlalchemy.sql.functions import coalesce
34 from sqlalchemy.sql.functions import coalesce
35
35
36 from rhodecode.lib import helpers as h, diffs
36 from rhodecode.lib import helpers as h, diffs
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.channelstream import channelstream_request
38 from rhodecode.lib.utils import action_logger
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
39 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
40 from rhodecode.model import BaseModel
40 from rhodecode.model import BaseModel
41 from rhodecode.model.db import (
41 from rhodecode.model.db import (
@@ -163,6 +163,13 b' class CommentsModel(BaseModel):'
163
163
164 return todos
164 return todos
165
165
166 def _log_audit_action(self, action, action_data, user, comment):
167 audit_logger.store(
168 action=action,
169 action_data=action_data,
170 user=user,
171 repo=comment.repo)
172
166 def create(self, text, repo, user, commit_id=None, pull_request=None,
173 def create(self, text, repo, user, commit_id=None, pull_request=None,
167 f_path=None, line_no=None, status_change=None,
174 f_path=None, line_no=None, status_change=None,
168 status_change_type=None, comment_type=None,
175 status_change_type=None, comment_type=None,
@@ -337,13 +344,15 b' class CommentsModel(BaseModel):'
337 email_kwargs=kwargs,
344 email_kwargs=kwargs,
338 )
345 )
339
346
340 action = (
347 Session().flush()
341 'user_commented_pull_request:{}'.format(
348 if comment.pull_request:
342 comment.pull_request.pull_request_id)
349 action = 'repo.pull_request.comment.create'
343 if comment.pull_request
350 else:
344 else 'user_commented_revision:{}'.format(comment.revision)
351 action = 'repo.commit.comment.create'
345 )
352
346 action_logger(user, action, comment.repo)
353 comment_data = comment.get_api_data()
354 self._log_audit_action(
355 action, {'data': comment_data}, user, comment)
347
356
348 registry = get_current_registry()
357 registry = get_current_registry()
349 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
358 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
@@ -385,15 +394,22 b' class CommentsModel(BaseModel):'
385
394
386 return comment
395 return comment
387
396
388 def delete(self, comment):
397 def delete(self, comment, user):
389 """
398 """
390 Deletes given comment
399 Deletes given comment
391
392 :param comment_id:
393 """
400 """
394 comment = self.__get_commit_comment(comment)
401 comment = self.__get_commit_comment(comment)
402 old_data = comment.get_api_data()
395 Session().delete(comment)
403 Session().delete(comment)
396
404
405 if comment.pull_request:
406 action = 'repo.pull_request.comment.delete'
407 else:
408 action = 'repo.commit.comment.delete'
409
410 self._log_audit_action(
411 action, {'old_data': old_data}, user, comment)
412
397 return comment
413 return comment
398
414
399 def get_all_comments(self, repo_id, revision=None, pull_request=None):
415 def get_all_comments(self, repo_id, revision=None, pull_request=None):
@@ -3122,6 +3122,25 b' class ChangesetComment(Base, BaseModel):'
3122 else:
3122 else:
3123 return '<DB:Comment at %#x>' % id(self)
3123 return '<DB:Comment at %#x>' % id(self)
3124
3124
3125 def get_api_data(self):
3126 comment = self
3127 data = {
3128 'comment_id': comment.comment_id,
3129 'comment_type': comment.comment_type,
3130 'comment_text': comment.text,
3131 'comment_status': comment.status_change,
3132 'comment_f_path': comment.f_path,
3133 'comment_lineno': comment.line_no,
3134 'comment_author': comment.author,
3135 'comment_created_on': comment.created_on
3136 }
3137 return data
3138
3139 def __json__(self):
3140 data = dict()
3141 data.update(self.get_api_data())
3142 return data
3143
3125
3144
3126 class ChangesetStatus(Base, BaseModel):
3145 class ChangesetStatus(Base, BaseModel):
3127 __tablename__ = 'changeset_statuses'
3146 __tablename__ = 'changeset_statuses'
@@ -3173,6 +3192,19 b' class ChangesetStatus(Base, BaseModel):'
3173 def status_lbl(self):
3192 def status_lbl(self):
3174 return ChangesetStatus.get_status_lbl(self.status)
3193 return ChangesetStatus.get_status_lbl(self.status)
3175
3194
3195 def get_api_data(self):
3196 status = self
3197 data = {
3198 'status_id': status.changeset_status_id,
3199 'status': status.status,
3200 }
3201 return data
3202
3203 def __json__(self):
3204 data = dict()
3205 data.update(self.get_api_data())
3206 return data
3207
3176
3208
3177 class _PullRequestBase(BaseModel):
3209 class _PullRequestBase(BaseModel):
3178 """
3210 """
@@ -3304,15 +3336,19 b' class _PullRequestBase(BaseModel):'
3304 else:
3336 else:
3305 return None
3337 return None
3306
3338
3307 def get_api_data(self):
3339 def get_api_data(self, with_merge_state=True):
3308 from pylons import url
3309 from rhodecode.model.pull_request import PullRequestModel
3340 from rhodecode.model.pull_request import PullRequestModel
3341
3310 pull_request = self
3342 pull_request = self
3311 merge_status = PullRequestModel().merge_status(pull_request)
3343 if with_merge_state:
3312
3344 merge_status = PullRequestModel().merge_status(pull_request)
3313 pull_request_url = url(
3345 merge_state = {
3314 'pullrequest_show', repo_name=self.target_repo.repo_name,
3346 'status': merge_status[0],
3315 pull_request_id=self.pull_request_id, qualified=True)
3347 'message': safe_unicode(merge_status[1]),
3348 }
3349 else:
3350 merge_state = {'status': 'not_available',
3351 'message': 'not_available'}
3316
3352
3317 merge_data = {
3353 merge_data = {
3318 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3354 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
@@ -3323,7 +3359,7 b' class _PullRequestBase(BaseModel):'
3323
3359
3324 data = {
3360 data = {
3325 'pull_request_id': pull_request.pull_request_id,
3361 'pull_request_id': pull_request.pull_request_id,
3326 'url': pull_request_url,
3362 'url': PullRequestModel().get_url(pull_request),
3327 'title': pull_request.title,
3363 'title': pull_request.title,
3328 'description': pull_request.description,
3364 'description': pull_request.description,
3329 'status': pull_request.status,
3365 'status': pull_request.status,
@@ -3331,10 +3367,7 b' class _PullRequestBase(BaseModel):'
3331 'updated_on': pull_request.updated_on,
3367 'updated_on': pull_request.updated_on,
3332 'commit_ids': pull_request.revisions,
3368 'commit_ids': pull_request.revisions,
3333 'review_status': pull_request.calculated_review_status(),
3369 'review_status': pull_request.calculated_review_status(),
3334 'mergeable': {
3370 'mergeable': merge_state,
3335 'status': merge_status[0],
3336 'message': unicode(merge_status[1]),
3337 },
3338 'source': {
3371 'source': {
3339 'clone_url': pull_request.source_repo.clone_url(),
3372 'clone_url': pull_request.source_repo.clone_url(),
3340 'repository': pull_request.source_repo.repo_name,
3373 'repository': pull_request.source_repo.repo_name,
@@ -3389,7 +3422,8 b' class PullRequest(Base, _PullRequestBase'
3389
3422
3390 reviewers = relationship('PullRequestReviewers',
3423 reviewers = relationship('PullRequestReviewers',
3391 cascade="all, delete, delete-orphan")
3424 cascade="all, delete, delete-orphan")
3392 statuses = relationship('ChangesetStatus')
3425 statuses = relationship('ChangesetStatus',
3426 cascade="all, delete, delete-orphan")
3393 comments = relationship('ChangesetComment',
3427 comments = relationship('ChangesetComment',
3394 cascade="all, delete, delete-orphan")
3428 cascade="all, delete, delete-orphan")
3395 versions = relationship('PullRequestVersion',
3429 versions = relationship('PullRequestVersion',
@@ -36,11 +36,11 b' from sqlalchemy import or_'
36
36
37 from rhodecode import events
37 from rhodecode import events
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
38 from rhodecode.lib import helpers as h, hooks_utils, diffs
39 from rhodecode.lib import audit_logger
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
41 from rhodecode.lib.markup_renderer import (
42 from rhodecode.lib.markup_renderer import (
42 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
43 from rhodecode.lib.utils import action_logger
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
44 from rhodecode.lib.utils2 import safe_unicode, safe_str, md5_safe
45 from rhodecode.lib.vcs.backends.base import (
45 from rhodecode.lib.vcs.backends.base import (
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
46 Reference, MergeResponse, MergeFailureReason, UpdateFailureReason)
@@ -470,6 +470,11 b' class PullRequestModel(BaseModel):'
470 self._trigger_pull_request_hook(
470 self._trigger_pull_request_hook(
471 pull_request, created_by_user, 'create')
471 pull_request, created_by_user, 'create')
472
472
473 creation_data = pull_request.get_api_data(with_merge_state=False)
474 self._log_audit_action(
475 'repo.pull_request.create', {'data': creation_data},
476 created_by_user, pull_request)
477
473 return pull_request
478 return pull_request
474
479
475 def _trigger_pull_request_hook(self, pull_request, user, action):
480 def _trigger_pull_request_hook(self, pull_request, user, action):
@@ -520,7 +525,12 b' class PullRequestModel(BaseModel):'
520 log.debug(
525 log.debug(
521 "Merge was successful, updating the pull request comments.")
526 "Merge was successful, updating the pull request comments.")
522 self._comment_and_close_pr(pull_request, user, merge_state)
527 self._comment_and_close_pr(pull_request, user, merge_state)
523 self._log_action('user_merged_pull_request', user, pull_request)
528
529 self._log_audit_action(
530 'repo.pull_request.merge',
531 {'merge_state': merge_state.__dict__},
532 user, pull_request)
533
524 else:
534 else:
525 log.warn("Merge failed, not updating the pull request.")
535 log.warn("Merge failed, not updating the pull request.")
526 return merge_state
536 return merge_state
@@ -899,8 +909,9 b' class PullRequestModel(BaseModel):'
899 renderer = RstTemplateRenderer()
909 renderer = RstTemplateRenderer()
900 return renderer.render('pull_request_update.mako', **params)
910 return renderer.render('pull_request_update.mako', **params)
901
911
902 def edit(self, pull_request, title, description):
912 def edit(self, pull_request, title, description, user):
903 pull_request = self.__get_pull_request(pull_request)
913 pull_request = self.__get_pull_request(pull_request)
914 old_data = pull_request.get_api_data(with_merge_state=False)
904 if pull_request.is_closed():
915 if pull_request.is_closed():
905 raise ValueError('This pull request is closed')
916 raise ValueError('This pull request is closed')
906 if title:
917 if title:
@@ -908,8 +919,11 b' class PullRequestModel(BaseModel):'
908 pull_request.description = description
919 pull_request.description = description
909 pull_request.updated_on = datetime.datetime.now()
920 pull_request.updated_on = datetime.datetime.now()
910 Session().add(pull_request)
921 Session().add(pull_request)
922 self._log_audit_action(
923 'repo.pull_request.edit', {'old_data': old_data},
924 user, pull_request)
911
925
912 def update_reviewers(self, pull_request, reviewer_data):
926 def update_reviewers(self, pull_request, reviewer_data, user):
913 """
927 """
914 Update the reviewers in the pull request
928 Update the reviewers in the pull request
915
929
@@ -946,8 +960,11 b' class PullRequestModel(BaseModel):'
946 reviewer.pull_request = pull_request
960 reviewer.pull_request = pull_request
947 reviewer.reasons = reviewers[uid]['reasons']
961 reviewer.reasons = reviewers[uid]['reasons']
948 # NOTE(marcink): mandatory shouldn't be changed now
962 # NOTE(marcink): mandatory shouldn't be changed now
949 #reviewer.mandatory = reviewers[uid]['reasons']
963 # reviewer.mandatory = reviewers[uid]['reasons']
950 Session().add(reviewer)
964 Session().add(reviewer)
965 self._log_audit_action(
966 'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
967 user, pull_request)
951
968
952 for uid in ids_to_remove:
969 for uid in ids_to_remove:
953 changed = True
970 changed = True
@@ -958,7 +975,11 b' class PullRequestModel(BaseModel):'
958 # use .all() in case we accidentally added the same person twice
975 # use .all() in case we accidentally added the same person twice
959 # this CAN happen due to the lack of DB checks
976 # this CAN happen due to the lack of DB checks
960 for obj in reviewers:
977 for obj in reviewers:
978 old_data = obj.get_dict()
961 Session().delete(obj)
979 Session().delete(obj)
980 self._log_audit_action(
981 'repo.pull_request.reviewer.delete',
982 {'old_data': old_data}, user, pull_request)
962
983
963 if changed:
984 if changed:
964 pull_request.updated_on = datetime.datetime.now()
985 pull_request.updated_on = datetime.datetime.now()
@@ -1054,9 +1075,13 b' class PullRequestModel(BaseModel):'
1054 email_kwargs=kwargs,
1075 email_kwargs=kwargs,
1055 )
1076 )
1056
1077
1057 def delete(self, pull_request):
1078 def delete(self, pull_request, user):
1058 pull_request = self.__get_pull_request(pull_request)
1079 pull_request = self.__get_pull_request(pull_request)
1080 old_data = pull_request.get_api_data(with_merge_state=False)
1059 self._cleanup_merge_workspace(pull_request)
1081 self._cleanup_merge_workspace(pull_request)
1082 self._log_audit_action(
1083 'repo.pull_request.delete', {'old_data': old_data},
1084 user, pull_request)
1060 Session().delete(pull_request)
1085 Session().delete(pull_request)
1061
1086
1062 def close_pull_request(self, pull_request, user):
1087 def close_pull_request(self, pull_request, user):
@@ -1067,7 +1092,8 b' class PullRequestModel(BaseModel):'
1067 Session().add(pull_request)
1092 Session().add(pull_request)
1068 self._trigger_pull_request_hook(
1093 self._trigger_pull_request_hook(
1069 pull_request, pull_request.author, 'close')
1094 pull_request, pull_request.author, 'close')
1070 self._log_action('user_closed_pull_request', user, pull_request)
1095 self._log_audit_action(
1096 'repo.pull_request.close', {}, user, pull_request)
1071
1097
1072 def close_pull_request_with_comment(
1098 def close_pull_request_with_comment(
1073 self, pull_request, user, repo, message=None):
1099 self, pull_request, user, repo, message=None):
@@ -1402,12 +1428,12 b' class PullRequestModel(BaseModel):'
1402 settings = settings_model.get_general_settings()
1428 settings = settings_model.get_general_settings()
1403 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1429 return settings.get('rhodecode_hg_use_rebase_for_merging', False)
1404
1430
1405 def _log_action(self, action, user, pull_request):
1431 def _log_audit_action(self, action, action_data, user, pull_request):
1406 action_logger(
1432 audit_logger.store(
1407 user,
1433 action=action,
1408 '{action}:{pr_id}'.format(
1434 action_data=action_data,
1409 action=action, pr_id=pull_request.pull_request_id),
1435 user=user,
1410 pull_request.target_repo)
1436 repo=pull_request.target_repo)
1411
1437
1412 def get_reviewer_functions(self):
1438 def get_reviewer_functions(self):
1413 """
1439 """
@@ -199,6 +199,9 b' class TestAdminPermissionsController(Tes'
199 url('edit_user_ips', user_id=default_user_id),
199 url('edit_user_ips', user_id=default_user_id),
200 params={'_method': 'delete', 'del_ip_id': del_ip_id,
200 params={'_method': 'delete', 'del_ip_id': del_ip_id,
201 'csrf_token': self.csrf_token})
201 'csrf_token': self.csrf_token})
202
203 assert_session_flash(response, 'Removed ip address from user whitelist')
204
202 clear_all_caches()
205 clear_all_caches()
203 response = self.app.get(url('admin_permissions_ips'))
206 response = self.app.get(url('admin_permissions_ips'))
204 response.mustcontain('All IP addresses are allowed')
207 response.mustcontain('All IP addresses are allowed')
@@ -27,7 +27,7 b' from rhodecode.lib.vcs.nodes import File'
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.model.changeset_status import ChangesetStatusModel
28 from rhodecode.model.changeset_status import ChangesetStatusModel
29 from rhodecode.model.db import (
29 from rhodecode.model.db import (
30 PullRequest, ChangesetStatus, UserLog, Notification)
30 PullRequest, ChangesetStatus, UserLog, Notification, ChangesetComment)
31 from rhodecode.model.meta import Session
31 from rhodecode.model.meta import Session
32 from rhodecode.model.pull_request import PullRequestModel
32 from rhodecode.model.pull_request import PullRequestModel
33 from rhodecode.model.user import UserModel
33 from rhodecode.model.user import UserModel
@@ -256,29 +256,32 b' class TestPullrequestsController(object)'
256 'csrf_token': csrf_token},
256 'csrf_token': csrf_token},
257 extra_environ=xhr_header,)
257 extra_environ=xhr_header,)
258
258
259 action = 'user_closed_pull_request:%d' % pull_request_id
260 journal = UserLog.query()\
259 journal = UserLog.query()\
261 .filter(UserLog.user_id == author)\
260 .filter(UserLog.user_id == author)\
262 .filter(UserLog.repository_id == repo)\
261 .filter(UserLog.repository_id == repo) \
263 .filter(UserLog.action == action)\
262 .order_by('user_log_id') \
264 .all()
263 .all()
265 assert len(journal) == 1
264 assert journal[-1].action == 'repo.pull_request.close'
266
265
267 pull_request = PullRequest.get(pull_request_id)
266 pull_request = PullRequest.get(pull_request_id)
268 assert pull_request.is_closed()
267 assert pull_request.is_closed()
269
268
270 # check only the latest status, not the review status
271 status = ChangesetStatusModel().get_status(
269 status = ChangesetStatusModel().get_status(
272 pull_request.source_repo, pull_request=pull_request)
270 pull_request.source_repo, pull_request=pull_request)
273 assert status == ChangesetStatus.STATUS_APPROVED
271 assert status == ChangesetStatus.STATUS_APPROVED
274 assert pull_request.comments[-1].text == 'Closing a PR'
272 comments = ChangesetComment().query() \
273 .filter(ChangesetComment.pull_request == pull_request) \
274 .order_by(ChangesetComment.comment_id.asc())\
275 .all()
276 assert comments[-1].text == 'Closing a PR'
275
277
276 def test_comment_force_close_pull_request_rejected(
278 def test_comment_force_close_pull_request_rejected(
277 self, pr_util, csrf_token, xhr_header):
279 self, pr_util, csrf_token, xhr_header):
278 pull_request = pr_util.create_pull_request()
280 pull_request = pr_util.create_pull_request()
279 pull_request_id = pull_request.pull_request_id
281 pull_request_id = pull_request.pull_request_id
280 PullRequestModel().update_reviewers(
282 PullRequestModel().update_reviewers(
281 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)])
283 pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)],
284 pull_request.author)
282 author = pull_request.user_id
285 author = pull_request.user_id
283 repo = pull_request.target_repo.repo_id
286 repo = pull_request.target_repo.repo_id
284
287
@@ -294,12 +297,11 b' class TestPullrequestsController(object)'
294
297
295 pull_request = PullRequest.get(pull_request_id)
298 pull_request = PullRequest.get(pull_request_id)
296
299
297 action = 'user_closed_pull_request:%d' % pull_request_id
300 journal = UserLog.query()\
298 journal = UserLog.query().filter(
301 .filter(UserLog.user_id == author, UserLog.repository_id == repo) \
299 UserLog.user_id == author,
302 .order_by('user_log_id') \
300 UserLog.repository_id == repo,
303 .all()
301 UserLog.action == action).all()
304 assert journal[-1].action == 'repo.pull_request.close'
302 assert len(journal) == 1
303
305
304 # check only the latest status, not the review status
306 # check only the latest status, not the review status
305 status = ChangesetStatusModel().get_status(
307 status = ChangesetStatusModel().get_status(
@@ -449,7 +451,8 b' class TestPullrequestsController(object)'
449
451
450 # Change reviewers and check that a notification was made
452 # Change reviewers and check that a notification was made
451 PullRequestModel().update_reviewers(
453 PullRequestModel().update_reviewers(
452 pull_request.pull_request_id, [(1, [], False)])
454 pull_request.pull_request_id, [(1, [], False)],
455 pull_request.author)
453 assert len(notifications.all()) == 2
456 assert len(notifications.all()) == 2
454
457
455 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
458 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
@@ -541,25 +544,20 b' class TestPullrequestsController(object)'
541 pull_request, ChangesetStatus.STATUS_APPROVED)
544 pull_request, ChangesetStatus.STATUS_APPROVED)
542
545
543 # Check the relevant log entries were added
546 # Check the relevant log entries were added
544 user_logs = UserLog.query() \
547 user_logs = UserLog.query().order_by('-user_log_id').limit(3)
545 .filter(UserLog.version == UserLog.VERSION_1) \
546 .order_by('-user_log_id').limit(3)
547 actions = [log.action for log in user_logs]
548 actions = [log.action for log in user_logs]
548 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
549 pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request)
549 expected_actions = [
550 expected_actions = [
550 u'user_closed_pull_request:%d' % pull_request_id,
551 u'repo.pull_request.close',
551 u'user_merged_pull_request:%d' % pull_request_id,
552 u'repo.pull_request.merge',
552 # The action below reflect that the post push actions were executed
553 u'repo.pull_request.comment.create'
553 u'user_commented_pull_request:%d' % pull_request_id,
554 ]
554 ]
555 assert actions == expected_actions
555 assert actions == expected_actions
556
556
557 user_logs = UserLog.query() \
557 user_logs = UserLog.query().order_by('-user_log_id').limit(4)
558 .filter(UserLog.version == UserLog.VERSION_2) \
558 actions = [log for log in user_logs]
559 .order_by('-user_log_id').limit(1)
559 assert actions[-1].action == 'user.push'
560 actions = [log.action for log in user_logs]
560 assert actions[-1].action_data['commit_ids'] == pr_commit_ids
561 assert actions == ['user.push']
562 assert user_logs[0].action_data['commit_ids'] == pr_commit_ids
563
561
564 # Check post_push rcextension was really executed
562 # Check post_push rcextension was really executed
565 push_calls = rhodecode.EXTENSIONS.calls['post_push']
563 push_calls = rhodecode.EXTENSIONS.calls['post_push']
@@ -50,7 +50,9 b' class TestPullRequestModel(object):'
50 A pull request combined with multiples patches.
50 A pull request combined with multiples patches.
51 """
51 """
52 BackendClass = get_backend(backend.alias)
52 BackendClass = get_backend(backend.alias)
53 self.merge_patcher = mock.patch.object(BackendClass, 'merge')
53 self.merge_patcher = mock.patch.object(
54 BackendClass, 'merge', return_value=MergeResponse(
55 False, False, None, MergeFailureReason.UNKNOWN))
54 self.workspace_remove_patcher = mock.patch.object(
56 self.workspace_remove_patcher = mock.patch.object(
55 BackendClass, 'cleanup_merge_workspace')
57 BackendClass, 'cleanup_merge_workspace')
56
58
@@ -117,7 +119,8 b' class TestPullRequestModel(object):'
117
119
118 def test_get_awaiting_my_review(self, pull_request):
120 def test_get_awaiting_my_review(self, pull_request):
119 PullRequestModel().update_reviewers(
121 PullRequestModel().update_reviewers(
120 pull_request, [(pull_request.author, ['author'], False)])
122 pull_request, [(pull_request.author, ['author'], False)],
123 pull_request.author)
121 prs = PullRequestModel().get_awaiting_my_review(
124 prs = PullRequestModel().get_awaiting_my_review(
122 pull_request.target_repo, user_id=pull_request.author.user_id)
125 pull_request.target_repo, user_id=pull_request.author.user_id)
123 assert isinstance(prs, list)
126 assert isinstance(prs, list)
@@ -125,13 +128,14 b' class TestPullRequestModel(object):'
125
128
126 def test_count_awaiting_my_review(self, pull_request):
129 def test_count_awaiting_my_review(self, pull_request):
127 PullRequestModel().update_reviewers(
130 PullRequestModel().update_reviewers(
128 pull_request, [(pull_request.author, ['author'], False)])
131 pull_request, [(pull_request.author, ['author'], False)],
132 pull_request.author)
129 pr_count = PullRequestModel().count_awaiting_my_review(
133 pr_count = PullRequestModel().count_awaiting_my_review(
130 pull_request.target_repo, user_id=pull_request.author.user_id)
134 pull_request.target_repo, user_id=pull_request.author.user_id)
131 assert pr_count == 1
135 assert pr_count == 1
132
136
133 def test_delete_calls_cleanup_merge(self, pull_request):
137 def test_delete_calls_cleanup_merge(self, pull_request):
134 PullRequestModel().delete(pull_request)
138 PullRequestModel().delete(pull_request, pull_request.author)
135
139
136 self.workspace_remove_mock.assert_called_once_with(
140 self.workspace_remove_mock.assert_called_once_with(
137 self.workspace_id)
141 self.workspace_id)
@@ -892,7 +892,7 b' class RepoServer(object):'
892
892
893
893
894 @pytest.fixture
894 @pytest.fixture
895 def pr_util(backend, request):
895 def pr_util(backend, request, config_stub):
896 """
896 """
897 Utility for tests of models and for functional tests around pull requests.
897 Utility for tests of models and for functional tests around pull requests.
898
898
@@ -1085,7 +1085,7 b' class PRTestUtility(object):'
1085 # request will already be deleted.
1085 # request will already be deleted.
1086 pull_request = PullRequest().get(self.pull_request_id)
1086 pull_request = PullRequest().get(self.pull_request_id)
1087 if pull_request:
1087 if pull_request:
1088 PullRequestModel().delete(pull_request)
1088 PullRequestModel().delete(pull_request, pull_request.author)
1089 Session().commit()
1089 Session().commit()
1090
1090
1091 if self.notification_patcher:
1091 if self.notification_patcher:
@@ -1648,14 +1648,6 b' def no_notifications(request):'
1648 request.addfinalizer(notification_patcher.stop)
1648 request.addfinalizer(notification_patcher.stop)
1649
1649
1650
1650
1651 @pytest.fixture
1652 def silence_action_logger(request):
1653 notification_patcher = mock.patch(
1654 'rhodecode.lib.utils.action_logger')
1655 notification_patcher.start()
1656 request.addfinalizer(notification_patcher.stop)
1657
1658
1659 @pytest.fixture(scope='session')
1651 @pytest.fixture(scope='session')
1660 def repeat(request):
1652 def repeat(request):
1661 """
1653 """
General Comments 0
You need to be logged in to leave comments. Login now