##// END OF EJS Templates
events: add event system for RepoEvents
dan -
r375:41f1288c default
parent child Browse files
Show More
@@ -0,0 +1,49 b''
1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 from pyramid.threadlocal import get_current_registry
20
21
22 class RhodecodeEvent(object):
23 """
24 Base event class for all Rhodecode events
25 """
26
27
28 def trigger(event):
29 """
30 Helper method to send an event. This wraps the pyramid logic to send an
31 event.
32 """
33 # For the first step we are using pyramids thread locals here. If the
34 # event mechanism works out as a good solution we should think about
35 # passing the registry as an argument to get rid of it.
36 registry = get_current_registry()
37 registry.notify(event)
38
39
40 from rhodecode.events.user import (
41 UserPreCreate, UserPreUpdate, UserRegistered
42 )
43
44 from rhodecode.events.repo import (
45 RepoPreCreateEvent, RepoCreatedEvent,
46 RepoPreDeleteEvent, RepoDeletedEvent,
47 RepoPrePushEvent, RepoPushEvent,
48 RepoPrePullEvent, RepoPullEvent,
49 ) No newline at end of file
@@ -0,0 +1,115 b''
1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 from rhodecode.model.db import Repository, Session
20 from rhodecode.events import RhodecodeEvent
21
22
23 class RepoEvent(RhodecodeEvent):
24 """
25 Base class for events acting on a repository.
26
27 :param repo: a :class:`Repository` instance
28 """
29 def __init__(self, repo):
30 self.repo = repo
31
32
33 class RepoPreCreateEvent(RepoEvent):
34 """
35 An instance of this class is emitted as an :term:`event` before a repo is
36 created.
37
38 :param repo_name: repository name
39 """
40 name = 'repo-pre-create'
41
42
43 class RepoCreatedEvent(RepoEvent):
44 """
45 An instance of this class is emitted as an :term:`event` whenever a repo is
46 created.
47 """
48 name = 'repo-created'
49
50
51 class RepoPreDeleteEvent(RepoEvent):
52 """
53 An instance of this class is emitted as an :term:`event` whenever a repo is
54 created.
55 """
56 name = 'repo-pre-delete'
57
58
59 class RepoDeletedEvent(RepoEvent):
60 """
61 An instance of this class is emitted as an :term:`event` whenever a repo is
62 created.
63 """
64 name = 'repo-deleted'
65
66
67 class RepoVCSEvent(RepoEvent):
68 """
69 Base class for events triggered by the VCS
70 """
71 def __init__(self, repo_name, extras):
72 self.repo = Repository.get_by_repo_name(repo_name)
73 if not self.repo:
74 raise Exception('repo by this name %s does not exist' % repo_name)
75 self.extras = extras
76 super(RepoVCSEvent, self).__init__(self.repo)
77
78
79 class RepoPrePullEvent(RepoVCSEvent):
80 """
81 An instance of this class is emitted as an :term:`event` before commits
82 are pulled from a repo.
83 """
84 name = 'repo-pre-pull'
85
86
87 class RepoPullEvent(RepoVCSEvent):
88 """
89 An instance of this class is emitted as an :term:`event` after commits
90 are pulled from a repo.
91 """
92 name = 'repo-pull'
93
94
95 class RepoPrePushEvent(RepoVCSEvent):
96 """
97 An instance of this class is emitted as an :term:`event` before commits
98 are pushed to a repo.
99 """
100 name = 'repo-pre-push'
101
102
103 class RepoPushEvent(RepoVCSEvent):
104 """
105 An instance of this class is emitted as an :term:`event` after commits
106 are pushed to a repo.
107
108 :param extras: (optional) dict of data from proxied VCS actions
109 """
110 name = 'repo-push'
111
112 def __init__(self, repo_name, pushed_commit_ids, extras):
113 super(RepoPushEvent, self).__init__(repo_name, extras)
114 self.pushed_commit_ids = pushed_commit_ids
115
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -0,0 +1,38 b''
1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 #
3 # This program is free software: you can redistribute it and/or modify
4 # it under the terms of the GNU Affero General Public License, version 3
5 # (only), as published by the Free Software Foundation.
6 #
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
11 #
12 # You should have received a copy of the GNU Affero General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 #
15 # This program is dual-licensed. If you wish to learn more about the
16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18
19 import mock
20 import decorator
21
22
23 def assert_fires_events(*expected_events):
24 """ Testing decorator to check if the function fires events in order """
25 def deco(func):
26 def wrapper(func, *args, **kwargs):
27 with mock.patch('rhodecode.events.trigger') as mock_trigger:
28 result = func(*args, **kwargs)
29
30 captured_events = []
31 for call in mock_trigger.call_args_list:
32 event = call[0][0]
33 captured_events.append(type(event))
34
35 assert set(captured_events) == set(expected_events)
36 return result
37 return decorator.decorator(wrapper, func)
38 return deco No newline at end of file
@@ -0,0 +1,68 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.tests.events.conftest import assert_fires_events
24
25 from rhodecode.lib import hooks_base, utils2
26 from rhodecode.model.repo import RepoModel
27 from rhodecode.events.repo import (
28 RepoPrePullEvent, RepoPullEvent,
29 RepoPrePushEvent, RepoPushEvent,
30 RepoPreCreateEvent, RepoCreatedEvent,
31 RepoPreDeleteEvent, RepoDeletedEvent,
32 )
33
34
35 @pytest.fixture
36 def scm_extras(user_regular, repo_stub):
37 extras = utils2.AttributeDict({
38 'ip': '127.0.0.1',
39 'username': user_regular.username,
40 'action': '',
41 'repository': repo_stub.repo_name,
42 'scm': repo_stub.scm_instance().alias,
43 'config': '',
44 'server_url': 'http://example.com',
45 'make_lock': None,
46 'locked_by': [None],
47 'commit_ids': ['a' * 40] * 3,
48 })
49 return extras
50
51
52 @assert_fires_events(
53 RepoPreCreateEvent, RepoCreatedEvent, RepoPreDeleteEvent, RepoDeletedEvent)
54 def test_create_delete_repo_fires_events(backend):
55 repo = backend.create_repo()
56 RepoModel().delete(repo)
57
58
59 @assert_fires_events(RepoPrePushEvent, RepoPushEvent)
60 def test_pull_fires_events(scm_extras):
61 hooks_base.pre_push(scm_extras)
62 hooks_base.post_push(scm_extras)
63
64
65 @assert_fires_events(RepoPrePullEvent, RepoPullEvent)
66 def test_push_fires_events(scm_extras):
67 hooks_base.pre_pull(scm_extras)
68 hooks_base.post_pull(scm_extras)
1 NO CONTENT: file renamed from rhodecode/interfaces.py to rhodecode/events/interfaces.py
@@ -1,53 +1,54 b''
1 1 # Copyright (C) 2016-2016 RhodeCode GmbH
2 2 #
3 3 # This program is free software: you can redistribute it and/or modify
4 4 # it under the terms of the GNU Affero General Public License, version 3
5 5 # (only), as published by the Free Software Foundation.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU Affero General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 #
15 15 # This program is dual-licensed. If you wish to learn more about the
16 16 # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 from zope.interface import implementer
20 from rhodecode.interfaces import (
20 from rhodecode.events import RhodecodeEvent
21 from rhodecode.events.interfaces import (
21 22 IUserRegistered, IUserPreCreate, IUserPreUpdate)
22 23
23 24
24 25 @implementer(IUserRegistered)
25 class UserRegistered(object):
26 class UserRegistered(RhodecodeEvent):
26 27 """
27 28 An instance of this class is emitted as an :term:`event` whenever a user
28 29 account is registered.
29 30 """
30 31 def __init__(self, user, session):
31 32 self.user = user
32 33 self.session = session
33 34
34 35
35 36 @implementer(IUserPreCreate)
36 class UserPreCreate(object):
37 class UserPreCreate(RhodecodeEvent):
37 38 """
38 39 An instance of this class is emitted as an :term:`event` before a new user
39 40 object is created.
40 41 """
41 42 def __init__(self, user_data):
42 43 self.user_data = user_data
43 44
44 45
45 46 @implementer(IUserPreUpdate)
46 class UserPreUpdate(object):
47 class UserPreUpdate(RhodecodeEvent):
47 48 """
48 49 An instance of this class is emitted as an :term:`event` before a user
49 50 object is updated.
50 51 """
51 52 def __init__(self, user, user_data):
52 53 self.user = user
53 54 self.user_data = user_data
@@ -1,366 +1,378 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # 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
29 29 import rhodecode
30 from rhodecode import events
30 31 from rhodecode.lib import helpers as h
31 32 from rhodecode.lib.utils import action_logger
32 33 from rhodecode.lib.utils2 import safe_str
33 34 from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError
34 35 from rhodecode.model.db import Repository, User
35 36
36 37
37 38 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
38 39
39 40
40 41 def _get_scm_size(alias, root_path):
41 42
42 43 if not alias.startswith('.'):
43 44 alias += '.'
44 45
45 46 size_scm, size_root = 0, 0
46 47 for path, unused_dirs, files in os.walk(safe_str(root_path)):
47 48 if path.find(alias) != -1:
48 49 for f in files:
49 50 try:
50 51 size_scm += os.path.getsize(os.path.join(path, f))
51 52 except OSError:
52 53 pass
53 54 else:
54 55 for f in files:
55 56 try:
56 57 size_root += os.path.getsize(os.path.join(path, f))
57 58 except OSError:
58 59 pass
59 60
60 61 size_scm_f = h.format_byte_size_binary(size_scm)
61 62 size_root_f = h.format_byte_size_binary(size_root)
62 63 size_total_f = h.format_byte_size_binary(size_root + size_scm)
63 64
64 65 return size_scm_f, size_root_f, size_total_f
65 66
66 67
67 68 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
68 69 def repo_size(extras):
69 70 """Present size of repository after push."""
70 71 repo = Repository.get_by_repo_name(extras.repository)
71 72 vcs_part = safe_str(u'.%s' % repo.repo_type)
72 73 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
73 74 repo.repo_full_path)
74 75 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
75 76 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
76 77 return HookResponse(0, msg)
77 78
78 79
79 80 def pre_push(extras):
80 81 """
81 82 Hook executed before pushing code.
82 83
83 84 It bans pushing when the repository is locked.
84 85 """
85 86 usr = User.get_by_username(extras.username)
86 87
87 88
88 89 output = ''
89 90 if extras.locked_by[0] and usr.user_id != int(extras.locked_by[0]):
90 91 locked_by = User.get(extras.locked_by[0]).username
91 92 reason = extras.locked_by[2]
92 93 # this exception is interpreted in git/hg middlewares and based
93 94 # on that proper return code is server to client
94 95 _http_ret = HTTPLockedRC(
95 96 _locked_by_explanation(extras.repository, locked_by, reason))
96 97 if str(_http_ret.code).startswith('2'):
97 98 # 2xx Codes don't raise exceptions
98 99 output = _http_ret.title
99 100 else:
100 101 raise _http_ret
101 102
102 103 # Calling hooks after checking the lock, for consistent behavior
103 104 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
104 105
106 events.trigger(events.RepoPrePushEvent(repo_name=extras.repository,
107 extras=extras))
108
105 109 return HookResponse(0, output)
106 110
107 111
108 112 def pre_pull(extras):
109 113 """
110 114 Hook executed before pulling the code.
111 115
112 116 It bans pulling when the repository is locked.
113 117 """
114 118
115 119 output = ''
116 120 if extras.locked_by[0]:
117 121 locked_by = User.get(extras.locked_by[0]).username
118 122 reason = extras.locked_by[2]
119 123 # this exception is interpreted in git/hg middlewares and based
120 124 # on that proper return code is server to client
121 125 _http_ret = HTTPLockedRC(
122 126 _locked_by_explanation(extras.repository, locked_by, reason))
123 127 if str(_http_ret.code).startswith('2'):
124 128 # 2xx Codes don't raise exceptions
125 129 output = _http_ret.title
126 130 else:
127 131 raise _http_ret
128 132
129 133 # Calling hooks after checking the lock, for consistent behavior
130 134 pre_pull_extension(**extras)
135 events.trigger(events.RepoPrePullEvent(repo_name=extras.repository,
136 extras=extras))
131 137
132 138 return HookResponse(0, output)
133 139
134 140
135 141 def post_pull(extras):
136 142 """Hook executed after client pulls the code."""
137 143 user = User.get_by_username(extras.username)
138 144 action = 'pull'
139 145 action_logger(user, action, extras.repository, extras.ip, commit=True)
140 146
147 events.trigger(events.RepoPullEvent(repo_name=extras.repository,
148 extras=extras))
141 149 # extension hook call
142 150 post_pull_extension(**extras)
143 151
144 152 output = ''
145 153 # make lock is a tri state False, True, None. We only make lock on True
146 154 if extras.make_lock is True:
147 155 Repository.lock(Repository.get_by_repo_name(extras.repository),
148 156 user.user_id,
149 157 lock_reason=Repository.LOCK_PULL)
150 158 msg = 'Made lock on repo `%s`' % (extras.repository,)
151 159 output += msg
152 160
153 161 if extras.locked_by[0]:
154 162 locked_by = User.get(extras.locked_by[0]).username
155 163 reason = extras.locked_by[2]
156 164 _http_ret = HTTPLockedRC(
157 165 _locked_by_explanation(extras.repository, locked_by, reason))
158 166 if str(_http_ret.code).startswith('2'):
159 167 # 2xx Codes don't raise exceptions
160 168 output += _http_ret.title
161 169
162 170 return HookResponse(0, output)
163 171
164 172
165 173 def post_push(extras):
166 174 """Hook executed after user pushes to the repository."""
167 175 action_tmpl = extras.action + ':%s'
168 176 commit_ids = extras.commit_ids[:29000]
169 177
170 178 action = action_tmpl % ','.join(commit_ids)
171 179 action_logger(
172 180 extras.username, action, extras.repository, extras.ip, commit=True)
173 181
182 events.trigger(events.RepoPushEvent(repo_name=extras.repository,
183 pushed_commit_ids=commit_ids,
184 extras=extras))
185
174 186 # extension hook call
175 187 post_push_extension(
176 188 repo_store_path=Repository.base_path(),
177 189 pushed_revs=commit_ids,
178 190 **extras)
179 191
180 192 output = ''
181 193 # make lock is a tri state False, True, None. We only release lock on False
182 194 if extras.make_lock is False:
183 195 Repository.unlock(Repository.get_by_repo_name(extras.repository))
184 196 msg = 'Released lock on repo `%s`\n' % extras.repository
185 197 output += msg
186 198
187 199 if extras.locked_by[0]:
188 200 locked_by = User.get(extras.locked_by[0]).username
189 201 reason = extras.locked_by[2]
190 202 _http_ret = HTTPLockedRC(
191 203 _locked_by_explanation(extras.repository, locked_by, reason))
192 204 # TODO: johbo: if not?
193 205 if str(_http_ret.code).startswith('2'):
194 206 # 2xx Codes don't raise exceptions
195 207 output += _http_ret.title
196 208
197 209 output += 'RhodeCode: push completed\n'
198 210
199 211 return HookResponse(0, output)
200 212
201 213
202 214 def _locked_by_explanation(repo_name, user_name, reason):
203 215 message = (
204 216 'Repository `%s` locked by user `%s`. Reason:`%s`'
205 217 % (repo_name, user_name, reason))
206 218 return message
207 219
208 220
209 221 def check_allowed_create_user(user_dict, created_by, **kwargs):
210 222 # pre create hooks
211 223 if pre_create_user.is_active():
212 224 allowed, reason = pre_create_user(created_by=created_by, **user_dict)
213 225 if not allowed:
214 226 raise UserCreationError(reason)
215 227
216 228
217 229 class ExtensionCallback(object):
218 230 """
219 231 Forwards a given call to rcextensions, sanitizes keyword arguments.
220 232
221 233 Does check if there is an extension active for that hook. If it is
222 234 there, it will forward all `kwargs_keys` keyword arguments to the
223 235 extension callback.
224 236 """
225 237
226 238 def __init__(self, hook_name, kwargs_keys):
227 239 self._hook_name = hook_name
228 240 self._kwargs_keys = set(kwargs_keys)
229 241
230 242 def __call__(self, *args, **kwargs):
231 243 kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys)
232 244 callback = self._get_callback()
233 245 if callback:
234 246 return callback(**kwargs_to_pass)
235 247
236 248 def is_active(self):
237 249 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
238 250
239 251 def _get_callback(self):
240 252 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
241 253
242 254
243 255 pre_pull_extension = ExtensionCallback(
244 256 hook_name='PRE_PULL_HOOK',
245 257 kwargs_keys=(
246 258 'server_url', 'config', 'scm', 'username', 'ip', 'action',
247 259 'repository'))
248 260
249 261
250 262 post_pull_extension = ExtensionCallback(
251 263 hook_name='PULL_HOOK',
252 264 kwargs_keys=(
253 265 'server_url', 'config', 'scm', 'username', 'ip', 'action',
254 266 'repository'))
255 267
256 268
257 269 pre_push_extension = ExtensionCallback(
258 270 hook_name='PRE_PUSH_HOOK',
259 271 kwargs_keys=(
260 272 'server_url', 'config', 'scm', 'username', 'ip', 'action',
261 273 'repository', 'repo_store_path'))
262 274
263 275
264 276 post_push_extension = ExtensionCallback(
265 277 hook_name='PUSH_HOOK',
266 278 kwargs_keys=(
267 279 'server_url', 'config', 'scm', 'username', 'ip', 'action',
268 280 'repository', 'repo_store_path', 'pushed_revs'))
269 281
270 282
271 283 pre_create_user = ExtensionCallback(
272 284 hook_name='PRE_CREATE_USER_HOOK',
273 285 kwargs_keys=(
274 286 'username', 'password', 'email', 'firstname', 'lastname', 'active',
275 287 'admin', 'created_by'))
276 288
277 289
278 290 log_create_pull_request = ExtensionCallback(
279 291 hook_name='CREATE_PULL_REQUEST',
280 292 kwargs_keys=(
281 293 'server_url', 'config', 'scm', 'username', 'ip', 'action',
282 294 'repository', 'pull_request_id', 'url', 'title', 'description',
283 295 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
284 296 'mergeable', 'source', 'target', 'author', 'reviewers'))
285 297
286 298
287 299 log_merge_pull_request = ExtensionCallback(
288 300 hook_name='MERGE_PULL_REQUEST',
289 301 kwargs_keys=(
290 302 'server_url', 'config', 'scm', 'username', 'ip', 'action',
291 303 'repository', 'pull_request_id', 'url', 'title', 'description',
292 304 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
293 305 'mergeable', 'source', 'target', 'author', 'reviewers'))
294 306
295 307
296 308 log_close_pull_request = ExtensionCallback(
297 309 hook_name='CLOSE_PULL_REQUEST',
298 310 kwargs_keys=(
299 311 'server_url', 'config', 'scm', 'username', 'ip', 'action',
300 312 'repository', 'pull_request_id', 'url', 'title', 'description',
301 313 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
302 314 'mergeable', 'source', 'target', 'author', 'reviewers'))
303 315
304 316
305 317 log_review_pull_request = ExtensionCallback(
306 318 hook_name='REVIEW_PULL_REQUEST',
307 319 kwargs_keys=(
308 320 'server_url', 'config', 'scm', 'username', 'ip', 'action',
309 321 'repository', 'pull_request_id', 'url', 'title', 'description',
310 322 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
311 323 'mergeable', 'source', 'target', 'author', 'reviewers'))
312 324
313 325
314 326 log_update_pull_request = ExtensionCallback(
315 327 hook_name='UPDATE_PULL_REQUEST',
316 328 kwargs_keys=(
317 329 'server_url', 'config', 'scm', 'username', 'ip', 'action',
318 330 'repository', 'pull_request_id', 'url', 'title', 'description',
319 331 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
320 332 'mergeable', 'source', 'target', 'author', 'reviewers'))
321 333
322 334
323 335 log_create_user = ExtensionCallback(
324 336 hook_name='CREATE_USER_HOOK',
325 337 kwargs_keys=(
326 338 'username', 'full_name_or_username', 'full_contact', 'user_id',
327 339 'name', 'firstname', 'short_contact', 'admin', 'lastname',
328 340 'ip_addresses', 'extern_type', 'extern_name',
329 341 'email', 'api_key', 'api_keys', 'last_login',
330 342 'full_name', 'active', 'password', 'emails',
331 343 'inherit_default_permissions', 'created_by', 'created_on'))
332 344
333 345
334 346 log_delete_user = ExtensionCallback(
335 347 hook_name='DELETE_USER_HOOK',
336 348 kwargs_keys=(
337 349 'username', 'full_name_or_username', 'full_contact', 'user_id',
338 350 'name', 'firstname', 'short_contact', 'admin', 'lastname',
339 351 'ip_addresses',
340 352 'email', 'api_key', 'last_login',
341 353 'full_name', 'active', 'password', 'emails',
342 354 'inherit_default_permissions', 'deleted_by'))
343 355
344 356
345 357 log_create_repository = ExtensionCallback(
346 358 hook_name='CREATE_REPO_HOOK',
347 359 kwargs_keys=(
348 360 'repo_name', 'repo_type', 'description', 'private', 'created_on',
349 361 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
350 362 'clone_uri', 'fork_id', 'group_id', 'created_by'))
351 363
352 364
353 365 log_delete_repository = ExtensionCallback(
354 366 hook_name='DELETE_REPO_HOOK',
355 367 kwargs_keys=(
356 368 'repo_name', 'repo_type', 'description', 'private', 'created_on',
357 369 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
358 370 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
359 371
360 372
361 373 log_create_repository_group = ExtensionCallback(
362 374 hook_name='CREATE_REPO_GROUP_HOOK',
363 375 kwargs_keys=(
364 376 'group_name', 'group_parent_id', 'group_description',
365 377 'group_id', 'user_id', 'created_by', 'created_on',
366 378 'enable_locking'))
@@ -1,164 +1,153 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # 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 The application's model objects
23 23
24 24 :example:
25 25
26 26 .. code-block:: python
27 27
28 28 from paste.deploy import appconfig
29 29 from pylons import config
30 30 from sqlalchemy import engine_from_config
31 31 from rhodecode.config.environment import load_environment
32 32
33 33 conf = appconfig('config:development.ini', relative_to = './../../')
34 34 load_environment(conf.global_conf, conf.local_conf)
35 35
36 36 engine = engine_from_config(config, 'sqlalchemy.')
37 37 init_model(engine)
38 38 # RUN YOUR CODE HERE
39 39
40 40 """
41 41
42 42
43 43 import logging
44 44
45 45 from pylons import config
46 46 from pyramid.threadlocal import get_current_registry
47 47
48 48 from rhodecode.model import meta, db
49 49 from rhodecode.lib.utils2 import obfuscate_url_pw, get_encryption_key
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def init_model(engine, encryption_key=None):
55 55 """
56 56 Initializes db session, bind the engine with the metadata,
57 57 Call this before using any of the tables or classes in the model,
58 58 preferably once in application start
59 59
60 60 :param engine: engine to bind to
61 61 """
62 62 engine_str = obfuscate_url_pw(str(engine.url))
63 63 log.info("initializing db for %s", engine_str)
64 64 meta.Base.metadata.bind = engine
65 65 db.ENCRYPTION_KEY = encryption_key
66 66
67 67
68 68 def init_model_encryption(migration_models):
69 69 migration_models.ENCRYPTION_KEY = get_encryption_key(config)
70 70 db.ENCRYPTION_KEY = get_encryption_key(config)
71 71
72 72
73 73 class BaseModel(object):
74 74 """
75 75 Base Model for all RhodeCode models, it adds sql alchemy session
76 76 into instance of model
77 77
78 78 :param sa: If passed it reuses this session instead of creating a new one
79 79 """
80 80
81 81 cls = None # override in child class
82 82
83 83 def __init__(self, sa=None):
84 84 if sa is not None:
85 85 self.sa = sa
86 86 else:
87 87 self.sa = meta.Session()
88 88
89 89 def _get_instance(self, cls, instance, callback=None):
90 90 """
91 91 Gets instance of given cls using some simple lookup mechanism.
92 92
93 93 :param cls: class to fetch
94 94 :param instance: int or Instance
95 95 :param callback: callback to call if all lookups failed
96 96 """
97 97
98 98 if isinstance(instance, cls):
99 99 return instance
100 100 elif isinstance(instance, (int, long)):
101 101 return cls.get(instance)
102 102 else:
103 103 if instance:
104 104 if callback is None:
105 105 raise Exception(
106 106 'given object must be int, long or Instance of %s '
107 107 'got %s, no callback provided' % (cls, type(instance))
108 108 )
109 109 else:
110 110 return callback(instance)
111 111
112 112 def _get_user(self, user):
113 113 """
114 114 Helper method to get user by ID, or username fallback
115 115
116 116 :param user: UserID, username, or User instance
117 117 """
118 118 return self._get_instance(
119 119 db.User, user, callback=db.User.get_by_username)
120 120
121 121 def _get_user_group(self, user_group):
122 122 """
123 123 Helper method to get user by ID, or username fallback
124 124
125 125 :param user_group: UserGroupID, user_group_name, or UserGroup instance
126 126 """
127 127 return self._get_instance(
128 128 db.UserGroup, user_group, callback=db.UserGroup.get_by_group_name)
129 129
130 130 def _get_repo(self, repository):
131 131 """
132 132 Helper method to get repository by ID, or repository name
133 133
134 134 :param repository: RepoID, repository name or Repository Instance
135 135 """
136 136 return self._get_instance(
137 137 db.Repository, repository, callback=db.Repository.get_by_repo_name)
138 138
139 139 def _get_perm(self, permission):
140 140 """
141 141 Helper method to get permission by ID, or permission name
142 142
143 143 :param permission: PermissionID, permission_name or Permission instance
144 144 """
145 145 return self._get_instance(
146 146 db.Permission, permission, callback=db.Permission.get_by_key)
147 147
148 def send_event(self, event):
149 """
150 Helper method to send an event. This wraps the pyramid logic to send an
151 event.
152 """
153 # For the first step we are using pyramids thread locals here. If the
154 # event mechanism works out as a good solution we should think about
155 # passing the registry into the constructor to get rid of it.
156 registry = get_current_registry()
157 registry.notify(event)
158
159 148 @classmethod
160 149 def get_all(cls):
161 150 """
162 151 Returns all instances of what is defined in `cls` class variable
163 152 """
164 153 return cls.cls.getAll()
@@ -1,924 +1,931 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # 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 Repository model for rhodecode
23 23 """
24 24
25 25 import logging
26 26 import os
27 27 import re
28 28 import shutil
29 29 import time
30 30 import traceback
31 31 from datetime import datetime
32 32
33 33 from sqlalchemy.sql import func
34 34 from sqlalchemy.sql.expression import true, or_
35 35 from zope.cachedescriptors.property import Lazy as LazyProperty
36 36
37 from rhodecode import events
37 38 from rhodecode.lib import helpers as h
38 39 from rhodecode.lib.auth import HasUserGroupPermissionAny
39 40 from rhodecode.lib.caching_query import FromCache
40 41 from rhodecode.lib.exceptions import AttachedForksError
41 42 from rhodecode.lib.hooks_base import log_delete_repository
42 43 from rhodecode.lib.utils import make_db_config
43 44 from rhodecode.lib.utils2 import (
44 45 safe_str, safe_unicode, remove_prefix, obfuscate_url_pw,
45 46 get_current_rhodecode_user, safe_int, datetime_to_time, action_logger_generic)
46 47 from rhodecode.lib.vcs.backends import get_backend
47 48 from rhodecode.model import BaseModel
48 49 from rhodecode.model.db import (
49 50 Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm,
50 51 UserGroupRepoGroupToPerm, User, Permission, Statistics, UserGroup,
51 52 RepoGroup, RepositoryField)
52 53 from rhodecode.model.scm import UserGroupList
53 54 from rhodecode.model.settings import VcsSettingsModel
54 55
55 56
56 57 log = logging.getLogger(__name__)
57 58
58 59
59 60 class RepoModel(BaseModel):
60 61
61 62 cls = Repository
62 63
63 64 def _get_user_group(self, users_group):
64 65 return self._get_instance(UserGroup, users_group,
65 66 callback=UserGroup.get_by_group_name)
66 67
67 68 def _get_repo_group(self, repo_group):
68 69 return self._get_instance(RepoGroup, repo_group,
69 70 callback=RepoGroup.get_by_group_name)
70 71
71 72 def _create_default_perms(self, repository, private):
72 73 # create default permission
73 74 default = 'repository.read'
74 75 def_user = User.get_default_user()
75 76 for p in def_user.user_perms:
76 77 if p.permission.permission_name.startswith('repository.'):
77 78 default = p.permission.permission_name
78 79 break
79 80
80 81 default_perm = 'repository.none' if private else default
81 82
82 83 repo_to_perm = UserRepoToPerm()
83 84 repo_to_perm.permission = Permission.get_by_key(default_perm)
84 85
85 86 repo_to_perm.repository = repository
86 87 repo_to_perm.user_id = def_user.user_id
87 88
88 89 return repo_to_perm
89 90
90 91 @LazyProperty
91 92 def repos_path(self):
92 93 """
93 94 Gets the repositories root path from database
94 95 """
95 96 settings_model = VcsSettingsModel(sa=self.sa)
96 97 return settings_model.get_repos_location()
97 98
98 99 def get(self, repo_id, cache=False):
99 100 repo = self.sa.query(Repository) \
100 101 .filter(Repository.repo_id == repo_id)
101 102
102 103 if cache:
103 104 repo = repo.options(FromCache("sql_cache_short",
104 105 "get_repo_%s" % repo_id))
105 106 return repo.scalar()
106 107
107 108 def get_repo(self, repository):
108 109 return self._get_repo(repository)
109 110
110 111 def get_by_repo_name(self, repo_name, cache=False):
111 112 repo = self.sa.query(Repository) \
112 113 .filter(Repository.repo_name == repo_name)
113 114
114 115 if cache:
115 116 repo = repo.options(FromCache("sql_cache_short",
116 117 "get_repo_%s" % repo_name))
117 118 return repo.scalar()
118 119
119 120 def _extract_id_from_repo_name(self, repo_name):
120 121 if repo_name.startswith('/'):
121 122 repo_name = repo_name.lstrip('/')
122 123 by_id_match = re.match(r'^_(\d{1,})', repo_name)
123 124 if by_id_match:
124 125 return by_id_match.groups()[0]
125 126
126 127 def get_repo_by_id(self, repo_name):
127 128 """
128 129 Extracts repo_name by id from special urls.
129 130 Example url is _11/repo_name
130 131
131 132 :param repo_name:
132 133 :return: repo object if matched else None
133 134 """
134 135 try:
135 136 _repo_id = self._extract_id_from_repo_name(repo_name)
136 137 if _repo_id:
137 138 return self.get(_repo_id)
138 139 except Exception:
139 140 log.exception('Failed to extract repo_name from URL')
140 141
141 142 return None
142 143
143 144 def get_users(self, name_contains=None, limit=20, only_active=True):
144 145 # TODO: mikhail: move this method to the UserModel.
145 146 query = self.sa.query(User)
146 147 if only_active:
147 148 query = query.filter(User.active == true())
148 149
149 150 if name_contains:
150 151 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
151 152 query = query.filter(
152 153 or_(
153 154 User.name.ilike(ilike_expression),
154 155 User.lastname.ilike(ilike_expression),
155 156 User.username.ilike(ilike_expression)
156 157 )
157 158 )
158 159 query = query.limit(limit)
159 160 users = query.all()
160 161
161 162 _users = [
162 163 {
163 164 'id': user.user_id,
164 165 'first_name': user.name,
165 166 'last_name': user.lastname,
166 167 'username': user.username,
167 168 'icon_link': h.gravatar_url(user.email, 14),
168 169 'value_display': h.person(user.email),
169 170 'value': user.username,
170 171 'value_type': 'user',
171 172 'active': user.active,
172 173 }
173 174 for user in users
174 175 ]
175 176 return _users
176 177
177 178 def get_user_groups(self, name_contains=None, limit=20, only_active=True):
178 179 # TODO: mikhail: move this method to the UserGroupModel.
179 180 query = self.sa.query(UserGroup)
180 181 if only_active:
181 182 query = query.filter(UserGroup.users_group_active == true())
182 183
183 184 if name_contains:
184 185 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
185 186 query = query.filter(
186 187 UserGroup.users_group_name.ilike(ilike_expression))\
187 188 .order_by(func.length(UserGroup.users_group_name))\
188 189 .order_by(UserGroup.users_group_name)
189 190
190 191 query = query.limit(limit)
191 192 user_groups = query.all()
192 193 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
193 194 user_groups = UserGroupList(user_groups, perm_set=perm_set)
194 195
195 196 _groups = [
196 197 {
197 198 'id': group.users_group_id,
198 199 # TODO: marcink figure out a way to generate the url for the
199 200 # icon
200 201 'icon_link': '',
201 202 'value_display': 'Group: %s (%d members)' % (
202 203 group.users_group_name, len(group.members),),
203 204 'value': group.users_group_name,
204 205 'value_type': 'user_group',
205 206 'active': group.users_group_active,
206 207 }
207 208 for group in user_groups
208 209 ]
209 210 return _groups
210 211
211 212 @classmethod
212 213 def update_repoinfo(cls, repositories=None):
213 214 if not repositories:
214 215 repositories = Repository.getAll()
215 216 for repo in repositories:
216 217 repo.update_commit_cache()
217 218
218 219 def get_repos_as_dict(self, repo_list=None, admin=False,
219 220 super_user_actions=False):
220 221
221 222 from rhodecode.lib.utils import PartialRenderer
222 223 _render = PartialRenderer('data_table/_dt_elements.html')
223 224 c = _render.c
224 225
225 226 def quick_menu(repo_name):
226 227 return _render('quick_menu', repo_name)
227 228
228 229 def repo_lnk(name, rtype, rstate, private, fork_of):
229 230 return _render('repo_name', name, rtype, rstate, private, fork_of,
230 231 short_name=not admin, admin=False)
231 232
232 233 def last_change(last_change):
233 234 return _render("last_change", last_change)
234 235
235 236 def rss_lnk(repo_name):
236 237 return _render("rss", repo_name)
237 238
238 239 def atom_lnk(repo_name):
239 240 return _render("atom", repo_name)
240 241
241 242 def last_rev(repo_name, cs_cache):
242 243 return _render('revision', repo_name, cs_cache.get('revision'),
243 244 cs_cache.get('raw_id'), cs_cache.get('author'),
244 245 cs_cache.get('message'))
245 246
246 247 def desc(desc):
247 248 if c.visual.stylify_metatags:
248 249 return h.urlify_text(h.escaped_stylize(h.truncate(desc, 60)))
249 250 else:
250 251 return h.urlify_text(h.html_escape(h.truncate(desc, 60)))
251 252
252 253 def state(repo_state):
253 254 return _render("repo_state", repo_state)
254 255
255 256 def repo_actions(repo_name):
256 257 return _render('repo_actions', repo_name, super_user_actions)
257 258
258 259 def user_profile(username):
259 260 return _render('user_profile', username)
260 261
261 262 repos_data = []
262 263 for repo in repo_list:
263 264 cs_cache = repo.changeset_cache
264 265 row = {
265 266 "menu": quick_menu(repo.repo_name),
266 267
267 268 "name": repo_lnk(repo.repo_name, repo.repo_type,
268 269 repo.repo_state, repo.private, repo.fork),
269 270 "name_raw": repo.repo_name.lower(),
270 271
271 272 "last_change": last_change(repo.last_db_change),
272 273 "last_change_raw": datetime_to_time(repo.last_db_change),
273 274
274 275 "last_changeset": last_rev(repo.repo_name, cs_cache),
275 276 "last_changeset_raw": cs_cache.get('revision'),
276 277
277 278 "desc": desc(repo.description),
278 279 "owner": user_profile(repo.user.username),
279 280
280 281 "state": state(repo.repo_state),
281 282 "rss": rss_lnk(repo.repo_name),
282 283
283 284 "atom": atom_lnk(repo.repo_name),
284 285 }
285 286 if admin:
286 287 row.update({
287 288 "action": repo_actions(repo.repo_name),
288 289 })
289 290 repos_data.append(row)
290 291
291 292 return repos_data
292 293
293 294 def _get_defaults(self, repo_name):
294 295 """
295 296 Gets information about repository, and returns a dict for
296 297 usage in forms
297 298
298 299 :param repo_name:
299 300 """
300 301
301 302 repo_info = Repository.get_by_repo_name(repo_name)
302 303
303 304 if repo_info is None:
304 305 return None
305 306
306 307 defaults = repo_info.get_dict()
307 308 defaults['repo_name'] = repo_info.just_name
308 309
309 310 groups = repo_info.groups_with_parents
310 311 parent_group = groups[-1] if groups else None
311 312
312 313 # we use -1 as this is how in HTML, we mark an empty group
313 314 defaults['repo_group'] = getattr(parent_group, 'group_id', -1)
314 315
315 316 keys_to_process = (
316 317 {'k': 'repo_type', 'strip': False},
317 318 {'k': 'repo_enable_downloads', 'strip': True},
318 319 {'k': 'repo_description', 'strip': True},
319 320 {'k': 'repo_enable_locking', 'strip': True},
320 321 {'k': 'repo_landing_rev', 'strip': True},
321 322 {'k': 'clone_uri', 'strip': False},
322 323 {'k': 'repo_private', 'strip': True},
323 324 {'k': 'repo_enable_statistics', 'strip': True}
324 325 )
325 326
326 327 for item in keys_to_process:
327 328 attr = item['k']
328 329 if item['strip']:
329 330 attr = remove_prefix(item['k'], 'repo_')
330 331
331 332 val = defaults[attr]
332 333 if item['k'] == 'repo_landing_rev':
333 334 val = ':'.join(defaults[attr])
334 335 defaults[item['k']] = val
335 336 if item['k'] == 'clone_uri':
336 337 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
337 338
338 339 # fill owner
339 340 if repo_info.user:
340 341 defaults.update({'user': repo_info.user.username})
341 342 else:
342 343 replacement_user = User.get_first_super_admin().username
343 344 defaults.update({'user': replacement_user})
344 345
345 346 # fill repository users
346 347 for p in repo_info.repo_to_perm:
347 348 defaults.update({'u_perm_%s' % p.user.user_id:
348 349 p.permission.permission_name})
349 350
350 351 # fill repository groups
351 352 for p in repo_info.users_group_to_perm:
352 353 defaults.update({'g_perm_%s' % p.users_group.users_group_id:
353 354 p.permission.permission_name})
354 355
355 356 return defaults
356 357
357 358 def update(self, repo, **kwargs):
358 359 try:
359 360 cur_repo = self._get_repo(repo)
360 361 source_repo_name = cur_repo.repo_name
361 362 if 'user' in kwargs:
362 363 cur_repo.user = User.get_by_username(kwargs['user'])
363 364
364 365 if 'repo_group' in kwargs:
365 366 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
366 367 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
367 368
368 369 update_keys = [
369 370 (1, 'repo_enable_downloads'),
370 371 (1, 'repo_description'),
371 372 (1, 'repo_enable_locking'),
372 373 (1, 'repo_landing_rev'),
373 374 (1, 'repo_private'),
374 375 (1, 'repo_enable_statistics'),
375 376 (0, 'clone_uri'),
376 377 (0, 'fork_id')
377 378 ]
378 379 for strip, k in update_keys:
379 380 if k in kwargs:
380 381 val = kwargs[k]
381 382 if strip:
382 383 k = remove_prefix(k, 'repo_')
383 384 if k == 'clone_uri':
384 385 from rhodecode.model.validators import Missing
385 386 _change = kwargs.get('clone_uri_change')
386 387 if _change in [Missing, 'OLD']:
387 388 # we don't change the value, so use original one
388 389 val = cur_repo.clone_uri
389 390
390 391 setattr(cur_repo, k, val)
391 392
392 393 new_name = cur_repo.get_new_name(kwargs['repo_name'])
393 394 cur_repo.repo_name = new_name
394 395
395 396 # if private flag is set, reset default permission to NONE
396 397 if kwargs.get('repo_private'):
397 398 EMPTY_PERM = 'repository.none'
398 399 RepoModel().grant_user_permission(
399 400 repo=cur_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM
400 401 )
401 402
402 403 # handle extra fields
403 404 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
404 405 kwargs):
405 406 k = RepositoryField.un_prefix_key(field)
406 407 ex_field = RepositoryField.get_by_key_name(
407 408 key=k, repo=cur_repo)
408 409 if ex_field:
409 410 ex_field.field_value = kwargs[field]
410 411 self.sa.add(ex_field)
411 412 self.sa.add(cur_repo)
412 413
413 414 if source_repo_name != new_name:
414 415 # rename repository
415 416 self._rename_filesystem_repo(
416 417 old=source_repo_name, new=new_name)
417 418
418 419 return cur_repo
419 420 except Exception:
420 421 log.error(traceback.format_exc())
421 422 raise
422 423
423 424 def _create_repo(self, repo_name, repo_type, description, owner,
424 425 private=False, clone_uri=None, repo_group=None,
425 426 landing_rev='rev:tip', fork_of=None,
426 427 copy_fork_permissions=False, enable_statistics=False,
427 428 enable_locking=False, enable_downloads=False,
428 429 copy_group_permissions=False,
429 430 state=Repository.STATE_PENDING):
430 431 """
431 432 Create repository inside database with PENDING state, this should be
432 433 only executed by create() repo. With exception of importing existing
433 434 repos
434 435 """
435 436 from rhodecode.model.scm import ScmModel
436 437
437 438 owner = self._get_user(owner)
438 439 fork_of = self._get_repo(fork_of)
439 440 repo_group = self._get_repo_group(safe_int(repo_group))
440 441
441 442 try:
442 443 repo_name = safe_unicode(repo_name)
443 444 description = safe_unicode(description)
444 445 # repo name is just a name of repository
445 446 # while repo_name_full is a full qualified name that is combined
446 447 # with name and path of group
447 448 repo_name_full = repo_name
448 449 repo_name = repo_name.split(Repository.NAME_SEP)[-1]
449 450
450 451 new_repo = Repository()
451 452 new_repo.repo_state = state
452 453 new_repo.enable_statistics = False
453 454 new_repo.repo_name = repo_name_full
454 455 new_repo.repo_type = repo_type
455 456 new_repo.user = owner
456 457 new_repo.group = repo_group
457 458 new_repo.description = description or repo_name
458 459 new_repo.private = private
459 460 new_repo.clone_uri = clone_uri
460 461 new_repo.landing_rev = landing_rev
461 462
462 463 new_repo.enable_statistics = enable_statistics
463 464 new_repo.enable_locking = enable_locking
464 465 new_repo.enable_downloads = enable_downloads
465 466
466 467 if repo_group:
467 468 new_repo.enable_locking = repo_group.enable_locking
468 469
469 470 if fork_of:
470 471 parent_repo = fork_of
471 472 new_repo.fork = parent_repo
472 473
474 events.trigger(events.RepoPreCreateEvent(new_repo))
475
473 476 self.sa.add(new_repo)
474 477
475 478 EMPTY_PERM = 'repository.none'
476 479 if fork_of and copy_fork_permissions:
477 480 repo = fork_of
478 481 user_perms = UserRepoToPerm.query() \
479 482 .filter(UserRepoToPerm.repository == repo).all()
480 483 group_perms = UserGroupRepoToPerm.query() \
481 484 .filter(UserGroupRepoToPerm.repository == repo).all()
482 485
483 486 for perm in user_perms:
484 487 UserRepoToPerm.create(
485 488 perm.user, new_repo, perm.permission)
486 489
487 490 for perm in group_perms:
488 491 UserGroupRepoToPerm.create(
489 492 perm.users_group, new_repo, perm.permission)
490 493 # in case we copy permissions and also set this repo to private
491 494 # override the default user permission to make it a private
492 495 # repo
493 496 if private:
494 497 RepoModel(self.sa).grant_user_permission(
495 498 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
496 499
497 500 elif repo_group and copy_group_permissions:
498 501 user_perms = UserRepoGroupToPerm.query() \
499 502 .filter(UserRepoGroupToPerm.group == repo_group).all()
500 503
501 504 group_perms = UserGroupRepoGroupToPerm.query() \
502 505 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
503 506
504 507 for perm in user_perms:
505 508 perm_name = perm.permission.permission_name.replace(
506 509 'group.', 'repository.')
507 510 perm_obj = Permission.get_by_key(perm_name)
508 511 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
509 512
510 513 for perm in group_perms:
511 514 perm_name = perm.permission.permission_name.replace(
512 515 'group.', 'repository.')
513 516 perm_obj = Permission.get_by_key(perm_name)
514 517 UserGroupRepoToPerm.create(
515 518 perm.users_group, new_repo, perm_obj)
516 519
517 520 if private:
518 521 RepoModel(self.sa).grant_user_permission(
519 522 repo=new_repo, user=User.DEFAULT_USER, perm=EMPTY_PERM)
520 523
521 524 else:
522 525 perm_obj = self._create_default_perms(new_repo, private)
523 526 self.sa.add(perm_obj)
524 527
525 528 # now automatically start following this repository as owner
526 529 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
527 530 owner.user_id)
531
528 532 # we need to flush here, in order to check if database won't
529 533 # throw any exceptions, create filesystem dirs at the very end
530 534 self.sa.flush()
535 events.trigger(events.RepoCreatedEvent(new_repo))
536 return new_repo
531 537
532 return new_repo
533 538 except Exception:
534 539 log.error(traceback.format_exc())
535 540 raise
536 541
537 542 def create(self, form_data, cur_user):
538 543 """
539 544 Create repository using celery tasks
540 545
541 546 :param form_data:
542 547 :param cur_user:
543 548 """
544 549 from rhodecode.lib.celerylib import tasks, run_task
545 550 return run_task(tasks.create_repo, form_data, cur_user)
546 551
547 552 def update_permissions(self, repo, perm_additions=None, perm_updates=None,
548 553 perm_deletions=None, check_perms=True,
549 554 cur_user=None):
550 555 if not perm_additions:
551 556 perm_additions = []
552 557 if not perm_updates:
553 558 perm_updates = []
554 559 if not perm_deletions:
555 560 perm_deletions = []
556 561
557 562 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
558 563
559 564 # update permissions
560 565 for member_id, perm, member_type in perm_updates:
561 566 member_id = int(member_id)
562 567 if member_type == 'user':
563 568 # this updates also current one if found
564 569 self.grant_user_permission(
565 570 repo=repo, user=member_id, perm=perm)
566 571 else: # set for user group
567 572 # check if we have permissions to alter this usergroup
568 573 member_name = UserGroup.get(member_id).users_group_name
569 574 if not check_perms or HasUserGroupPermissionAny(
570 575 *req_perms)(member_name, user=cur_user):
571 576 self.grant_user_group_permission(
572 577 repo=repo, group_name=member_id, perm=perm)
573 578
574 579 # set new permissions
575 580 for member_id, perm, member_type in perm_additions:
576 581 member_id = int(member_id)
577 582 if member_type == 'user':
578 583 self.grant_user_permission(
579 584 repo=repo, user=member_id, perm=perm)
580 585 else: # set for user group
581 586 # check if we have permissions to alter this usergroup
582 587 member_name = UserGroup.get(member_id).users_group_name
583 588 if not check_perms or HasUserGroupPermissionAny(
584 589 *req_perms)(member_name, user=cur_user):
585 590 self.grant_user_group_permission(
586 591 repo=repo, group_name=member_id, perm=perm)
587 592
588 593 # delete permissions
589 594 for member_id, perm, member_type in perm_deletions:
590 595 member_id = int(member_id)
591 596 if member_type == 'user':
592 597 self.revoke_user_permission(repo=repo, user=member_id)
593 598 else: # set for user group
594 599 # check if we have permissions to alter this usergroup
595 600 member_name = UserGroup.get(member_id).users_group_name
596 601 if not check_perms or HasUserGroupPermissionAny(
597 602 *req_perms)(member_name, user=cur_user):
598 603 self.revoke_user_group_permission(
599 604 repo=repo, group_name=member_id)
600 605
601 606 def create_fork(self, form_data, cur_user):
602 607 """
603 608 Simple wrapper into executing celery task for fork creation
604 609
605 610 :param form_data:
606 611 :param cur_user:
607 612 """
608 613 from rhodecode.lib.celerylib import tasks, run_task
609 614 return run_task(tasks.create_repo_fork, form_data, cur_user)
610 615
611 616 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
612 617 """
613 618 Delete given repository, forks parameter defines what do do with
614 619 attached forks. Throws AttachedForksError if deleted repo has attached
615 620 forks
616 621
617 622 :param repo:
618 623 :param forks: str 'delete' or 'detach'
619 624 :param fs_remove: remove(archive) repo from filesystem
620 625 """
621 626 if not cur_user:
622 627 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
623 628 repo = self._get_repo(repo)
624 629 if repo:
625 630 if forks == 'detach':
626 631 for r in repo.forks:
627 632 r.fork = None
628 633 self.sa.add(r)
629 634 elif forks == 'delete':
630 635 for r in repo.forks:
631 636 self.delete(r, forks='delete')
632 637 elif [f for f in repo.forks]:
633 638 raise AttachedForksError()
634 639
635 640 old_repo_dict = repo.get_dict()
641 events.trigger(events.RepoPreDeleteEvent(repo))
636 642 try:
637 643 self.sa.delete(repo)
638 644 if fs_remove:
639 645 self._delete_filesystem_repo(repo)
640 646 else:
641 647 log.debug('skipping removal from filesystem')
642 648 old_repo_dict.update({
643 649 'deleted_by': cur_user,
644 650 'deleted_on': time.time(),
645 651 })
646 652 log_delete_repository(**old_repo_dict)
653 events.trigger(events.RepoDeletedEvent(repo))
647 654 except Exception:
648 655 log.error(traceback.format_exc())
649 656 raise
650 657
651 658 def grant_user_permission(self, repo, user, perm):
652 659 """
653 660 Grant permission for user on given repository, or update existing one
654 661 if found
655 662
656 663 :param repo: Instance of Repository, repository_id, or repository name
657 664 :param user: Instance of User, user_id or username
658 665 :param perm: Instance of Permission, or permission_name
659 666 """
660 667 user = self._get_user(user)
661 668 repo = self._get_repo(repo)
662 669 permission = self._get_perm(perm)
663 670
664 671 # check if we have that permission already
665 672 obj = self.sa.query(UserRepoToPerm) \
666 673 .filter(UserRepoToPerm.user == user) \
667 674 .filter(UserRepoToPerm.repository == repo) \
668 675 .scalar()
669 676 if obj is None:
670 677 # create new !
671 678 obj = UserRepoToPerm()
672 679 obj.repository = repo
673 680 obj.user = user
674 681 obj.permission = permission
675 682 self.sa.add(obj)
676 683 log.debug('Granted perm %s to %s on %s', perm, user, repo)
677 684 action_logger_generic(
678 685 'granted permission: {} to user: {} on repo: {}'.format(
679 686 perm, user, repo), namespace='security.repo')
680 687 return obj
681 688
682 689 def revoke_user_permission(self, repo, user):
683 690 """
684 691 Revoke permission for user on given repository
685 692
686 693 :param repo: Instance of Repository, repository_id, or repository name
687 694 :param user: Instance of User, user_id or username
688 695 """
689 696
690 697 user = self._get_user(user)
691 698 repo = self._get_repo(repo)
692 699
693 700 obj = self.sa.query(UserRepoToPerm) \
694 701 .filter(UserRepoToPerm.repository == repo) \
695 702 .filter(UserRepoToPerm.user == user) \
696 703 .scalar()
697 704 if obj:
698 705 self.sa.delete(obj)
699 706 log.debug('Revoked perm on %s on %s', repo, user)
700 707 action_logger_generic(
701 708 'revoked permission from user: {} on repo: {}'.format(
702 709 user, repo), namespace='security.repo')
703 710
704 711 def grant_user_group_permission(self, repo, group_name, perm):
705 712 """
706 713 Grant permission for user group on given repository, or update
707 714 existing one if found
708 715
709 716 :param repo: Instance of Repository, repository_id, or repository name
710 717 :param group_name: Instance of UserGroup, users_group_id,
711 718 or user group name
712 719 :param perm: Instance of Permission, or permission_name
713 720 """
714 721 repo = self._get_repo(repo)
715 722 group_name = self._get_user_group(group_name)
716 723 permission = self._get_perm(perm)
717 724
718 725 # check if we have that permission already
719 726 obj = self.sa.query(UserGroupRepoToPerm) \
720 727 .filter(UserGroupRepoToPerm.users_group == group_name) \
721 728 .filter(UserGroupRepoToPerm.repository == repo) \
722 729 .scalar()
723 730
724 731 if obj is None:
725 732 # create new
726 733 obj = UserGroupRepoToPerm()
727 734
728 735 obj.repository = repo
729 736 obj.users_group = group_name
730 737 obj.permission = permission
731 738 self.sa.add(obj)
732 739 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
733 740 action_logger_generic(
734 741 'granted permission: {} to usergroup: {} on repo: {}'.format(
735 742 perm, group_name, repo), namespace='security.repo')
736 743
737 744 return obj
738 745
739 746 def revoke_user_group_permission(self, repo, group_name):
740 747 """
741 748 Revoke permission for user group on given repository
742 749
743 750 :param repo: Instance of Repository, repository_id, or repository name
744 751 :param group_name: Instance of UserGroup, users_group_id,
745 752 or user group name
746 753 """
747 754 repo = self._get_repo(repo)
748 755 group_name = self._get_user_group(group_name)
749 756
750 757 obj = self.sa.query(UserGroupRepoToPerm) \
751 758 .filter(UserGroupRepoToPerm.repository == repo) \
752 759 .filter(UserGroupRepoToPerm.users_group == group_name) \
753 760 .scalar()
754 761 if obj:
755 762 self.sa.delete(obj)
756 763 log.debug('Revoked perm to %s on %s', repo, group_name)
757 764 action_logger_generic(
758 765 'revoked permission from usergroup: {} on repo: {}'.format(
759 766 group_name, repo), namespace='security.repo')
760 767
761 768 def delete_stats(self, repo_name):
762 769 """
763 770 removes stats for given repo
764 771
765 772 :param repo_name:
766 773 """
767 774 repo = self._get_repo(repo_name)
768 775 try:
769 776 obj = self.sa.query(Statistics) \
770 777 .filter(Statistics.repository == repo).scalar()
771 778 if obj:
772 779 self.sa.delete(obj)
773 780 except Exception:
774 781 log.error(traceback.format_exc())
775 782 raise
776 783
777 784 def add_repo_field(self, repo_name, field_key, field_label, field_value='',
778 785 field_type='str', field_desc=''):
779 786
780 787 repo = self._get_repo(repo_name)
781 788
782 789 new_field = RepositoryField()
783 790 new_field.repository = repo
784 791 new_field.field_key = field_key
785 792 new_field.field_type = field_type # python type
786 793 new_field.field_value = field_value
787 794 new_field.field_desc = field_desc
788 795 new_field.field_label = field_label
789 796 self.sa.add(new_field)
790 797 return new_field
791 798
792 799 def delete_repo_field(self, repo_name, field_key):
793 800 repo = self._get_repo(repo_name)
794 801 field = RepositoryField.get_by_key_name(field_key, repo)
795 802 if field:
796 803 self.sa.delete(field)
797 804
798 805 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
799 806 clone_uri=None, repo_store_location=None,
800 807 use_global_config=False):
801 808 """
802 809 makes repository on filesystem. It's group aware means it'll create
803 810 a repository within a group, and alter the paths accordingly of
804 811 group location
805 812
806 813 :param repo_name:
807 814 :param alias:
808 815 :param parent:
809 816 :param clone_uri:
810 817 :param repo_store_location:
811 818 """
812 819 from rhodecode.lib.utils import is_valid_repo, is_valid_repo_group
813 820 from rhodecode.model.scm import ScmModel
814 821
815 822 if Repository.NAME_SEP in repo_name:
816 823 raise ValueError(
817 824 'repo_name must not contain groups got `%s`' % repo_name)
818 825
819 826 if isinstance(repo_group, RepoGroup):
820 827 new_parent_path = os.sep.join(repo_group.full_path_splitted)
821 828 else:
822 829 new_parent_path = repo_group or ''
823 830
824 831 if repo_store_location:
825 832 _paths = [repo_store_location]
826 833 else:
827 834 _paths = [self.repos_path, new_parent_path, repo_name]
828 835 # we need to make it str for mercurial
829 836 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
830 837
831 838 # check if this path is not a repository
832 839 if is_valid_repo(repo_path, self.repos_path):
833 840 raise Exception('This path %s is a valid repository' % repo_path)
834 841
835 842 # check if this path is a group
836 843 if is_valid_repo_group(repo_path, self.repos_path):
837 844 raise Exception('This path %s is a valid group' % repo_path)
838 845
839 846 log.info('creating repo %s in %s from url: `%s`',
840 847 repo_name, safe_unicode(repo_path),
841 848 obfuscate_url_pw(clone_uri))
842 849
843 850 backend = get_backend(repo_type)
844 851
845 852 config_repo = None if use_global_config else repo_name
846 853 if config_repo and new_parent_path:
847 854 config_repo = Repository.NAME_SEP.join(
848 855 (new_parent_path, config_repo))
849 856 config = make_db_config(clear_session=False, repo=config_repo)
850 857 config.set('extensions', 'largefiles', '')
851 858
852 859 # patch and reset hooks section of UI config to not run any
853 860 # hooks on creating remote repo
854 861 config.clear_section('hooks')
855 862
856 863 # TODO: johbo: Unify this, hardcoded "bare=True" does not look nice
857 864 if repo_type == 'git':
858 865 repo = backend(
859 866 repo_path, config=config, create=True, src_url=clone_uri,
860 867 bare=True)
861 868 else:
862 869 repo = backend(
863 870 repo_path, config=config, create=True, src_url=clone_uri)
864 871
865 872 ScmModel().install_hooks(repo, repo_type=repo_type)
866 873
867 874 log.debug('Created repo %s with %s backend',
868 875 safe_unicode(repo_name), safe_unicode(repo_type))
869 876 return repo
870 877
871 878 def _rename_filesystem_repo(self, old, new):
872 879 """
873 880 renames repository on filesystem
874 881
875 882 :param old: old name
876 883 :param new: new name
877 884 """
878 885 log.info('renaming repo from %s to %s', old, new)
879 886
880 887 old_path = os.path.join(self.repos_path, old)
881 888 new_path = os.path.join(self.repos_path, new)
882 889 if os.path.isdir(new_path):
883 890 raise Exception(
884 891 'Was trying to rename to already existing dir %s' % new_path
885 892 )
886 893 shutil.move(old_path, new_path)
887 894
888 895 def _delete_filesystem_repo(self, repo):
889 896 """
890 897 removes repo from filesystem, the removal is acctually made by
891 898 added rm__ prefix into dir, and rename internat .hg/.git dirs so this
892 899 repository is no longer valid for rhodecode, can be undeleted later on
893 900 by reverting the renames on this repository
894 901
895 902 :param repo: repo object
896 903 """
897 904 rm_path = os.path.join(self.repos_path, repo.repo_name)
898 905 repo_group = repo.group
899 906 log.info("Removing repository %s", rm_path)
900 907 # disable hg/git internal that it doesn't get detected as repo
901 908 alias = repo.repo_type
902 909
903 910 config = make_db_config(clear_session=False)
904 911 config.set('extensions', 'largefiles', '')
905 912 bare = getattr(repo.scm_instance(config=config), 'bare', False)
906 913
907 914 # skip this for bare git repos
908 915 if not bare:
909 916 # disable VCS repo
910 917 vcs_path = os.path.join(rm_path, '.%s' % alias)
911 918 if os.path.exists(vcs_path):
912 919 shutil.move(vcs_path, os.path.join(rm_path, 'rm__.%s' % alias))
913 920
914 921 _now = datetime.now()
915 922 _ms = str(_now.microsecond).rjust(6, '0')
916 923 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
917 924 repo.just_name)
918 925 if repo_group:
919 926 # if repository is in group, prefix the removal path with the group
920 927 args = repo_group.full_path_splitted + [_d]
921 928 _d = os.path.join(*args)
922 929
923 930 if os.path.isdir(rm_path):
924 931 shutil.move(rm_path, os.path.join(self.repos_path, _d))
@@ -1,838 +1,838 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # 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 users model for RhodeCode
23 23 """
24 24
25 25 import logging
26 26 import traceback
27 27
28 28 import datetime
29 29 from pylons.i18n.translation import _
30 30
31 31 import ipaddress
32 32 from sqlalchemy.exc import DatabaseError
33 33 from sqlalchemy.sql.expression import true, false
34 34
35 from rhodecode.events import UserPreCreate, UserPreUpdate
35 from rhodecode import events
36 36 from rhodecode.lib.utils2 import (
37 37 safe_unicode, get_current_rhodecode_user, action_logger_generic,
38 38 AttributeDict)
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.model import BaseModel
41 41 from rhodecode.model.auth_token import AuthTokenModel
42 42 from rhodecode.model.db import (
43 43 User, UserToPerm, UserEmailMap, UserIpMap)
44 44 from rhodecode.lib.exceptions import (
45 45 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
46 46 UserOwnsUserGroupsException, NotAllowedToCreateUserError)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.repo_group import RepoGroupModel
49 49
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 class UserModel(BaseModel):
55 55 cls = User
56 56
57 57 def get(self, user_id, cache=False):
58 58 user = self.sa.query(User)
59 59 if cache:
60 60 user = user.options(FromCache("sql_cache_short",
61 61 "get_user_%s" % user_id))
62 62 return user.get(user_id)
63 63
64 64 def get_user(self, user):
65 65 return self._get_user(user)
66 66
67 67 def get_by_username(self, username, cache=False, case_insensitive=False):
68 68
69 69 if case_insensitive:
70 70 user = self.sa.query(User).filter(User.username.ilike(username))
71 71 else:
72 72 user = self.sa.query(User)\
73 73 .filter(User.username == username)
74 74 if cache:
75 75 user = user.options(FromCache("sql_cache_short",
76 76 "get_user_%s" % username))
77 77 return user.scalar()
78 78
79 79 def get_by_email(self, email, cache=False, case_insensitive=False):
80 80 return User.get_by_email(email, case_insensitive, cache)
81 81
82 82 def get_by_auth_token(self, auth_token, cache=False):
83 83 return User.get_by_auth_token(auth_token, cache)
84 84
85 85 def get_active_user_count(self, cache=False):
86 86 return User.query().filter(
87 87 User.active == True).filter(
88 88 User.username != User.DEFAULT_USER).count()
89 89
90 90 def create(self, form_data, cur_user=None):
91 91 if not cur_user:
92 92 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
93 93
94 94 user_data = {
95 95 'username': form_data['username'],
96 96 'password': form_data['password'],
97 97 'email': form_data['email'],
98 98 'firstname': form_data['firstname'],
99 99 'lastname': form_data['lastname'],
100 100 'active': form_data['active'],
101 101 'extern_type': form_data['extern_type'],
102 102 'extern_name': form_data['extern_name'],
103 103 'admin': False,
104 104 'cur_user': cur_user
105 105 }
106 106
107 107 try:
108 108 if form_data.get('create_repo_group'):
109 109 user_data['create_repo_group'] = True
110 110 if form_data.get('password_change'):
111 111 user_data['force_password_change'] = True
112 112
113 113 return UserModel().create_or_update(**user_data)
114 114 except Exception:
115 115 log.error(traceback.format_exc())
116 116 raise
117 117
118 118 def update_user(self, user, skip_attrs=None, **kwargs):
119 119 from rhodecode.lib.auth import get_crypt_password
120 120
121 121 user = self._get_user(user)
122 122 if user.username == User.DEFAULT_USER:
123 123 raise DefaultUserException(
124 124 _("You can't Edit this user since it's"
125 125 " crucial for entire application"))
126 126
127 127 # first store only defaults
128 128 user_attrs = {
129 129 'updating_user_id': user.user_id,
130 130 'username': user.username,
131 131 'password': user.password,
132 132 'email': user.email,
133 133 'firstname': user.name,
134 134 'lastname': user.lastname,
135 135 'active': user.active,
136 136 'admin': user.admin,
137 137 'extern_name': user.extern_name,
138 138 'extern_type': user.extern_type,
139 139 'language': user.user_data.get('language')
140 140 }
141 141
142 142 # in case there's new_password, that comes from form, use it to
143 143 # store password
144 144 if kwargs.get('new_password'):
145 145 kwargs['password'] = kwargs['new_password']
146 146
147 147 # cleanups, my_account password change form
148 148 kwargs.pop('current_password', None)
149 149 kwargs.pop('new_password', None)
150 150 kwargs.pop('new_password_confirmation', None)
151 151
152 152 # cleanups, user edit password change form
153 153 kwargs.pop('password_confirmation', None)
154 154 kwargs.pop('password_change', None)
155 155
156 156 # create repo group on user creation
157 157 kwargs.pop('create_repo_group', None)
158 158
159 159 # legacy forms send name, which is the firstname
160 160 firstname = kwargs.pop('name', None)
161 161 if firstname:
162 162 kwargs['firstname'] = firstname
163 163
164 164 for k, v in kwargs.items():
165 165 # skip if we don't want to update this
166 166 if skip_attrs and k in skip_attrs:
167 167 continue
168 168
169 169 user_attrs[k] = v
170 170
171 171 try:
172 172 return self.create_or_update(**user_attrs)
173 173 except Exception:
174 174 log.error(traceback.format_exc())
175 175 raise
176 176
177 177 def create_or_update(
178 178 self, username, password, email, firstname='', lastname='',
179 179 active=True, admin=False, extern_type=None, extern_name=None,
180 180 cur_user=None, plugin=None, force_password_change=False,
181 181 allow_to_create_user=True, create_repo_group=False,
182 182 updating_user_id=None, language=None, strict_creation_check=True):
183 183 """
184 184 Creates a new instance if not found, or updates current one
185 185
186 186 :param username:
187 187 :param password:
188 188 :param email:
189 189 :param firstname:
190 190 :param lastname:
191 191 :param active:
192 192 :param admin:
193 193 :param extern_type:
194 194 :param extern_name:
195 195 :param cur_user:
196 196 :param plugin: optional plugin this method was called from
197 197 :param force_password_change: toggles new or existing user flag
198 198 for password change
199 199 :param allow_to_create_user: Defines if the method can actually create
200 200 new users
201 201 :param create_repo_group: Defines if the method should also
202 202 create an repo group with user name, and owner
203 203 :param updating_user_id: if we set it up this is the user we want to
204 204 update this allows to editing username.
205 205 :param language: language of user from interface.
206 206
207 207 :returns: new User object with injected `is_new_user` attribute.
208 208 """
209 209 if not cur_user:
210 210 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
211 211
212 212 from rhodecode.lib.auth import (
213 213 get_crypt_password, check_password, generate_auth_token)
214 214 from rhodecode.lib.hooks_base import (
215 215 log_create_user, check_allowed_create_user)
216 216
217 217 def _password_change(new_user, password):
218 218 # empty password
219 219 if not new_user.password:
220 220 return False
221 221
222 222 # password check is only needed for RhodeCode internal auth calls
223 223 # in case it's a plugin we don't care
224 224 if not plugin:
225 225
226 226 # first check if we gave crypted password back, and if it matches
227 227 # it's not password change
228 228 if new_user.password == password:
229 229 return False
230 230
231 231 password_match = check_password(password, new_user.password)
232 232 if not password_match:
233 233 return True
234 234
235 235 return False
236 236
237 237 user_data = {
238 238 'username': username,
239 239 'password': password,
240 240 'email': email,
241 241 'firstname': firstname,
242 242 'lastname': lastname,
243 243 'active': active,
244 244 'admin': admin
245 245 }
246 246
247 247 if updating_user_id:
248 248 log.debug('Checking for existing account in RhodeCode '
249 249 'database with user_id `%s` ' % (updating_user_id,))
250 250 user = User.get(updating_user_id)
251 251 else:
252 252 log.debug('Checking for existing account in RhodeCode '
253 253 'database with username `%s` ' % (username,))
254 254 user = User.get_by_username(username, case_insensitive=True)
255 255
256 256 if user is None:
257 257 # we check internal flag if this method is actually allowed to
258 258 # create new user
259 259 if not allow_to_create_user:
260 260 msg = ('Method wants to create new user, but it is not '
261 261 'allowed to do so')
262 262 log.warning(msg)
263 263 raise NotAllowedToCreateUserError(msg)
264 264
265 265 log.debug('Creating new user %s', username)
266 266
267 267 # only if we create user that is active
268 268 new_active_user = active
269 269 if new_active_user and strict_creation_check:
270 270 # raises UserCreationError if it's not allowed for any reason to
271 271 # create new active user, this also executes pre-create hooks
272 272 check_allowed_create_user(user_data, cur_user, strict_check=True)
273 self.send_event(UserPreCreate(user_data))
273 events.trigger(events.UserPreCreate(user_data))
274 274 new_user = User()
275 275 edit = False
276 276 else:
277 277 log.debug('updating user %s', username)
278 self.send_event(UserPreUpdate(user, user_data))
278 events.trigger(events.UserPreUpdate(user, user_data))
279 279 new_user = user
280 280 edit = True
281 281
282 282 # we're not allowed to edit default user
283 283 if user.username == User.DEFAULT_USER:
284 284 raise DefaultUserException(
285 285 _("You can't edit this user (`%(username)s`) since it's "
286 286 "crucial for entire application") % {'username': user.username})
287 287
288 288 # inject special attribute that will tell us if User is new or old
289 289 new_user.is_new_user = not edit
290 290 # for users that didn's specify auth type, we use RhodeCode built in
291 291 from rhodecode.authentication.plugins import auth_rhodecode
292 292 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.name
293 293 extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.name
294 294
295 295 try:
296 296 new_user.username = username
297 297 new_user.admin = admin
298 298 new_user.email = email
299 299 new_user.active = active
300 300 new_user.extern_name = safe_unicode(extern_name)
301 301 new_user.extern_type = safe_unicode(extern_type)
302 302 new_user.name = firstname
303 303 new_user.lastname = lastname
304 304
305 305 if not edit:
306 306 new_user.api_key = generate_auth_token(username)
307 307
308 308 # set password only if creating an user or password is changed
309 309 if not edit or _password_change(new_user, password):
310 310 reason = 'new password' if edit else 'new user'
311 311 log.debug('Updating password reason=>%s', reason)
312 312 new_user.password = get_crypt_password(password) if password else None
313 313
314 314 if force_password_change:
315 315 new_user.update_userdata(force_password_change=True)
316 316 if language:
317 317 new_user.update_userdata(language=language)
318 318
319 319 self.sa.add(new_user)
320 320
321 321 if not edit and create_repo_group:
322 322 # create new group same as username, and make this user an owner
323 323 desc = RepoGroupModel.PERSONAL_GROUP_DESC % {'username': username}
324 324 RepoGroupModel().create(group_name=username,
325 325 group_description=desc,
326 326 owner=username, commit_early=False)
327 327 if not edit:
328 328 # add the RSS token
329 329 AuthTokenModel().create(username,
330 330 description='Generated feed token',
331 331 role=AuthTokenModel.cls.ROLE_FEED)
332 332 log_create_user(created_by=cur_user, **new_user.get_dict())
333 333 return new_user
334 334 except (DatabaseError,):
335 335 log.error(traceback.format_exc())
336 336 raise
337 337
338 338 def create_registration(self, form_data):
339 339 from rhodecode.model.notification import NotificationModel
340 340 from rhodecode.model.notification import EmailNotificationModel
341 341
342 342 try:
343 343 form_data['admin'] = False
344 344 form_data['extern_name'] = 'rhodecode'
345 345 form_data['extern_type'] = 'rhodecode'
346 346 new_user = self.create(form_data)
347 347
348 348 self.sa.add(new_user)
349 349 self.sa.flush()
350 350
351 351 user_data = new_user.get_dict()
352 352 kwargs = {
353 353 # use SQLALCHEMY safe dump of user data
354 354 'user': AttributeDict(user_data),
355 355 'date': datetime.datetime.now()
356 356 }
357 357 notification_type = EmailNotificationModel.TYPE_REGISTRATION
358 358 # pre-generate the subject for notification itself
359 359 (subject,
360 360 _h, _e, # we don't care about those
361 361 body_plaintext) = EmailNotificationModel().render_email(
362 362 notification_type, **kwargs)
363 363
364 364 # create notification objects, and emails
365 365 NotificationModel().create(
366 366 created_by=new_user,
367 367 notification_subject=subject,
368 368 notification_body=body_plaintext,
369 369 notification_type=notification_type,
370 370 recipients=None, # all admins
371 371 email_kwargs=kwargs,
372 372 )
373 373
374 374 return new_user
375 375 except Exception:
376 376 log.error(traceback.format_exc())
377 377 raise
378 378
379 379 def _handle_user_repos(self, username, repositories, handle_mode=None):
380 380 _superadmin = self.cls.get_first_super_admin()
381 381 left_overs = True
382 382
383 383 from rhodecode.model.repo import RepoModel
384 384
385 385 if handle_mode == 'detach':
386 386 for obj in repositories:
387 387 obj.user = _superadmin
388 388 # set description we know why we super admin now owns
389 389 # additional repositories that were orphaned !
390 390 obj.description += ' \n::detached repository from deleted user: %s' % (username,)
391 391 self.sa.add(obj)
392 392 left_overs = False
393 393 elif handle_mode == 'delete':
394 394 for obj in repositories:
395 395 RepoModel().delete(obj, forks='detach')
396 396 left_overs = False
397 397
398 398 # if nothing is done we have left overs left
399 399 return left_overs
400 400
401 401 def _handle_user_repo_groups(self, username, repository_groups,
402 402 handle_mode=None):
403 403 _superadmin = self.cls.get_first_super_admin()
404 404 left_overs = True
405 405
406 406 from rhodecode.model.repo_group import RepoGroupModel
407 407
408 408 if handle_mode == 'detach':
409 409 for r in repository_groups:
410 410 r.user = _superadmin
411 411 # set description we know why we super admin now owns
412 412 # additional repositories that were orphaned !
413 413 r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
414 414 self.sa.add(r)
415 415 left_overs = False
416 416 elif handle_mode == 'delete':
417 417 for r in repository_groups:
418 418 RepoGroupModel().delete(r)
419 419 left_overs = False
420 420
421 421 # if nothing is done we have left overs left
422 422 return left_overs
423 423
424 424 def _handle_user_user_groups(self, username, user_groups, handle_mode=None):
425 425 _superadmin = self.cls.get_first_super_admin()
426 426 left_overs = True
427 427
428 428 from rhodecode.model.user_group import UserGroupModel
429 429
430 430 if handle_mode == 'detach':
431 431 for r in user_groups:
432 432 for user_user_group_to_perm in r.user_user_group_to_perm:
433 433 if user_user_group_to_perm.user.username == username:
434 434 user_user_group_to_perm.user = _superadmin
435 435 r.user = _superadmin
436 436 # set description we know why we super admin now owns
437 437 # additional repositories that were orphaned !
438 438 r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
439 439 self.sa.add(r)
440 440 left_overs = False
441 441 elif handle_mode == 'delete':
442 442 for r in user_groups:
443 443 UserGroupModel().delete(r)
444 444 left_overs = False
445 445
446 446 # if nothing is done we have left overs left
447 447 return left_overs
448 448
449 449 def delete(self, user, cur_user=None, handle_repos=None,
450 450 handle_repo_groups=None, handle_user_groups=None):
451 451 if not cur_user:
452 452 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
453 453 user = self._get_user(user)
454 454
455 455 try:
456 456 if user.username == User.DEFAULT_USER:
457 457 raise DefaultUserException(
458 458 _(u"You can't remove this user since it's"
459 459 u" crucial for entire application"))
460 460
461 461 left_overs = self._handle_user_repos(
462 462 user.username, user.repositories, handle_repos)
463 463 if left_overs and user.repositories:
464 464 repos = [x.repo_name for x in user.repositories]
465 465 raise UserOwnsReposException(
466 466 _(u'user "%s" still owns %s repositories and cannot be '
467 467 u'removed. Switch owners or remove those repositories:%s')
468 468 % (user.username, len(repos), ', '.join(repos)))
469 469
470 470 left_overs = self._handle_user_repo_groups(
471 471 user.username, user.repository_groups, handle_repo_groups)
472 472 if left_overs and user.repository_groups:
473 473 repo_groups = [x.group_name for x in user.repository_groups]
474 474 raise UserOwnsRepoGroupsException(
475 475 _(u'user "%s" still owns %s repository groups and cannot be '
476 476 u'removed. Switch owners or remove those repository groups:%s')
477 477 % (user.username, len(repo_groups), ', '.join(repo_groups)))
478 478
479 479 left_overs = self._handle_user_user_groups(
480 480 user.username, user.user_groups, handle_user_groups)
481 481 if left_overs and user.user_groups:
482 482 user_groups = [x.users_group_name for x in user.user_groups]
483 483 raise UserOwnsUserGroupsException(
484 484 _(u'user "%s" still owns %s user groups and cannot be '
485 485 u'removed. Switch owners or remove those user groups:%s')
486 486 % (user.username, len(user_groups), ', '.join(user_groups)))
487 487
488 488 # we might change the user data with detach/delete, make sure
489 489 # the object is marked as expired before actually deleting !
490 490 self.sa.expire(user)
491 491 self.sa.delete(user)
492 492 from rhodecode.lib.hooks_base import log_delete_user
493 493 log_delete_user(deleted_by=cur_user, **user.get_dict())
494 494 except Exception:
495 495 log.error(traceback.format_exc())
496 496 raise
497 497
498 498 def reset_password_link(self, data, pwd_reset_url):
499 499 from rhodecode.lib.celerylib import tasks, run_task
500 500 from rhodecode.model.notification import EmailNotificationModel
501 501 user_email = data['email']
502 502 try:
503 503 user = User.get_by_email(user_email)
504 504 if user:
505 505 log.debug('password reset user found %s', user)
506 506
507 507 email_kwargs = {
508 508 'password_reset_url': pwd_reset_url,
509 509 'user': user,
510 510 'email': user_email,
511 511 'date': datetime.datetime.now()
512 512 }
513 513
514 514 (subject, headers, email_body,
515 515 email_body_plaintext) = EmailNotificationModel().render_email(
516 516 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
517 517
518 518 recipients = [user_email]
519 519
520 520 action_logger_generic(
521 521 'sending password reset email to user: {}'.format(
522 522 user), namespace='security.password_reset')
523 523
524 524 run_task(tasks.send_email, recipients, subject,
525 525 email_body_plaintext, email_body)
526 526
527 527 else:
528 528 log.debug("password reset email %s not found", user_email)
529 529 except Exception:
530 530 log.error(traceback.format_exc())
531 531 return False
532 532
533 533 return True
534 534
535 535 def reset_password(self, data):
536 536 from rhodecode.lib.celerylib import tasks, run_task
537 537 from rhodecode.model.notification import EmailNotificationModel
538 538 from rhodecode.lib import auth
539 539 user_email = data['email']
540 540 pre_db = True
541 541 try:
542 542 user = User.get_by_email(user_email)
543 543 new_passwd = auth.PasswordGenerator().gen_password(
544 544 12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
545 545 if user:
546 546 user.password = auth.get_crypt_password(new_passwd)
547 547 # also force this user to reset his password !
548 548 user.update_userdata(force_password_change=True)
549 549
550 550 Session().add(user)
551 551 Session().commit()
552 552 log.info('change password for %s', user_email)
553 553 if new_passwd is None:
554 554 raise Exception('unable to generate new password')
555 555
556 556 pre_db = False
557 557
558 558 email_kwargs = {
559 559 'new_password': new_passwd,
560 560 'user': user,
561 561 'email': user_email,
562 562 'date': datetime.datetime.now()
563 563 }
564 564
565 565 (subject, headers, email_body,
566 566 email_body_plaintext) = EmailNotificationModel().render_email(
567 567 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, **email_kwargs)
568 568
569 569 recipients = [user_email]
570 570
571 571 action_logger_generic(
572 572 'sent new password to user: {} with email: {}'.format(
573 573 user, user_email), namespace='security.password_reset')
574 574
575 575 run_task(tasks.send_email, recipients, subject,
576 576 email_body_plaintext, email_body)
577 577
578 578 except Exception:
579 579 log.error('Failed to update user password')
580 580 log.error(traceback.format_exc())
581 581 if pre_db:
582 582 # we rollback only if local db stuff fails. If it goes into
583 583 # run_task, we're pass rollback state this wouldn't work then
584 584 Session().rollback()
585 585
586 586 return True
587 587
588 588 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
589 589 """
590 590 Fetches auth_user by user_id,or api_key if present.
591 591 Fills auth_user attributes with those taken from database.
592 592 Additionally set's is_authenitated if lookup fails
593 593 present in database
594 594
595 595 :param auth_user: instance of user to set attributes
596 596 :param user_id: user id to fetch by
597 597 :param api_key: api key to fetch by
598 598 :param username: username to fetch by
599 599 """
600 600 if user_id is None and api_key is None and username is None:
601 601 raise Exception('You need to pass user_id, api_key or username')
602 602
603 603 log.debug(
604 604 'doing fill data based on: user_id:%s api_key:%s username:%s',
605 605 user_id, api_key, username)
606 606 try:
607 607 dbuser = None
608 608 if user_id:
609 609 dbuser = self.get(user_id)
610 610 elif api_key:
611 611 dbuser = self.get_by_auth_token(api_key)
612 612 elif username:
613 613 dbuser = self.get_by_username(username)
614 614
615 615 if not dbuser:
616 616 log.warning(
617 617 'Unable to lookup user by id:%s api_key:%s username:%s',
618 618 user_id, api_key, username)
619 619 return False
620 620 if not dbuser.active:
621 621 log.debug('User `%s` is inactive, skipping fill data', username)
622 622 return False
623 623
624 624 log.debug('filling user:%s data', dbuser)
625 625
626 626 # TODO: johbo: Think about this and find a clean solution
627 627 user_data = dbuser.get_dict()
628 628 user_data.update(dbuser.get_api_data(include_secrets=True))
629 629
630 630 for k, v in user_data.iteritems():
631 631 # properties of auth user we dont update
632 632 if k not in ['auth_tokens', 'permissions']:
633 633 setattr(auth_user, k, v)
634 634
635 635 # few extras
636 636 setattr(auth_user, 'feed_token', dbuser.feed_token)
637 637 except Exception:
638 638 log.error(traceback.format_exc())
639 639 auth_user.is_authenticated = False
640 640 return False
641 641
642 642 return True
643 643
644 644 def has_perm(self, user, perm):
645 645 perm = self._get_perm(perm)
646 646 user = self._get_user(user)
647 647
648 648 return UserToPerm.query().filter(UserToPerm.user == user)\
649 649 .filter(UserToPerm.permission == perm).scalar() is not None
650 650
651 651 def grant_perm(self, user, perm):
652 652 """
653 653 Grant user global permissions
654 654
655 655 :param user:
656 656 :param perm:
657 657 """
658 658 user = self._get_user(user)
659 659 perm = self._get_perm(perm)
660 660 # if this permission is already granted skip it
661 661 _perm = UserToPerm.query()\
662 662 .filter(UserToPerm.user == user)\
663 663 .filter(UserToPerm.permission == perm)\
664 664 .scalar()
665 665 if _perm:
666 666 return
667 667 new = UserToPerm()
668 668 new.user = user
669 669 new.permission = perm
670 670 self.sa.add(new)
671 671 return new
672 672
673 673 def revoke_perm(self, user, perm):
674 674 """
675 675 Revoke users global permissions
676 676
677 677 :param user:
678 678 :param perm:
679 679 """
680 680 user = self._get_user(user)
681 681 perm = self._get_perm(perm)
682 682
683 683 obj = UserToPerm.query()\
684 684 .filter(UserToPerm.user == user)\
685 685 .filter(UserToPerm.permission == perm)\
686 686 .scalar()
687 687 if obj:
688 688 self.sa.delete(obj)
689 689
690 690 def add_extra_email(self, user, email):
691 691 """
692 692 Adds email address to UserEmailMap
693 693
694 694 :param user:
695 695 :param email:
696 696 """
697 697 from rhodecode.model import forms
698 698 form = forms.UserExtraEmailForm()()
699 699 data = form.to_python({'email': email})
700 700 user = self._get_user(user)
701 701
702 702 obj = UserEmailMap()
703 703 obj.user = user
704 704 obj.email = data['email']
705 705 self.sa.add(obj)
706 706 return obj
707 707
708 708 def delete_extra_email(self, user, email_id):
709 709 """
710 710 Removes email address from UserEmailMap
711 711
712 712 :param user:
713 713 :param email_id:
714 714 """
715 715 user = self._get_user(user)
716 716 obj = UserEmailMap.query().get(email_id)
717 717 if obj:
718 718 self.sa.delete(obj)
719 719
720 720 def parse_ip_range(self, ip_range):
721 721 ip_list = []
722 722 def make_unique(value):
723 723 seen = []
724 724 return [c for c in value if not (c in seen or seen.append(c))]
725 725
726 726 # firsts split by commas
727 727 for ip_range in ip_range.split(','):
728 728 if not ip_range:
729 729 continue
730 730 ip_range = ip_range.strip()
731 731 if '-' in ip_range:
732 732 start_ip, end_ip = ip_range.split('-', 1)
733 733 start_ip = ipaddress.ip_address(start_ip.strip())
734 734 end_ip = ipaddress.ip_address(end_ip.strip())
735 735 parsed_ip_range = []
736 736
737 737 for index in xrange(int(start_ip), int(end_ip) + 1):
738 738 new_ip = ipaddress.ip_address(index)
739 739 parsed_ip_range.append(str(new_ip))
740 740 ip_list.extend(parsed_ip_range)
741 741 else:
742 742 ip_list.append(ip_range)
743 743
744 744 return make_unique(ip_list)
745 745
746 746 def add_extra_ip(self, user, ip, description=None):
747 747 """
748 748 Adds ip address to UserIpMap
749 749
750 750 :param user:
751 751 :param ip:
752 752 """
753 753 from rhodecode.model import forms
754 754 form = forms.UserExtraIpForm()()
755 755 data = form.to_python({'ip': ip})
756 756 user = self._get_user(user)
757 757
758 758 obj = UserIpMap()
759 759 obj.user = user
760 760 obj.ip_addr = data['ip']
761 761 obj.description = description
762 762 self.sa.add(obj)
763 763 return obj
764 764
765 765 def delete_extra_ip(self, user, ip_id):
766 766 """
767 767 Removes ip address from UserIpMap
768 768
769 769 :param user:
770 770 :param ip_id:
771 771 """
772 772 user = self._get_user(user)
773 773 obj = UserIpMap.query().get(ip_id)
774 774 if obj:
775 775 self.sa.delete(obj)
776 776
777 777 def get_accounts_in_creation_order(self, current_user=None):
778 778 """
779 779 Get accounts in order of creation for deactivation for license limits
780 780
781 781 pick currently logged in user, and append to the list in position 0
782 782 pick all super-admins in order of creation date and add it to the list
783 783 pick all other accounts in order of creation and add it to the list.
784 784
785 785 Based on that list, the last accounts can be disabled as they are
786 786 created at the end and don't include any of the super admins as well
787 787 as the current user.
788 788
789 789 :param current_user: optionally current user running this operation
790 790 """
791 791
792 792 if not current_user:
793 793 current_user = get_current_rhodecode_user()
794 794 active_super_admins = [
795 795 x.user_id for x in User.query()
796 796 .filter(User.user_id != current_user.user_id)
797 797 .filter(User.active == true())
798 798 .filter(User.admin == true())
799 799 .order_by(User.created_on.asc())]
800 800
801 801 active_regular_users = [
802 802 x.user_id for x in User.query()
803 803 .filter(User.user_id != current_user.user_id)
804 804 .filter(User.active == true())
805 805 .filter(User.admin == false())
806 806 .order_by(User.created_on.asc())]
807 807
808 808 list_of_accounts = [current_user.user_id]
809 809 list_of_accounts += active_super_admins
810 810 list_of_accounts += active_regular_users
811 811
812 812 return list_of_accounts
813 813
814 814 def deactivate_last_users(self, expected_users):
815 815 """
816 816 Deactivate accounts that are over the license limits.
817 817 Algorithm of which accounts to disabled is based on the formula:
818 818
819 819 Get current user, then super admins in creation order, then regular
820 820 active users in creation order.
821 821
822 822 Using that list we mark all accounts from the end of it as inactive.
823 823 This way we block only latest created accounts.
824 824
825 825 :param expected_users: list of users in special order, we deactivate
826 826 the end N ammoun of users from that list
827 827 """
828 828
829 829 list_of_accounts = self.get_accounts_in_creation_order()
830 830
831 831 for acc_id in list_of_accounts[expected_users + 1:]:
832 832 user = User.get(acc_id)
833 833 log.info('Deactivating account %s for license unlock', user)
834 834 user.active = False
835 835 Session().add(user)
836 836 Session().commit()
837 837
838 838 return
@@ -1,53 +1,53 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # 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
23 23 from rhodecode.lib import hooks_base, utils2
24 24
25 25
26 26 @mock.patch.multiple(
27 27 hooks_base,
28 28 action_logger=mock.Mock(),
29 29 post_push_extension=mock.Mock(),
30 30 Repository=mock.Mock())
31 def test_post_push_truncates_commits():
31 def test_post_push_truncates_commits(user_regular, repo_stub):
32 32 extras = {
33 33 'ip': '127.0.0.1',
34 'username': 'test',
34 'username': user_regular.username,
35 35 'action': 'push_local',
36 'repository': 'test',
36 'repository': repo_stub.repo_name,
37 37 'scm': 'git',
38 38 'config': '',
39 39 'server_url': 'http://example.com',
40 40 'make_lock': None,
41 41 'locked_by': [None],
42 42 'commit_ids': ['abcde12345' * 4] * 30000,
43 43 }
44 44 extras = utils2.AttributeDict(extras)
45 45
46 46 hooks_base.post_push(extras)
47 47
48 48 # Calculate appropriate action string here
49 49 expected_action = 'push_local:%s' % ','.join(extras.commit_ids[:29000])
50 50
51 51 hooks_base.action_logger.assert_called_with(
52 52 extras.username, expected_action, extras.repository, extras.ip,
53 53 commit=True)
@@ -1,821 +1,826 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # 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 import textwrap
24 24
25 25 import rhodecode
26 26 from rhodecode.lib.utils2 import safe_unicode
27 27 from rhodecode.lib.vcs.backends import get_backend
28 28 from rhodecode.lib.vcs.backends.base import MergeResponse, MergeFailureReason
29 29 from rhodecode.lib.vcs.exceptions import RepositoryError
30 30 from rhodecode.lib.vcs.nodes import FileNode
31 31 from rhodecode.model.comment import ChangesetCommentsModel
32 32 from rhodecode.model.db import PullRequest, Session
33 33 from rhodecode.model.pull_request import PullRequestModel
34 34 from rhodecode.model.user import UserModel
35 35 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
36 36
37 37
38 38 pytestmark = [
39 39 pytest.mark.backends("git", "hg"),
40 40 ]
41 41
42 42
43 43 class TestPullRequestModel:
44 44
45 45 @pytest.fixture
46 46 def pull_request(self, request, backend, pr_util):
47 47 """
48 48 A pull request combined with multiples patches.
49 49 """
50 50 BackendClass = get_backend(backend.alias)
51 51 self.merge_patcher = mock.patch.object(BackendClass, 'merge')
52 52 self.workspace_remove_patcher = mock.patch.object(
53 53 BackendClass, 'cleanup_merge_workspace')
54 54
55 55 self.workspace_remove_mock = self.workspace_remove_patcher.start()
56 56 self.merge_mock = self.merge_patcher.start()
57 57 self.comment_patcher = mock.patch(
58 58 'rhodecode.model.changeset_status.ChangesetStatusModel.set_status')
59 59 self.comment_patcher.start()
60 60 self.notification_patcher = mock.patch(
61 61 'rhodecode.model.notification.NotificationModel.create')
62 62 self.notification_patcher.start()
63 63 self.helper_patcher = mock.patch(
64 64 'rhodecode.lib.helpers.url')
65 65 self.helper_patcher.start()
66 66
67 67 self.hook_patcher = mock.patch.object(PullRequestModel,
68 68 '_trigger_pull_request_hook')
69 69 self.hook_mock = self.hook_patcher.start()
70 70
71 71 self.invalidation_patcher = mock.patch(
72 72 'rhodecode.model.pull_request.ScmModel.mark_for_invalidation')
73 73 self.invalidation_mock = self.invalidation_patcher.start()
74 74
75 75 self.pull_request = pr_util.create_pull_request(
76 76 mergeable=True, name_suffix=u'Δ…Δ‡')
77 77 self.source_commit = self.pull_request.source_ref_parts.commit_id
78 78 self.target_commit = self.pull_request.target_ref_parts.commit_id
79 79 self.workspace_id = 'pr-%s' % self.pull_request.pull_request_id
80 80
81 81 @request.addfinalizer
82 82 def cleanup_pull_request():
83 83 calls = [mock.call(
84 84 self.pull_request, self.pull_request.author, 'create')]
85 85 self.hook_mock.assert_has_calls(calls)
86 86
87 87 self.workspace_remove_patcher.stop()
88 88 self.merge_patcher.stop()
89 89 self.comment_patcher.stop()
90 90 self.notification_patcher.stop()
91 91 self.helper_patcher.stop()
92 92 self.hook_patcher.stop()
93 93 self.invalidation_patcher.stop()
94 94
95 95 return self.pull_request
96 96
97 97 def test_get_all(self, pull_request):
98 98 prs = PullRequestModel().get_all(pull_request.target_repo)
99 99 assert isinstance(prs, list)
100 100 assert len(prs) == 1
101 101
102 102 def test_count_all(self, pull_request):
103 103 pr_count = PullRequestModel().count_all(pull_request.target_repo)
104 104 assert pr_count == 1
105 105
106 106 def test_get_awaiting_review(self, pull_request):
107 107 prs = PullRequestModel().get_awaiting_review(pull_request.target_repo)
108 108 assert isinstance(prs, list)
109 109 assert len(prs) == 1
110 110
111 111 def test_count_awaiting_review(self, pull_request):
112 112 pr_count = PullRequestModel().count_awaiting_review(
113 113 pull_request.target_repo)
114 114 assert pr_count == 1
115 115
116 116 def test_get_awaiting_my_review(self, pull_request):
117 117 PullRequestModel().update_reviewers(
118 118 pull_request, [pull_request.author])
119 119 prs = PullRequestModel().get_awaiting_my_review(
120 120 pull_request.target_repo, user_id=pull_request.author.user_id)
121 121 assert isinstance(prs, list)
122 122 assert len(prs) == 1
123 123
124 124 def test_count_awaiting_my_review(self, pull_request):
125 125 PullRequestModel().update_reviewers(
126 126 pull_request, [pull_request.author])
127 127 pr_count = PullRequestModel().count_awaiting_my_review(
128 128 pull_request.target_repo, user_id=pull_request.author.user_id)
129 129 assert pr_count == 1
130 130
131 131 def test_delete_calls_cleanup_merge(self, pull_request):
132 132 PullRequestModel().delete(pull_request)
133 133
134 134 self.workspace_remove_mock.assert_called_once_with(
135 135 self.workspace_id)
136 136
137 137 def test_close_calls_cleanup_and_hook(self, pull_request):
138 138 PullRequestModel().close_pull_request(
139 139 pull_request, pull_request.author)
140 140
141 141 self.workspace_remove_mock.assert_called_once_with(
142 142 self.workspace_id)
143 143 self.hook_mock.assert_called_with(
144 144 self.pull_request, self.pull_request.author, 'close')
145 145
146 146 def test_merge_status(self, pull_request):
147 147 self.merge_mock.return_value = MergeResponse(
148 148 True, False, None, MergeFailureReason.NONE)
149 149
150 150 assert pull_request._last_merge_source_rev is None
151 151 assert pull_request._last_merge_target_rev is None
152 152 assert pull_request._last_merge_status is None
153 153
154 154 status, msg = PullRequestModel().merge_status(pull_request)
155 155 assert status is True
156 156 assert msg.eval() == 'This pull request can be automatically merged.'
157 157 self.merge_mock.assert_called_once_with(
158 158 pull_request.target_ref_parts,
159 159 pull_request.source_repo.scm_instance(),
160 160 pull_request.source_ref_parts, self.workspace_id, dry_run=True)
161 161
162 162 assert pull_request._last_merge_source_rev == self.source_commit
163 163 assert pull_request._last_merge_target_rev == self.target_commit
164 164 assert pull_request._last_merge_status is MergeFailureReason.NONE
165 165
166 166 self.merge_mock.reset_mock()
167 167 status, msg = PullRequestModel().merge_status(pull_request)
168 168 assert status is True
169 169 assert msg.eval() == 'This pull request can be automatically merged.'
170 170 assert self.merge_mock.called is False
171 171
172 172 def test_merge_status_known_failure(self, pull_request):
173 173 self.merge_mock.return_value = MergeResponse(
174 174 False, False, None, MergeFailureReason.MERGE_FAILED)
175 175
176 176 assert pull_request._last_merge_source_rev is None
177 177 assert pull_request._last_merge_target_rev is None
178 178 assert pull_request._last_merge_status is None
179 179
180 180 status, msg = PullRequestModel().merge_status(pull_request)
181 181 assert status is False
182 182 assert (
183 183 msg.eval() ==
184 184 'This pull request cannot be merged because of conflicts.')
185 185 self.merge_mock.assert_called_once_with(
186 186 pull_request.target_ref_parts,
187 187 pull_request.source_repo.scm_instance(),
188 188 pull_request.source_ref_parts, self.workspace_id, dry_run=True)
189 189
190 190 assert pull_request._last_merge_source_rev == self.source_commit
191 191 assert pull_request._last_merge_target_rev == self.target_commit
192 192 assert (
193 193 pull_request._last_merge_status is MergeFailureReason.MERGE_FAILED)
194 194
195 195 self.merge_mock.reset_mock()
196 196 status, msg = PullRequestModel().merge_status(pull_request)
197 197 assert status is False
198 198 assert (
199 199 msg.eval() ==
200 200 'This pull request cannot be merged because of conflicts.')
201 201 assert self.merge_mock.called is False
202 202
203 203 def test_merge_status_unknown_failure(self, pull_request):
204 204 self.merge_mock.return_value = MergeResponse(
205 205 False, False, None, MergeFailureReason.UNKNOWN)
206 206
207 207 assert pull_request._last_merge_source_rev is None
208 208 assert pull_request._last_merge_target_rev is None
209 209 assert pull_request._last_merge_status is None
210 210
211 211 status, msg = PullRequestModel().merge_status(pull_request)
212 212 assert status is False
213 213 assert msg.eval() == (
214 214 'This pull request cannot be merged because of an unhandled'
215 215 ' exception.')
216 216 self.merge_mock.assert_called_once_with(
217 217 pull_request.target_ref_parts,
218 218 pull_request.source_repo.scm_instance(),
219 219 pull_request.source_ref_parts, self.workspace_id, dry_run=True)
220 220
221 221 assert pull_request._last_merge_source_rev is None
222 222 assert pull_request._last_merge_target_rev is None
223 223 assert pull_request._last_merge_status is None
224 224
225 225 self.merge_mock.reset_mock()
226 226 status, msg = PullRequestModel().merge_status(pull_request)
227 227 assert status is False
228 228 assert msg.eval() == (
229 229 'This pull request cannot be merged because of an unhandled'
230 230 ' exception.')
231 231 assert self.merge_mock.called is True
232 232
233 233 def test_merge_status_when_target_is_locked(self, pull_request):
234 234 pull_request.target_repo.locked = [1, u'12345.50', 'lock_web']
235 235 status, msg = PullRequestModel().merge_status(pull_request)
236 236 assert status is False
237 237 assert msg.eval() == (
238 238 'This pull request cannot be merged because the target repository'
239 239 ' is locked.')
240 240
241 241 def test_merge_status_requirements_check_target(self, pull_request):
242 242
243 243 def has_largefiles(self, repo):
244 244 return repo == pull_request.source_repo
245 245
246 246 patcher = mock.patch.object(
247 247 PullRequestModel, '_has_largefiles', has_largefiles)
248 248 with patcher:
249 249 status, msg = PullRequestModel().merge_status(pull_request)
250 250
251 251 assert status is False
252 252 assert msg == 'Target repository large files support is disabled.'
253 253
254 254 def test_merge_status_requirements_check_source(self, pull_request):
255 255
256 256 def has_largefiles(self, repo):
257 257 return repo == pull_request.target_repo
258 258
259 259 patcher = mock.patch.object(
260 260 PullRequestModel, '_has_largefiles', has_largefiles)
261 261 with patcher:
262 262 status, msg = PullRequestModel().merge_status(pull_request)
263 263
264 264 assert status is False
265 265 assert msg == 'Source repository large files support is disabled.'
266 266
267 267 def test_merge(self, pull_request, merge_extras):
268 268 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
269 269 self.merge_mock.return_value = MergeResponse(
270 270 True, True,
271 271 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
272 272 MergeFailureReason.NONE)
273 273
274 merge_extras['repository'] = pull_request.target_repo.repo_name
274 275 PullRequestModel().merge(
275 276 pull_request, pull_request.author, extras=merge_extras)
276 277
277 278 message = (
278 279 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
279 280 u'\n\n {pr_title}'.format(
280 281 pr_id=pull_request.pull_request_id,
281 282 source_repo=safe_unicode(
282 283 pull_request.source_repo.scm_instance().name),
283 284 source_ref_name=pull_request.source_ref_parts.name,
284 285 pr_title=safe_unicode(pull_request.title)
285 286 )
286 287 )
287 288 self.merge_mock.assert_called_once_with(
288 289 pull_request.target_ref_parts,
289 290 pull_request.source_repo.scm_instance(),
290 291 pull_request.source_ref_parts, self.workspace_id,
291 292 user_name=user.username, user_email=user.email, message=message
292 293 )
293 294 self.invalidation_mock.assert_called_once_with(
294 295 pull_request.target_repo.repo_name)
295 296
296 297 self.hook_mock.assert_called_with(
297 298 self.pull_request, self.pull_request.author, 'merge')
298 299
299 300 pull_request = PullRequest.get(pull_request.pull_request_id)
300 301 assert (
301 302 pull_request.merge_rev ==
302 303 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6')
303 304
304 305 def test_merge_failed(self, pull_request, merge_extras):
305 306 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
306 307 self.merge_mock.return_value = MergeResponse(
307 308 False, False,
308 309 '6126b7bfcc82ad2d3deaee22af926b082ce54cc6',
309 310 MergeFailureReason.MERGE_FAILED)
310 311
312 merge_extras['repository'] = pull_request.target_repo.repo_name
311 313 PullRequestModel().merge(
312 314 pull_request, pull_request.author, extras=merge_extras)
313 315
314 316 message = (
315 317 u'Merge pull request #{pr_id} from {source_repo} {source_ref_name}'
316 318 u'\n\n {pr_title}'.format(
317 319 pr_id=pull_request.pull_request_id,
318 320 source_repo=safe_unicode(
319 321 pull_request.source_repo.scm_instance().name),
320 322 source_ref_name=pull_request.source_ref_parts.name,
321 323 pr_title=safe_unicode(pull_request.title)
322 324 )
323 325 )
324 326 self.merge_mock.assert_called_once_with(
325 327 pull_request.target_ref_parts,
326 328 pull_request.source_repo.scm_instance(),
327 329 pull_request.source_ref_parts, self.workspace_id,
328 330 user_name=user.username, user_email=user.email, message=message
329 331 )
330 332
331 333 pull_request = PullRequest.get(pull_request.pull_request_id)
332 334 assert self.invalidation_mock.called is False
333 335 assert pull_request.merge_rev is None
334 336
335 337 def test_get_commit_ids(self, pull_request):
336 338 # The PR has been not merget yet, so expect an exception
337 339 with pytest.raises(ValueError):
338 340 PullRequestModel()._get_commit_ids(pull_request)
339 341
340 342 # Merge revision is in the revisions list
341 343 pull_request.merge_rev = pull_request.revisions[0]
342 344 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
343 345 assert commit_ids == pull_request.revisions
344 346
345 347 # Merge revision is not in the revisions list
346 348 pull_request.merge_rev = 'f000' * 10
347 349 commit_ids = PullRequestModel()._get_commit_ids(pull_request)
348 350 assert commit_ids == pull_request.revisions + [pull_request.merge_rev]
349 351
350 352 def test_get_diff_from_pr_version(self, pull_request):
351 353 diff = PullRequestModel()._get_diff_from_pr_or_version(
352 354 pull_request, context=6)
353 355 assert 'file_1' in diff.raw
354 356
355 357
356 358 class TestIntegrationMerge(object):
357 359 @pytest.mark.parametrize('extra_config', (
358 360 {'vcs.hooks.protocol': 'http', 'vcs.hooks.direct_calls': False},
359 361 {'vcs.hooks.protocol': 'Pyro4', 'vcs.hooks.direct_calls': False},
360 362 ))
361 363 def test_merge_triggers_push_hooks(
362 364 self, pr_util, user_admin, capture_rcextensions, merge_extras,
363 365 extra_config):
364 366 pull_request = pr_util.create_pull_request(
365 367 approved=True, mergeable=True)
366 368 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
369 merge_extras['repository'] = pull_request.target_repo.repo_name
367 370 Session().commit()
368 371
369 372 with mock.patch.dict(rhodecode.CONFIG, extra_config, clear=False):
370 373 merge_state = PullRequestModel().merge(
371 374 pull_request, user_admin, extras=merge_extras)
372 375
373 376 assert merge_state.executed
374 377 assert 'pre_push' in capture_rcextensions
375 378 assert 'post_push' in capture_rcextensions
376 379
377 380 def test_merge_can_be_rejected_by_pre_push_hook(
378 381 self, pr_util, user_admin, capture_rcextensions, merge_extras):
379 382 pull_request = pr_util.create_pull_request(
380 383 approved=True, mergeable=True)
381 384 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
385 merge_extras['repository'] = pull_request.target_repo.repo_name
382 386 Session().commit()
383 387
384 388 with mock.patch('rhodecode.EXTENSIONS.PRE_PUSH_HOOK') as pre_pull:
385 389 pre_pull.side_effect = RepositoryError("Disallow push!")
386 390 merge_status = PullRequestModel().merge(
387 391 pull_request, user_admin, extras=merge_extras)
388 392
389 393 assert not merge_status.executed
390 394 assert 'pre_push' not in capture_rcextensions
391 395 assert 'post_push' not in capture_rcextensions
392 396
393 397 def test_merge_fails_if_target_is_locked(
394 398 self, pr_util, user_regular, merge_extras):
395 399 pull_request = pr_util.create_pull_request(
396 400 approved=True, mergeable=True)
397 401 locked_by = [user_regular.user_id + 1, 12345.50, 'lock_web']
398 402 pull_request.target_repo.locked = locked_by
399 403 # TODO: johbo: Check if this can work based on the database, currently
400 404 # all data is pre-computed, that's why just updating the DB is not
401 405 # enough.
402 406 merge_extras['locked_by'] = locked_by
407 merge_extras['repository'] = pull_request.target_repo.repo_name
403 408 # TODO: johbo: Needed for sqlite, try to find an automatic way for it
404 409 Session().commit()
405 410 merge_status = PullRequestModel().merge(
406 411 pull_request, user_regular, extras=merge_extras)
407 412 assert not merge_status.executed
408 413
409 414
410 415 @pytest.mark.parametrize('use_outdated, inlines_count, outdated_count', [
411 416 (False, 1, 0),
412 417 (True, 0, 1),
413 418 ])
414 419 def test_outdated_comments(
415 420 pr_util, use_outdated, inlines_count, outdated_count):
416 421 pull_request = pr_util.create_pull_request()
417 422 pr_util.create_inline_comment(file_path='not_in_updated_diff')
418 423
419 424 with outdated_comments_patcher(use_outdated) as outdated_comment_mock:
420 425 pr_util.add_one_commit()
421 426 assert_inline_comments(
422 427 pull_request, visible=inlines_count, outdated=outdated_count)
423 428 outdated_comment_mock.assert_called_with(pull_request)
424 429
425 430
426 431 @pytest.fixture
427 432 def merge_extras(user_regular):
428 433 """
429 434 Context for the vcs operation when running a merge.
430 435 """
431 436 extras = {
432 437 'ip': '127.0.0.1',
433 438 'username': user_regular.username,
434 439 'action': 'push',
435 440 'repository': 'fake_target_repo_name',
436 441 'scm': 'git',
437 442 'config': 'fake_config_ini_path',
438 443 'make_lock': None,
439 444 'locked_by': [None, None, None],
440 445 'server_url': 'http://test.example.com:5000',
441 446 'hooks': ['push', 'pull'],
442 447 }
443 448 return extras
444 449
445 450
446 451 class TestUpdateCommentHandling(object):
447 452
448 453 @pytest.fixture(autouse=True, scope='class')
449 454 def enable_outdated_comments(self, request, pylonsapp):
450 455 config_patch = mock.patch.dict(
451 456 'rhodecode.CONFIG', {'rhodecode_use_outdated_comments': True})
452 457 config_patch.start()
453 458
454 459 @request.addfinalizer
455 460 def cleanup():
456 461 config_patch.stop()
457 462
458 463 def test_comment_stays_unflagged_on_unchanged_diff(self, pr_util):
459 464 commits = [
460 465 {'message': 'a'},
461 466 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
462 467 {'message': 'c', 'added': [FileNode('file_c', 'test_content\n')]},
463 468 ]
464 469 pull_request = pr_util.create_pull_request(
465 470 commits=commits, target_head='a', source_head='b', revisions=['b'])
466 471 pr_util.create_inline_comment(file_path='file_b')
467 472 pr_util.add_one_commit(head='c')
468 473
469 474 assert_inline_comments(pull_request, visible=1, outdated=0)
470 475
471 476 def test_comment_stays_unflagged_on_change_above(self, pr_util):
472 477 original_content = ''.join(
473 478 ['line {}\n'.format(x) for x in range(1, 11)])
474 479 updated_content = 'new_line_at_top\n' + original_content
475 480 commits = [
476 481 {'message': 'a'},
477 482 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
478 483 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
479 484 ]
480 485 pull_request = pr_util.create_pull_request(
481 486 commits=commits, target_head='a', source_head='b', revisions=['b'])
482 487
483 488 with outdated_comments_patcher():
484 489 comment = pr_util.create_inline_comment(
485 490 line_no=u'n8', file_path='file_b')
486 491 pr_util.add_one_commit(head='c')
487 492
488 493 assert_inline_comments(pull_request, visible=1, outdated=0)
489 494 assert comment.line_no == u'n9'
490 495
491 496 def test_comment_stays_unflagged_on_change_below(self, pr_util):
492 497 original_content = ''.join(['line {}\n'.format(x) for x in range(10)])
493 498 updated_content = original_content + 'new_line_at_end\n'
494 499 commits = [
495 500 {'message': 'a'},
496 501 {'message': 'b', 'added': [FileNode('file_b', original_content)]},
497 502 {'message': 'c', 'changed': [FileNode('file_b', updated_content)]},
498 503 ]
499 504 pull_request = pr_util.create_pull_request(
500 505 commits=commits, target_head='a', source_head='b', revisions=['b'])
501 506 pr_util.create_inline_comment(file_path='file_b')
502 507 pr_util.add_one_commit(head='c')
503 508
504 509 assert_inline_comments(pull_request, visible=1, outdated=0)
505 510
506 511 @pytest.mark.parametrize('line_no', ['n4', 'o4', 'n10', 'o9'])
507 512 def test_comment_flagged_on_change_around_context(self, pr_util, line_no):
508 513 base_lines = ['line {}\n'.format(x) for x in range(1, 13)]
509 514 change_lines = list(base_lines)
510 515 change_lines.insert(6, 'line 6a added\n')
511 516
512 517 # Changes on the last line of sight
513 518 update_lines = list(change_lines)
514 519 update_lines[0] = 'line 1 changed\n'
515 520 update_lines[-1] = 'line 12 changed\n'
516 521
517 522 def file_b(lines):
518 523 return FileNode('file_b', ''.join(lines))
519 524
520 525 commits = [
521 526 {'message': 'a', 'added': [file_b(base_lines)]},
522 527 {'message': 'b', 'changed': [file_b(change_lines)]},
523 528 {'message': 'c', 'changed': [file_b(update_lines)]},
524 529 ]
525 530
526 531 pull_request = pr_util.create_pull_request(
527 532 commits=commits, target_head='a', source_head='b', revisions=['b'])
528 533 pr_util.create_inline_comment(line_no=line_no, file_path='file_b')
529 534
530 535 with outdated_comments_patcher():
531 536 pr_util.add_one_commit(head='c')
532 537 assert_inline_comments(pull_request, visible=0, outdated=1)
533 538
534 539 @pytest.mark.parametrize("change, content", [
535 540 ('changed', 'changed\n'),
536 541 ('removed', ''),
537 542 ], ids=['changed', 'removed'])
538 543 def test_comment_flagged_on_change(self, pr_util, change, content):
539 544 commits = [
540 545 {'message': 'a'},
541 546 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
542 547 {'message': 'c', change: [FileNode('file_b', content)]},
543 548 ]
544 549 pull_request = pr_util.create_pull_request(
545 550 commits=commits, target_head='a', source_head='b', revisions=['b'])
546 551 pr_util.create_inline_comment(file_path='file_b')
547 552
548 553 with outdated_comments_patcher():
549 554 pr_util.add_one_commit(head='c')
550 555 assert_inline_comments(pull_request, visible=0, outdated=1)
551 556
552 557
553 558 class TestUpdateChangedFiles(object):
554 559
555 560 def test_no_changes_on_unchanged_diff(self, pr_util):
556 561 commits = [
557 562 {'message': 'a'},
558 563 {'message': 'b',
559 564 'added': [FileNode('file_b', 'test_content b\n')]},
560 565 {'message': 'c',
561 566 'added': [FileNode('file_c', 'test_content c\n')]},
562 567 ]
563 568 # open a PR from a to b, adding file_b
564 569 pull_request = pr_util.create_pull_request(
565 570 commits=commits, target_head='a', source_head='b', revisions=['b'],
566 571 name_suffix='per-file-review')
567 572
568 573 # modify PR adding new file file_c
569 574 pr_util.add_one_commit(head='c')
570 575
571 576 assert_pr_file_changes(
572 577 pull_request,
573 578 added=['file_c'],
574 579 modified=[],
575 580 removed=[])
576 581
577 582 def test_modify_and_undo_modification_diff(self, pr_util):
578 583 commits = [
579 584 {'message': 'a'},
580 585 {'message': 'b',
581 586 'added': [FileNode('file_b', 'test_content b\n')]},
582 587 {'message': 'c',
583 588 'changed': [FileNode('file_b', 'test_content b modified\n')]},
584 589 {'message': 'd',
585 590 'changed': [FileNode('file_b', 'test_content b\n')]},
586 591 ]
587 592 # open a PR from a to b, adding file_b
588 593 pull_request = pr_util.create_pull_request(
589 594 commits=commits, target_head='a', source_head='b', revisions=['b'],
590 595 name_suffix='per-file-review')
591 596
592 597 # modify PR modifying file file_b
593 598 pr_util.add_one_commit(head='c')
594 599
595 600 assert_pr_file_changes(
596 601 pull_request,
597 602 added=[],
598 603 modified=['file_b'],
599 604 removed=[])
600 605
601 606 # move the head again to d, which rollbacks change,
602 607 # meaning we should indicate no changes
603 608 pr_util.add_one_commit(head='d')
604 609
605 610 assert_pr_file_changes(
606 611 pull_request,
607 612 added=[],
608 613 modified=[],
609 614 removed=[])
610 615
611 616 def test_updated_all_files_in_pr(self, pr_util):
612 617 commits = [
613 618 {'message': 'a'},
614 619 {'message': 'b', 'added': [
615 620 FileNode('file_a', 'test_content a\n'),
616 621 FileNode('file_b', 'test_content b\n'),
617 622 FileNode('file_c', 'test_content c\n')]},
618 623 {'message': 'c', 'changed': [
619 624 FileNode('file_a', 'test_content a changed\n'),
620 625 FileNode('file_b', 'test_content b changed\n'),
621 626 FileNode('file_c', 'test_content c changed\n')]},
622 627 ]
623 628 # open a PR from a to b, changing 3 files
624 629 pull_request = pr_util.create_pull_request(
625 630 commits=commits, target_head='a', source_head='b', revisions=['b'],
626 631 name_suffix='per-file-review')
627 632
628 633 pr_util.add_one_commit(head='c')
629 634
630 635 assert_pr_file_changes(
631 636 pull_request,
632 637 added=[],
633 638 modified=['file_a', 'file_b', 'file_c'],
634 639 removed=[])
635 640
636 641 def test_updated_and_removed_all_files_in_pr(self, pr_util):
637 642 commits = [
638 643 {'message': 'a'},
639 644 {'message': 'b', 'added': [
640 645 FileNode('file_a', 'test_content a\n'),
641 646 FileNode('file_b', 'test_content b\n'),
642 647 FileNode('file_c', 'test_content c\n')]},
643 648 {'message': 'c', 'removed': [
644 649 FileNode('file_a', 'test_content a changed\n'),
645 650 FileNode('file_b', 'test_content b changed\n'),
646 651 FileNode('file_c', 'test_content c changed\n')]},
647 652 ]
648 653 # open a PR from a to b, removing 3 files
649 654 pull_request = pr_util.create_pull_request(
650 655 commits=commits, target_head='a', source_head='b', revisions=['b'],
651 656 name_suffix='per-file-review')
652 657
653 658 pr_util.add_one_commit(head='c')
654 659
655 660 assert_pr_file_changes(
656 661 pull_request,
657 662 added=[],
658 663 modified=[],
659 664 removed=['file_a', 'file_b', 'file_c'])
660 665
661 666
662 667 def test_update_writes_snapshot_into_pull_request_version(pr_util):
663 668 model = PullRequestModel()
664 669 pull_request = pr_util.create_pull_request()
665 670 pr_util.update_source_repository()
666 671
667 672 model.update_commits(pull_request)
668 673
669 674 # Expect that it has a version entry now
670 675 assert len(model.get_versions(pull_request)) == 1
671 676
672 677
673 678 def test_update_skips_new_version_if_unchanged(pr_util):
674 679 pull_request = pr_util.create_pull_request()
675 680 model = PullRequestModel()
676 681 model.update_commits(pull_request)
677 682
678 683 # Expect that it still has no versions
679 684 assert len(model.get_versions(pull_request)) == 0
680 685
681 686
682 687 def test_update_assigns_comments_to_the_new_version(pr_util):
683 688 model = PullRequestModel()
684 689 pull_request = pr_util.create_pull_request()
685 690 comment = pr_util.create_comment()
686 691 pr_util.update_source_repository()
687 692
688 693 model.update_commits(pull_request)
689 694
690 695 # Expect that the comment is linked to the pr version now
691 696 assert comment.pull_request_version == model.get_versions(pull_request)[0]
692 697
693 698
694 699 def test_update_adds_a_comment_to_the_pull_request_about_the_change(pr_util):
695 700 model = PullRequestModel()
696 701 pull_request = pr_util.create_pull_request()
697 702 pr_util.update_source_repository()
698 703 pr_util.update_source_repository()
699 704
700 705 model.update_commits(pull_request)
701 706
702 707 # Expect to find a new comment about the change
703 708 expected_message = textwrap.dedent(
704 709 """\
705 710 Auto status change to |under_review|
706 711
707 712 .. role:: added
708 713 .. role:: removed
709 714 .. parsed-literal::
710 715
711 716 Changed commits:
712 717 * :added:`1 added`
713 718 * :removed:`0 removed`
714 719
715 720 Changed files:
716 721 * `A file_2 <#a_c--92ed3b5f07b4>`_
717 722
718 723 .. |under_review| replace:: *"Under Review"*"""
719 724 )
720 725 pull_request_comments = sorted(
721 726 pull_request.comments, key=lambda c: c.modified_at)
722 727 update_comment = pull_request_comments[-1]
723 728 assert update_comment.text == expected_message
724 729
725 730
726 731 def test_create_version_from_snapshot_updates_attributes(pr_util):
727 732 pull_request = pr_util.create_pull_request()
728 733
729 734 # Avoiding default values
730 735 pull_request.status = PullRequest.STATUS_CLOSED
731 736 pull_request._last_merge_source_rev = "0" * 40
732 737 pull_request._last_merge_target_rev = "1" * 40
733 738 pull_request._last_merge_status = 1
734 739 pull_request.merge_rev = "2" * 40
735 740
736 741 # Remember automatic values
737 742 created_on = pull_request.created_on
738 743 updated_on = pull_request.updated_on
739 744
740 745 # Create a new version of the pull request
741 746 version = PullRequestModel()._create_version_from_snapshot(pull_request)
742 747
743 748 # Check attributes
744 749 assert version.title == pr_util.create_parameters['title']
745 750 assert version.description == pr_util.create_parameters['description']
746 751 assert version.status == PullRequest.STATUS_CLOSED
747 752 assert version.created_on == created_on
748 753 assert version.updated_on == updated_on
749 754 assert version.user_id == pull_request.user_id
750 755 assert version.revisions == pr_util.create_parameters['revisions']
751 756 assert version.source_repo == pr_util.source_repository
752 757 assert version.source_ref == pr_util.create_parameters['source_ref']
753 758 assert version.target_repo == pr_util.target_repository
754 759 assert version.target_ref == pr_util.create_parameters['target_ref']
755 760 assert version._last_merge_source_rev == pull_request._last_merge_source_rev
756 761 assert version._last_merge_target_rev == pull_request._last_merge_target_rev
757 762 assert version._last_merge_status == pull_request._last_merge_status
758 763 assert version.merge_rev == pull_request.merge_rev
759 764 assert version.pull_request == pull_request
760 765
761 766
762 767 def test_link_comments_to_version_only_updates_unlinked_comments(pr_util):
763 768 version1 = pr_util.create_version_of_pull_request()
764 769 comment_linked = pr_util.create_comment(linked_to=version1)
765 770 comment_unlinked = pr_util.create_comment()
766 771 version2 = pr_util.create_version_of_pull_request()
767 772
768 773 PullRequestModel()._link_comments_to_version(version2)
769 774
770 775 # Expect that only the new comment is linked to version2
771 776 assert (
772 777 comment_unlinked.pull_request_version_id ==
773 778 version2.pull_request_version_id)
774 779 assert (
775 780 comment_linked.pull_request_version_id ==
776 781 version1.pull_request_version_id)
777 782 assert (
778 783 comment_unlinked.pull_request_version_id !=
779 784 comment_linked.pull_request_version_id)
780 785
781 786
782 787 def test_calculate_commits():
783 788 change = PullRequestModel()._calculate_commit_id_changes(
784 789 set([1, 2, 3]), set([1, 3, 4, 5]))
785 790 assert (set([4, 5]), set([1, 3]), set([2])) == (
786 791 change.added, change.common, change.removed)
787 792
788 793
789 794 def assert_inline_comments(pull_request, visible=None, outdated=None):
790 795 if visible is not None:
791 796 inline_comments = ChangesetCommentsModel().get_inline_comments(
792 797 pull_request.target_repo.repo_id, pull_request=pull_request)
793 798 assert len(inline_comments) == visible
794 799 if outdated is not None:
795 800 outdated_comments = ChangesetCommentsModel().get_outdated_comments(
796 801 pull_request.target_repo.repo_id, pull_request)
797 802 assert len(outdated_comments) == outdated
798 803
799 804
800 805 def assert_pr_file_changes(
801 806 pull_request, added=None, modified=None, removed=None):
802 807 pr_versions = PullRequestModel().get_versions(pull_request)
803 808 # always use first version, ie original PR to calculate changes
804 809 pull_request_version = pr_versions[0]
805 810 old_diff_data, new_diff_data = PullRequestModel()._generate_update_diffs(
806 811 pull_request, pull_request_version)
807 812 file_changes = PullRequestModel()._calculate_file_changes(
808 813 old_diff_data, new_diff_data)
809 814
810 815 assert added == file_changes.added, \
811 816 'expected added:%s vs value:%s' % (added, file_changes.added)
812 817 assert modified == file_changes.modified, \
813 818 'expected modified:%s vs value:%s' % (modified, file_changes.modified)
814 819 assert removed == file_changes.removed, \
815 820 'expected removed:%s vs value:%s' % (removed, file_changes.removed)
816 821
817 822
818 823 def outdated_comments_patcher(use_outdated=True):
819 824 return mock.patch.object(
820 825 ChangesetCommentsModel, 'use_outdated_comments',
821 826 return_value=use_outdated)
General Comments 0
You need to be logged in to leave comments. Login now