##// END OF EJS Templates
branch-permissions: handle vcs operations and branch permissions....
marcink -
r2979:095dcb4b default
parent child Browse files
Show More
@@ -0,0 +1,94 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 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 os
22 import pytest
23
24 from rhodecode.tests import (
25 TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
26 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
27 from rhodecode.tests.vcs_operations import (
28 Command, _check_proper_hg_push, _check_proper_git_push, _add_files_and_push)
29
30
31 @pytest.mark.usefixtures("disable_anonymous_user")
32 class TestVCSOperations(object):
33
34 @pytest.mark.parametrize('username, password', [
35 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
36 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
37 ])
38 @pytest.mark.parametrize('branch_perm', [
39 'branch.none',
40 'branch.merge',
41 'branch.push',
42 'branch.push_force',
43 ])
44 def test_push_to_protected_branch_fails_with_message_hg(
45 self, rc_web_server, tmpdir, branch_perm, user_util,
46 branch_permission_setter, username, password):
47 repo = user_util.create_repo(repo_type='hg')
48 repo_name = repo.repo_name
49 branch_permission_setter(repo_name, username, permission=branch_perm)
50
51 clone_url = rc_web_server.repo_clone_url(
52 repo.repo_name, user=username, passwd=password)
53 Command(os.path.dirname(tmpdir.strpath)).execute(
54 'hg clone', clone_url, tmpdir.strpath)
55
56 stdout, stderr = _add_files_and_push(
57 'hg', tmpdir.strpath, clone_url=clone_url)
58 if branch_perm in ['branch.push', 'branch.push_force']:
59 _check_proper_hg_push(stdout, stderr)
60 else:
61 msg = "Branch `default` changes rejected by rule `*`=>{}".format(branch_perm)
62 assert msg in stdout
63 assert "transaction abort" in stdout
64
65 @pytest.mark.parametrize('username, password', [
66 (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS),
67 (TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS),
68 ])
69 @pytest.mark.parametrize('branch_perm', [
70 'branch.none',
71 'branch.merge',
72 'branch.push',
73 'branch.push_force',
74 ])
75 def test_push_to_protected_branch_fails_with_message_git(
76 self, rc_web_server, tmpdir, branch_perm, user_util,
77 branch_permission_setter, username, password):
78 repo = user_util.create_repo(repo_type='git')
79 repo_name = repo.repo_name
80 branch_permission_setter(repo_name, username, permission=branch_perm)
81
82 clone_url = rc_web_server.repo_clone_url(
83 repo.repo_name, user=username, passwd=password)
84 Command(os.path.dirname(tmpdir.strpath)).execute(
85 'git clone', clone_url, tmpdir.strpath)
86
87 stdout, stderr = _add_files_and_push(
88 'git', tmpdir.strpath, clone_url=clone_url)
89 if branch_perm in ['branch.push', 'branch.push_force']:
90 _check_proper_git_push(stdout, stderr)
91 else:
92 msg = "Branch `master` changes rejected by rule `*`=>{}".format(branch_perm)
93 assert msg in stderr
94 assert "(pre-receive hook declined)" in stderr
@@ -0,0 +1,122 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 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
22 import os
23 import pytest
24
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests.vcs_operations import (
27 Command, _check_proper_hg_push, _check_proper_git_push,
28 _add_files, _add_files_and_push)
29
30
31 @pytest.mark.usefixtures("disable_anonymous_user")
32 class TestVCSOperations(object):
33
34 def test_push_force_hg(self, rc_web_server, tmpdir, user_util):
35 repo = user_util.create_repo(repo_type='hg')
36 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
37 Command(os.path.dirname(tmpdir.strpath)).execute(
38 'hg clone', clone_url, tmpdir.strpath)
39
40 stdout, stderr = _add_files_and_push(
41 'hg', tmpdir.strpath, clone_url=clone_url)
42 _check_proper_hg_push(stdout, stderr)
43
44 # rewrite history, and push with force
45 Command(tmpdir.strpath).execute(
46 'hg checkout -r 1 && hg commit -m "starting new head"')
47 _add_files('hg', tmpdir.strpath, clone_url=clone_url)
48
49 stdout, stderr = Command(tmpdir.strpath).execute(
50 'hg push --verbose -f {}'.format(clone_url))
51
52 _check_proper_hg_push(stdout, stderr)
53
54 def test_push_force_git(self, rc_web_server, tmpdir, user_util):
55 repo = user_util.create_repo(repo_type='git')
56 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
57 Command(os.path.dirname(tmpdir.strpath)).execute(
58 'git clone', clone_url, tmpdir.strpath)
59
60 stdout, stderr = _add_files_and_push(
61 'git', tmpdir.strpath, clone_url=clone_url)
62 _check_proper_git_push(stdout, stderr)
63
64 # rewrite history, and push with force
65 Command(tmpdir.strpath).execute(
66 'git reset --hard HEAD~2')
67 stdout, stderr = Command(tmpdir.strpath).execute(
68 'git push -f {} master'.format(clone_url))
69
70 assert '(forced update)' in stderr
71
72 def test_push_force_hg_blocked_by_branch_permissions(
73 self, rc_web_server, tmpdir, user_util, branch_permission_setter):
74 repo = user_util.create_repo(repo_type='hg')
75 repo_name = repo.repo_name
76 username = TEST_USER_ADMIN_LOGIN
77 branch_permission_setter(repo_name, username, permission='branch.push')
78
79 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
80 Command(os.path.dirname(tmpdir.strpath)).execute(
81 'hg clone', clone_url, tmpdir.strpath)
82
83 stdout, stderr = _add_files_and_push(
84 'hg', tmpdir.strpath, clone_url=clone_url)
85 _check_proper_hg_push(stdout, stderr)
86
87 # rewrite history, and push with force
88 Command(tmpdir.strpath).execute(
89 'hg checkout -r 1 && hg commit -m "starting new head"')
90 _add_files('hg', tmpdir.strpath, clone_url=clone_url)
91
92 stdout, stderr = Command(tmpdir.strpath).execute(
93 'hg push --verbose -f {}'.format(clone_url))
94
95 assert "Branch `default` changes rejected by rule `*`=>branch.push" in stdout
96 assert "FORCE PUSH FORBIDDEN" in stdout
97 assert "transaction abort" in stdout
98
99 def test_push_force_git_blocked_by_branch_permissions(
100 self, rc_web_server, tmpdir, user_util, branch_permission_setter):
101 repo = user_util.create_repo(repo_type='git')
102 repo_name = repo.repo_name
103 username = TEST_USER_ADMIN_LOGIN
104 branch_permission_setter(repo_name, username, permission='branch.push')
105
106 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
107 Command(os.path.dirname(tmpdir.strpath)).execute(
108 'git clone', clone_url, tmpdir.strpath)
109
110 stdout, stderr = _add_files_and_push(
111 'git', tmpdir.strpath, clone_url=clone_url)
112 _check_proper_git_push(stdout, stderr)
113
114 # rewrite history, and push with force
115 Command(tmpdir.strpath).execute(
116 'git reset --hard HEAD~2')
117 stdout, stderr = Command(tmpdir.strpath).execute(
118 'git push -f {} master'.format(clone_url))
119
120 assert "Branch `master` changes rejected by rule `*`=>branch.push" in stderr
121 assert "FORCE PUSH FORBIDDEN" in stderr
122 assert "(pre-receive hook declined)" in stderr
@@ -0,0 +1,115 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2018 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
22 import os
23 import pytest
24
25 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
26 from rhodecode.tests.vcs_operations import (
27 Command, _check_proper_hg_push, _check_proper_git_push, _add_files_and_push)
28
29
30 @pytest.mark.usefixtures("disable_anonymous_user")
31 class TestVCSOperations(object):
32
33 def test_push_new_branch_hg(self, rc_web_server, tmpdir, user_util):
34 repo = user_util.create_repo(repo_type='hg')
35 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
36 Command(os.path.dirname(tmpdir.strpath)).execute(
37 'hg clone', clone_url, tmpdir.strpath)
38
39 stdout, stderr = _add_files_and_push(
40 'hg', tmpdir.strpath, clone_url=clone_url)
41 _check_proper_hg_push(stdout, stderr)
42
43 # start new branch, and push file into it
44 Command(tmpdir.strpath).execute(
45 'hg branch dev && hg commit -m "starting dev branch"')
46 stdout, stderr = _add_files_and_push(
47 'hg', tmpdir.strpath, clone_url=clone_url, target_branch='dev',
48 new_branch=True)
49
50 _check_proper_hg_push(stdout, stderr)
51
52 def test_push_new_branch_git(self, rc_web_server, tmpdir, user_util):
53 repo = user_util.create_repo(repo_type='git')
54 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
55 Command(os.path.dirname(tmpdir.strpath)).execute(
56 'git clone', clone_url, tmpdir.strpath)
57
58 stdout, stderr = _add_files_and_push(
59 'git', tmpdir.strpath, clone_url=clone_url)
60 _check_proper_git_push(stdout, stderr)
61
62 # start new branch, and push file into it
63 Command(tmpdir.strpath).execute('git checkout -b dev')
64 stdout, stderr = _add_files_and_push(
65 'git', tmpdir.strpath, clone_url=clone_url, target_branch='dev',
66 new_branch=True)
67
68 _check_proper_git_push(stdout, stderr, branch='dev')
69
70 def test_push_new_branch_hg_with_branch_permissions_no_force_push(
71 self, rc_web_server, tmpdir, user_util, branch_permission_setter):
72 repo = user_util.create_repo(repo_type='hg')
73 repo_name = repo.repo_name
74 username = TEST_USER_ADMIN_LOGIN
75 branch_permission_setter(repo_name, username, permission='branch.push')
76
77 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
78 Command(os.path.dirname(tmpdir.strpath)).execute(
79 'hg clone', clone_url, tmpdir.strpath)
80
81 stdout, stderr = _add_files_and_push(
82 'hg', tmpdir.strpath, clone_url=clone_url)
83 _check_proper_hg_push(stdout, stderr)
84
85 # start new branch, and push file into it
86 Command(tmpdir.strpath).execute(
87 'hg branch dev && hg commit -m "starting dev branch"')
88 stdout, stderr = _add_files_and_push(
89 'hg', tmpdir.strpath, clone_url=clone_url, target_branch='dev',
90 new_branch=True)
91
92 _check_proper_hg_push(stdout, stderr)
93
94 def test_push_new_branch_git_with_branch_permissions_no_force_push(
95 self, rc_web_server, tmpdir, user_util, branch_permission_setter):
96 repo = user_util.create_repo(repo_type='git')
97 repo_name = repo.repo_name
98 username = TEST_USER_ADMIN_LOGIN
99 branch_permission_setter(repo_name, username, permission='branch.push')
100
101 clone_url = rc_web_server.repo_clone_url(repo.repo_name)
102 Command(os.path.dirname(tmpdir.strpath)).execute(
103 'git clone', clone_url, tmpdir.strpath)
104
105 stdout, stderr = _add_files_and_push(
106 'git', tmpdir.strpath, clone_url=clone_url)
107 _check_proper_git_push(stdout, stderr)
108
109 # start new branch, and push file into it
110 Command(tmpdir.strpath).execute('git checkout -b dev')
111 stdout, stderr = _add_files_and_push(
112 'git', tmpdir.strpath, clone_url=clone_url, target_branch='dev',
113 new_branch=True)
114
115 _check_proper_git_push(stdout, stderr, branch='dev')
@@ -1,2295 +1,2343 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 authentication and permission libraries
23 23 """
24 24
25 25 import os
26 26 import time
27 27 import inspect
28 28 import collections
29 29 import fnmatch
30 30 import hashlib
31 31 import itertools
32 32 import logging
33 33 import random
34 34 import traceback
35 35 from functools import wraps
36 36
37 37 import ipaddress
38 38
39 39 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
40 40 from sqlalchemy.orm.exc import ObjectDeletedError
41 41 from sqlalchemy.orm import joinedload
42 42 from zope.cachedescriptors.property import Lazy as LazyProperty
43 43
44 44 import rhodecode
45 45 from rhodecode.model import meta
46 46 from rhodecode.model.meta import Session
47 47 from rhodecode.model.user import UserModel
48 48 from rhodecode.model.db import (
49 49 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
50 50 UserIpMap, UserApiKeys, RepoGroup, UserGroup)
51 51 from rhodecode.lib import rc_cache
52 52 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
53 53 from rhodecode.lib.utils import (
54 54 get_repo_slug, get_repo_group_slug, get_user_group_slug)
55 55 from rhodecode.lib.caching_query import FromCache
56 56
57 57
58 58 if rhodecode.is_unix:
59 59 import bcrypt
60 60
61 61 log = logging.getLogger(__name__)
62 62
63 63 csrf_token_key = "csrf_token"
64 64
65 65
66 66 class PasswordGenerator(object):
67 67 """
68 68 This is a simple class for generating password from different sets of
69 69 characters
70 70 usage::
71 71
72 72 passwd_gen = PasswordGenerator()
73 73 #print 8-letter password containing only big and small letters
74 74 of alphabet
75 75 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
76 76 """
77 77 ALPHABETS_NUM = r'''1234567890'''
78 78 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
79 79 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
80 80 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
81 81 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
82 82 + ALPHABETS_NUM + ALPHABETS_SPECIAL
83 83 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
84 84 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
85 85 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
86 86 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
87 87
88 88 def __init__(self, passwd=''):
89 89 self.passwd = passwd
90 90
91 91 def gen_password(self, length, type_=None):
92 92 if type_ is None:
93 93 type_ = self.ALPHABETS_FULL
94 94 self.passwd = ''.join([random.choice(type_) for _ in range(length)])
95 95 return self.passwd
96 96
97 97
98 98 class _RhodeCodeCryptoBase(object):
99 99 ENC_PREF = None
100 100
101 101 def hash_create(self, str_):
102 102 """
103 103 hash the string using
104 104
105 105 :param str_: password to hash
106 106 """
107 107 raise NotImplementedError
108 108
109 109 def hash_check_with_upgrade(self, password, hashed):
110 110 """
111 111 Returns tuple in which first element is boolean that states that
112 112 given password matches it's hashed version, and the second is new hash
113 113 of the password, in case this password should be migrated to new
114 114 cipher.
115 115 """
116 116 checked_hash = self.hash_check(password, hashed)
117 117 return checked_hash, None
118 118
119 119 def hash_check(self, password, hashed):
120 120 """
121 121 Checks matching password with it's hashed value.
122 122
123 123 :param password: password
124 124 :param hashed: password in hashed form
125 125 """
126 126 raise NotImplementedError
127 127
128 128 def _assert_bytes(self, value):
129 129 """
130 130 Passing in an `unicode` object can lead to hard to detect issues
131 131 if passwords contain non-ascii characters. Doing a type check
132 132 during runtime, so that such mistakes are detected early on.
133 133 """
134 134 if not isinstance(value, str):
135 135 raise TypeError(
136 136 "Bytestring required as input, got %r." % (value, ))
137 137
138 138
139 139 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
140 140 ENC_PREF = ('$2a$10', '$2b$10')
141 141
142 142 def hash_create(self, str_):
143 143 self._assert_bytes(str_)
144 144 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
145 145
146 146 def hash_check_with_upgrade(self, password, hashed):
147 147 """
148 148 Returns tuple in which first element is boolean that states that
149 149 given password matches it's hashed version, and the second is new hash
150 150 of the password, in case this password should be migrated to new
151 151 cipher.
152 152
153 153 This implements special upgrade logic which works like that:
154 154 - check if the given password == bcrypted hash, if yes then we
155 155 properly used password and it was already in bcrypt. Proceed
156 156 without any changes
157 157 - if bcrypt hash check is not working try with sha256. If hash compare
158 158 is ok, it means we using correct but old hashed password. indicate
159 159 hash change and proceed
160 160 """
161 161
162 162 new_hash = None
163 163
164 164 # regular pw check
165 165 password_match_bcrypt = self.hash_check(password, hashed)
166 166
167 167 # now we want to know if the password was maybe from sha256
168 168 # basically calling _RhodeCodeCryptoSha256().hash_check()
169 169 if not password_match_bcrypt:
170 170 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
171 171 new_hash = self.hash_create(password) # make new bcrypt hash
172 172 password_match_bcrypt = True
173 173
174 174 return password_match_bcrypt, new_hash
175 175
176 176 def hash_check(self, password, hashed):
177 177 """
178 178 Checks matching password with it's hashed value.
179 179
180 180 :param password: password
181 181 :param hashed: password in hashed form
182 182 """
183 183 self._assert_bytes(password)
184 184 try:
185 185 return bcrypt.hashpw(password, hashed) == hashed
186 186 except ValueError as e:
187 187 # we're having a invalid salt here probably, we should not crash
188 188 # just return with False as it would be a wrong password.
189 189 log.debug('Failed to check password hash using bcrypt %s',
190 190 safe_str(e))
191 191
192 192 return False
193 193
194 194
195 195 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
196 196 ENC_PREF = '_'
197 197
198 198 def hash_create(self, str_):
199 199 self._assert_bytes(str_)
200 200 return hashlib.sha256(str_).hexdigest()
201 201
202 202 def hash_check(self, password, hashed):
203 203 """
204 204 Checks matching password with it's hashed value.
205 205
206 206 :param password: password
207 207 :param hashed: password in hashed form
208 208 """
209 209 self._assert_bytes(password)
210 210 return hashlib.sha256(password).hexdigest() == hashed
211 211
212 212
213 213 class _RhodeCodeCryptoTest(_RhodeCodeCryptoBase):
214 214 ENC_PREF = '_'
215 215
216 216 def hash_create(self, str_):
217 217 self._assert_bytes(str_)
218 218 return sha1(str_)
219 219
220 220 def hash_check(self, password, hashed):
221 221 """
222 222 Checks matching password with it's hashed value.
223 223
224 224 :param password: password
225 225 :param hashed: password in hashed form
226 226 """
227 227 self._assert_bytes(password)
228 228 return sha1(password) == hashed
229 229
230 230
231 231 def crypto_backend():
232 232 """
233 233 Return the matching crypto backend.
234 234
235 235 Selection is based on if we run tests or not, we pick sha1-test backend to run
236 236 tests faster since BCRYPT is expensive to calculate
237 237 """
238 238 if rhodecode.is_test:
239 239 RhodeCodeCrypto = _RhodeCodeCryptoTest()
240 240 else:
241 241 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
242 242
243 243 return RhodeCodeCrypto
244 244
245 245
246 246 def get_crypt_password(password):
247 247 """
248 248 Create the hash of `password` with the active crypto backend.
249 249
250 250 :param password: The cleartext password.
251 251 :type password: unicode
252 252 """
253 253 password = safe_str(password)
254 254 return crypto_backend().hash_create(password)
255 255
256 256
257 257 def check_password(password, hashed):
258 258 """
259 259 Check if the value in `password` matches the hash in `hashed`.
260 260
261 261 :param password: The cleartext password.
262 262 :type password: unicode
263 263
264 264 :param hashed: The expected hashed version of the password.
265 265 :type hashed: The hash has to be passed in in text representation.
266 266 """
267 267 password = safe_str(password)
268 268 return crypto_backend().hash_check(password, hashed)
269 269
270 270
271 271 def generate_auth_token(data, salt=None):
272 272 """
273 273 Generates API KEY from given string
274 274 """
275 275
276 276 if salt is None:
277 277 salt = os.urandom(16)
278 278 return hashlib.sha1(safe_str(data) + salt).hexdigest()
279 279
280 280
281 281 def get_came_from(request):
282 282 """
283 283 get query_string+path from request sanitized after removing auth_token
284 284 """
285 285 _req = request
286 286
287 287 path = _req.path
288 288 if 'auth_token' in _req.GET:
289 289 # sanitize the request and remove auth_token for redirection
290 290 _req.GET.pop('auth_token')
291 291 qs = _req.query_string
292 292 if qs:
293 293 path += '?' + qs
294 294
295 295 return path
296 296
297 297
298 298 class CookieStoreWrapper(object):
299 299
300 300 def __init__(self, cookie_store):
301 301 self.cookie_store = cookie_store
302 302
303 303 def __repr__(self):
304 304 return 'CookieStore<%s>' % (self.cookie_store)
305 305
306 306 def get(self, key, other=None):
307 307 if isinstance(self.cookie_store, dict):
308 308 return self.cookie_store.get(key, other)
309 309 elif isinstance(self.cookie_store, AuthUser):
310 310 return self.cookie_store.__dict__.get(key, other)
311 311
312 312
313 313 def _cached_perms_data(user_id, scope, user_is_admin,
314 314 user_inherit_default_permissions, explicit, algo,
315 315 calculate_super_admin):
316 316
317 317 permissions = PermissionCalculator(
318 318 user_id, scope, user_is_admin, user_inherit_default_permissions,
319 319 explicit, algo, calculate_super_admin)
320 320 return permissions.calculate()
321 321
322 322
323 323 class PermOrigin(object):
324 324 SUPER_ADMIN = 'superadmin'
325 325
326 326 REPO_USER = 'user:%s'
327 327 REPO_USERGROUP = 'usergroup:%s'
328 328 REPO_OWNER = 'repo.owner'
329 329 REPO_DEFAULT = 'repo.default'
330 330 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
331 331 REPO_PRIVATE = 'repo.private'
332 332
333 333 REPOGROUP_USER = 'user:%s'
334 334 REPOGROUP_USERGROUP = 'usergroup:%s'
335 335 REPOGROUP_OWNER = 'group.owner'
336 336 REPOGROUP_DEFAULT = 'group.default'
337 337 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
338 338
339 339 USERGROUP_USER = 'user:%s'
340 340 USERGROUP_USERGROUP = 'usergroup:%s'
341 341 USERGROUP_OWNER = 'usergroup.owner'
342 342 USERGROUP_DEFAULT = 'usergroup.default'
343 343 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
344 344
345 345
346 346 class PermOriginDict(dict):
347 347 """
348 348 A special dict used for tracking permissions along with their origins.
349 349
350 350 `__setitem__` has been overridden to expect a tuple(perm, origin)
351 351 `__getitem__` will return only the perm
352 352 `.perm_origin_stack` will return the stack of (perm, origin) set per key
353 353
354 354 >>> perms = PermOriginDict()
355 355 >>> perms['resource'] = 'read', 'default'
356 356 >>> perms['resource']
357 357 'read'
358 358 >>> perms['resource'] = 'write', 'admin'
359 359 >>> perms['resource']
360 360 'write'
361 361 >>> perms.perm_origin_stack
362 362 {'resource': [('read', 'default'), ('write', 'admin')]}
363 363 """
364 364
365 365 def __init__(self, *args, **kw):
366 366 dict.__init__(self, *args, **kw)
367 367 self.perm_origin_stack = collections.OrderedDict()
368 368
369 369 def __setitem__(self, key, (perm, origin)):
370 370 self.perm_origin_stack.setdefault(key, []).append(
371 371 (perm, origin))
372 372 dict.__setitem__(self, key, perm)
373 373
374 374
375 375 class BranchPermOriginDict(PermOriginDict):
376 376 """
377 377 Dedicated branch permissions dict, with tracking of patterns and origins.
378 378
379 379 >>> perms = BranchPermOriginDict()
380 380 >>> perms['resource'] = '*pattern', 'read', 'default'
381 381 >>> perms['resource']
382 382 {'*pattern': 'read'}
383 383 >>> perms['resource'] = '*pattern', 'write', 'admin'
384 384 >>> perms['resource']
385 385 {'*pattern': 'write'}
386 386 >>> perms.perm_origin_stack
387 387 {'resource': {'*pattern': [('read', 'default'), ('write', 'admin')]}}
388 388 """
389 389 def __setitem__(self, key, (pattern, perm, origin)):
390 390
391 391 self.perm_origin_stack.setdefault(key, {}) \
392 392 .setdefault(pattern, []).append((perm, origin))
393 393
394 394 if key in self:
395 395 self[key].__setitem__(pattern, perm)
396 396 else:
397 397 patterns = collections.OrderedDict()
398 398 patterns[pattern] = perm
399 399 dict.__setitem__(self, key, patterns)
400 400
401 401
402 402 class PermissionCalculator(object):
403 403
404 404 def __init__(
405 405 self, user_id, scope, user_is_admin,
406 406 user_inherit_default_permissions, explicit, algo,
407 calculate_super_admin=False):
407 calculate_super_admin_as_user=False):
408 408
409 409 self.user_id = user_id
410 410 self.user_is_admin = user_is_admin
411 411 self.inherit_default_permissions = user_inherit_default_permissions
412 412 self.explicit = explicit
413 413 self.algo = algo
414 self.calculate_super_admin = calculate_super_admin
414 self.calculate_super_admin_as_user = calculate_super_admin_as_user
415 415
416 416 scope = scope or {}
417 417 self.scope_repo_id = scope.get('repo_id')
418 418 self.scope_repo_group_id = scope.get('repo_group_id')
419 419 self.scope_user_group_id = scope.get('user_group_id')
420 420
421 421 self.default_user_id = User.get_default_user(cache=True).user_id
422 422
423 423 self.permissions_repositories = PermOriginDict()
424 424 self.permissions_repository_groups = PermOriginDict()
425 425 self.permissions_user_groups = PermOriginDict()
426 426 self.permissions_repository_branches = BranchPermOriginDict()
427 427 self.permissions_global = set()
428 428
429 429 self.default_repo_perms = Permission.get_default_repo_perms(
430 430 self.default_user_id, self.scope_repo_id)
431 431 self.default_repo_groups_perms = Permission.get_default_group_perms(
432 432 self.default_user_id, self.scope_repo_group_id)
433 433 self.default_user_group_perms = \
434 434 Permission.get_default_user_group_perms(
435 435 self.default_user_id, self.scope_user_group_id)
436 436
437 437 # default branch perms
438 438 self.default_branch_repo_perms = \
439 439 Permission.get_default_repo_branch_perms(
440 440 self.default_user_id, self.scope_repo_id)
441 441
442 442 def calculate(self):
443 if self.user_is_admin and not self.calculate_super_admin:
444 return self._admin_permissions()
443 if self.user_is_admin and not self.calculate_super_admin_as_user:
444 return self._calculate_admin_permissions()
445 445
446 446 self._calculate_global_default_permissions()
447 447 self._calculate_global_permissions()
448 448 self._calculate_default_permissions()
449 449 self._calculate_repository_permissions()
450 450 self._calculate_repository_branch_permissions()
451 451 self._calculate_repository_group_permissions()
452 452 self._calculate_user_group_permissions()
453 453 return self._permission_structure()
454 454
455 def _admin_permissions(self):
455 def _calculate_admin_permissions(self):
456 456 """
457 457 admin user have all default rights for repositories
458 458 and groups set to admin
459 459 """
460 460 self.permissions_global.add('hg.admin')
461 461 self.permissions_global.add('hg.create.write_on_repogroup.true')
462 462
463 463 # repositories
464 464 for perm in self.default_repo_perms:
465 465 r_k = perm.UserRepoToPerm.repository.repo_name
466 466 p = 'repository.admin'
467 467 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN
468 468
469 469 # repository groups
470 470 for perm in self.default_repo_groups_perms:
471 471 rg_k = perm.UserRepoGroupToPerm.group.group_name
472 472 p = 'group.admin'
473 473 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN
474 474
475 475 # user groups
476 476 for perm in self.default_user_group_perms:
477 477 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
478 478 p = 'usergroup.admin'
479 479 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN
480 480
481 481 # branch permissions
482 # TODO(marcink): validate this, especially
483 # how this should work using multiple patterns specified ??
484 # looks ok, but still needs double check !!
485 for perm in self.default_branch_repo_perms:
486 r_k = perm.UserRepoToPerm.repository.repo_name
487 p = 'branch.push_force'
488 self.permissions_repository_branches[r_k] = '*', p, PermOrigin.SUPER_ADMIN
482 # since super-admin also can have custom rule permissions
483 # we *always* need to calculate those inherited from default, and also explicit
484 self._calculate_default_permissions_repository_branches(
485 user_inherit_object_permissions=False)
486 self._calculate_repository_branch_permissions()
489 487
490 488 return self._permission_structure()
491 489
492 490 def _calculate_global_default_permissions(self):
493 491 """
494 492 global permissions taken from the default user
495 493 """
496 494 default_global_perms = UserToPerm.query()\
497 495 .filter(UserToPerm.user_id == self.default_user_id)\
498 496 .options(joinedload(UserToPerm.permission))
499 497
500 498 for perm in default_global_perms:
501 499 self.permissions_global.add(perm.permission.permission_name)
502 500
503 501 if self.user_is_admin:
504 502 self.permissions_global.add('hg.admin')
505 503 self.permissions_global.add('hg.create.write_on_repogroup.true')
506 504
507 505 def _calculate_global_permissions(self):
508 506 """
509 507 Set global system permissions with user permissions or permissions
510 508 taken from the user groups of the current user.
511 509
512 510 The permissions include repo creating, repo group creating, forking
513 511 etc.
514 512 """
515 513
516 514 # now we read the defined permissions and overwrite what we have set
517 515 # before those can be configured from groups or users explicitly.
518 516
519 517 # In case we want to extend this list we should make sure
520 518 # this is in sync with User.DEFAULT_USER_PERMISSIONS definitions
521 519 _configurable = frozenset([
522 520 'hg.fork.none', 'hg.fork.repository',
523 521 'hg.create.none', 'hg.create.repository',
524 522 'hg.usergroup.create.false', 'hg.usergroup.create.true',
525 523 'hg.repogroup.create.false', 'hg.repogroup.create.true',
526 524 'hg.create.write_on_repogroup.false', 'hg.create.write_on_repogroup.true',
527 525 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
528 526 ])
529 527
530 528 # USER GROUPS comes first user group global permissions
531 529 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
532 530 .options(joinedload(UserGroupToPerm.permission))\
533 531 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
534 532 UserGroupMember.users_group_id))\
535 533 .filter(UserGroupMember.user_id == self.user_id)\
536 534 .order_by(UserGroupToPerm.users_group_id)\
537 535 .all()
538 536
539 537 # need to group here by groups since user can be in more than
540 538 # one group, so we get all groups
541 539 _explicit_grouped_perms = [
542 540 [x, list(y)] for x, y in
543 541 itertools.groupby(user_perms_from_users_groups,
544 542 lambda _x: _x.users_group)]
545 543
546 544 for gr, perms in _explicit_grouped_perms:
547 545 # since user can be in multiple groups iterate over them and
548 546 # select the lowest permissions first (more explicit)
549 547 # TODO(marcink): do this^^
550 548
551 549 # group doesn't inherit default permissions so we actually set them
552 550 if not gr.inherit_default_permissions:
553 551 # NEED TO IGNORE all previously set configurable permissions
554 552 # and replace them with explicitly set from this user
555 553 # group permissions
556 554 self.permissions_global = self.permissions_global.difference(
557 555 _configurable)
558 556 for perm in perms:
559 557 self.permissions_global.add(perm.permission.permission_name)
560 558
561 559 # user explicit global permissions
562 560 user_perms = Session().query(UserToPerm)\
563 561 .options(joinedload(UserToPerm.permission))\
564 562 .filter(UserToPerm.user_id == self.user_id).all()
565 563
566 564 if not self.inherit_default_permissions:
567 565 # NEED TO IGNORE all configurable permissions and
568 566 # replace them with explicitly set from this user permissions
569 567 self.permissions_global = self.permissions_global.difference(
570 568 _configurable)
571 569 for perm in user_perms:
572 570 self.permissions_global.add(perm.permission.permission_name)
573 571
574 def _calculate_default_permissions(self):
575 """
576 Set default user permissions for repositories, repository branches,
577 repository groups, user groups taken from the default user.
578
579 Calculate inheritance of object permissions based on what we have now
580 in GLOBAL permissions. We check if .false is in GLOBAL since this is
581 explicitly set. Inherit is the opposite of .false being there.
582
583 .. note::
584
585 the syntax is little bit odd but what we need to check here is
586 the opposite of .false permission being in the list so even for
587 inconsistent state when both .true/.false is there
588 .false is more important
589
590 """
591 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
592 in self.permissions_global)
593
594 # default permissions for repositories, taken from `default` user permissions
572 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
595 573 for perm in self.default_repo_perms:
596 574 r_k = perm.UserRepoToPerm.repository.repo_name
597 575 p = perm.Permission.permission_name
598 576 o = PermOrigin.REPO_DEFAULT
599 577 self.permissions_repositories[r_k] = p, o
600 578
601 579 # if we decide this user isn't inheriting permissions from
602 580 # default user we set him to .none so only explicit
603 581 # permissions work
604 582 if not user_inherit_object_permissions:
605 583 p = 'repository.none'
606 584 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
607 585 self.permissions_repositories[r_k] = p, o
608 586
609 587 if perm.Repository.private and not (
610 588 perm.Repository.user_id == self.user_id):
611 589 # disable defaults for private repos,
612 590 p = 'repository.none'
613 591 o = PermOrigin.REPO_PRIVATE
614 592 self.permissions_repositories[r_k] = p, o
615 593
616 594 elif perm.Repository.user_id == self.user_id:
617 595 # set admin if owner
618 596 p = 'repository.admin'
619 597 o = PermOrigin.REPO_OWNER
620 598 self.permissions_repositories[r_k] = p, o
621 599
622 600 if self.user_is_admin:
623 601 p = 'repository.admin'
624 602 o = PermOrigin.SUPER_ADMIN
625 603 self.permissions_repositories[r_k] = p, o
626 604
627 # default permissions branch for repositories, taken from `default` user permissions
605 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
628 606 for perm in self.default_branch_repo_perms:
629 607
630 608 r_k = perm.UserRepoToPerm.repository.repo_name
631 609 p = perm.Permission.permission_name
632 610 pattern = perm.UserToRepoBranchPermission.branch_pattern
633 611 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
634 612
635 613 if not self.explicit:
636 614 # TODO(marcink): fix this for multiple entries
637 615 cur_perm = self.permissions_repository_branches.get(r_k) or 'branch.none'
638 616 p = self._choose_permission(p, cur_perm)
639 617
640 618 # NOTE(marcink): register all pattern/perm instances in this
641 619 # special dict that aggregates entries
642 620 self.permissions_repository_branches[r_k] = pattern, p, o
643 621
644 # default permissions for repository groups taken from `default` user permission
622 def _calculate_default_permissions_repository_groups(self, user_inherit_object_permissions):
645 623 for perm in self.default_repo_groups_perms:
646 624 rg_k = perm.UserRepoGroupToPerm.group.group_name
647 625 p = perm.Permission.permission_name
648 626 o = PermOrigin.REPOGROUP_DEFAULT
649 627 self.permissions_repository_groups[rg_k] = p, o
650 628
651 629 # if we decide this user isn't inheriting permissions from default
652 630 # user we set him to .none so only explicit permissions work
653 631 if not user_inherit_object_permissions:
654 632 p = 'group.none'
655 633 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
656 634 self.permissions_repository_groups[rg_k] = p, o
657 635
658 636 if perm.RepoGroup.user_id == self.user_id:
659 637 # set admin if owner
660 638 p = 'group.admin'
661 639 o = PermOrigin.REPOGROUP_OWNER
662 640 self.permissions_repository_groups[rg_k] = p, o
663 641
664 642 if self.user_is_admin:
665 643 p = 'group.admin'
666 644 o = PermOrigin.SUPER_ADMIN
667 645 self.permissions_repository_groups[rg_k] = p, o
668 646
669 # default permissions for user groups taken from `default` user permission
647 def _calculate_default_permissions_user_groups(self, user_inherit_object_permissions):
670 648 for perm in self.default_user_group_perms:
671 649 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
672 650 p = perm.Permission.permission_name
673 651 o = PermOrigin.USERGROUP_DEFAULT
674 652 self.permissions_user_groups[u_k] = p, o
675 653
676 654 # if we decide this user isn't inheriting permissions from default
677 655 # user we set him to .none so only explicit permissions work
678 656 if not user_inherit_object_permissions:
679 657 p = 'usergroup.none'
680 658 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
681 659 self.permissions_user_groups[u_k] = p, o
682 660
683 661 if perm.UserGroup.user_id == self.user_id:
684 662 # set admin if owner
685 663 p = 'usergroup.admin'
686 664 o = PermOrigin.USERGROUP_OWNER
687 665 self.permissions_user_groups[u_k] = p, o
688 666
689 667 if self.user_is_admin:
690 668 p = 'usergroup.admin'
691 669 o = PermOrigin.SUPER_ADMIN
692 670 self.permissions_user_groups[u_k] = p, o
693 671
672 def _calculate_default_permissions(self):
673 """
674 Set default user permissions for repositories, repository branches,
675 repository groups, user groups taken from the default user.
676
677 Calculate inheritance of object permissions based on what we have now
678 in GLOBAL permissions. We check if .false is in GLOBAL since this is
679 explicitly set. Inherit is the opposite of .false being there.
680
681 .. note::
682
683 the syntax is little bit odd but what we need to check here is
684 the opposite of .false permission being in the list so even for
685 inconsistent state when both .true/.false is there
686 .false is more important
687
688 """
689 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
690 in self.permissions_global)
691
692 # default permissions inherited from `default` user permissions
693 self._calculate_default_permissions_repositories(
694 user_inherit_object_permissions)
695
696 self._calculate_default_permissions_repository_branches(
697 user_inherit_object_permissions)
698
699 self._calculate_default_permissions_repository_groups(
700 user_inherit_object_permissions)
701
702 self._calculate_default_permissions_user_groups(
703 user_inherit_object_permissions)
704
694 705 def _calculate_repository_permissions(self):
695 706 """
696 707 Repository permissions for the current user.
697 708
698 709 Check if the user is part of user groups for this repository and
699 710 fill in the permission from it. `_choose_permission` decides of which
700 711 permission should be selected based on selected method.
701 712 """
702 713
703 714 # user group for repositories permissions
704 715 user_repo_perms_from_user_group = Permission\
705 716 .get_default_repo_perms_from_user_group(
706 717 self.user_id, self.scope_repo_id)
707 718
708 719 multiple_counter = collections.defaultdict(int)
709 720 for perm in user_repo_perms_from_user_group:
710 721 r_k = perm.UserGroupRepoToPerm.repository.repo_name
711 722 multiple_counter[r_k] += 1
712 723 p = perm.Permission.permission_name
713 724 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
714 725 .users_group.users_group_name
715 726
716 727 if multiple_counter[r_k] > 1:
717 728 cur_perm = self.permissions_repositories[r_k]
718 729 p = self._choose_permission(p, cur_perm)
719 730
720 731 self.permissions_repositories[r_k] = p, o
721 732
722 733 if perm.Repository.user_id == self.user_id:
723 734 # set admin if owner
724 735 p = 'repository.admin'
725 736 o = PermOrigin.REPO_OWNER
726 737 self.permissions_repositories[r_k] = p, o
727 738
728 739 if self.user_is_admin:
729 740 p = 'repository.admin'
730 741 o = PermOrigin.SUPER_ADMIN
731 742 self.permissions_repositories[r_k] = p, o
732 743
733 744 # user explicit permissions for repositories, overrides any specified
734 745 # by the group permission
735 746 user_repo_perms = Permission.get_default_repo_perms(
736 747 self.user_id, self.scope_repo_id)
737 748 for perm in user_repo_perms:
738 749 r_k = perm.UserRepoToPerm.repository.repo_name
739 750 p = perm.Permission.permission_name
740 751 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
741 752
742 753 if not self.explicit:
743 754 cur_perm = self.permissions_repositories.get(
744 755 r_k, 'repository.none')
745 756 p = self._choose_permission(p, cur_perm)
746 757
747 758 self.permissions_repositories[r_k] = p, o
748 759
749 760 if perm.Repository.user_id == self.user_id:
750 761 # set admin if owner
751 762 p = 'repository.admin'
752 763 o = PermOrigin.REPO_OWNER
753 764 self.permissions_repositories[r_k] = p, o
754 765
755 766 if self.user_is_admin:
756 767 p = 'repository.admin'
757 768 o = PermOrigin.SUPER_ADMIN
758 769 self.permissions_repositories[r_k] = p, o
759 770
760 771 def _calculate_repository_branch_permissions(self):
761 772 # user group for repositories permissions
762 773 user_repo_branch_perms_from_user_group = Permission\
763 774 .get_default_repo_branch_perms_from_user_group(
764 775 self.user_id, self.scope_repo_id)
765 776
766 777 multiple_counter = collections.defaultdict(int)
767 778 for perm in user_repo_branch_perms_from_user_group:
768 779 r_k = perm.UserGroupRepoToPerm.repository.repo_name
769 780 p = perm.Permission.permission_name
770 781 pattern = perm.UserGroupToRepoBranchPermission.branch_pattern
771 782 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
772 783 .users_group.users_group_name
773 784
774 785 multiple_counter[r_k] += 1
775 786 if multiple_counter[r_k] > 1:
776 787 # TODO(marcink): fix this for multi branch support, and multiple entries
777 788 cur_perm = self.permissions_repository_branches[r_k]
778 789 p = self._choose_permission(p, cur_perm)
779 790
780 791 self.permissions_repository_branches[r_k] = pattern, p, o
781 792
782 793 # user explicit branch permissions for repositories, overrides
783 794 # any specified by the group permission
784 795 user_repo_branch_perms = Permission.get_default_repo_branch_perms(
785 796 self.user_id, self.scope_repo_id)
797
786 798 for perm in user_repo_branch_perms:
787 799
788 800 r_k = perm.UserRepoToPerm.repository.repo_name
789 801 p = perm.Permission.permission_name
790 802 pattern = perm.UserToRepoBranchPermission.branch_pattern
791 803 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
792 804
793 805 if not self.explicit:
794 806 # TODO(marcink): fix this for multiple entries
795 807 cur_perm = self.permissions_repository_branches.get(r_k) or 'branch.none'
796 808 p = self._choose_permission(p, cur_perm)
797 809
798 810 # NOTE(marcink): register all pattern/perm instances in this
799 811 # special dict that aggregates entries
800 812 self.permissions_repository_branches[r_k] = pattern, p, o
801 813
802
803 814 def _calculate_repository_group_permissions(self):
804 815 """
805 816 Repository group permissions for the current user.
806 817
807 818 Check if the user is part of user groups for repository groups and
808 819 fill in the permissions from it. `_choose_permission` decides of which
809 820 permission should be selected based on selected method.
810 821 """
811 822 # user group for repo groups permissions
812 823 user_repo_group_perms_from_user_group = Permission\
813 824 .get_default_group_perms_from_user_group(
814 825 self.user_id, self.scope_repo_group_id)
815 826
816 827 multiple_counter = collections.defaultdict(int)
817 828 for perm in user_repo_group_perms_from_user_group:
818 829 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
819 830 multiple_counter[rg_k] += 1
820 831 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
821 832 .users_group.users_group_name
822 833 p = perm.Permission.permission_name
823 834
824 835 if multiple_counter[rg_k] > 1:
825 836 cur_perm = self.permissions_repository_groups[rg_k]
826 837 p = self._choose_permission(p, cur_perm)
827 838 self.permissions_repository_groups[rg_k] = p, o
828 839
829 840 if perm.RepoGroup.user_id == self.user_id:
830 841 # set admin if owner, even for member of other user group
831 842 p = 'group.admin'
832 843 o = PermOrigin.REPOGROUP_OWNER
833 844 self.permissions_repository_groups[rg_k] = p, o
834 845
835 846 if self.user_is_admin:
836 847 p = 'group.admin'
837 848 o = PermOrigin.SUPER_ADMIN
838 849 self.permissions_repository_groups[rg_k] = p, o
839 850
840 851 # user explicit permissions for repository groups
841 852 user_repo_groups_perms = Permission.get_default_group_perms(
842 853 self.user_id, self.scope_repo_group_id)
843 854 for perm in user_repo_groups_perms:
844 855 rg_k = perm.UserRepoGroupToPerm.group.group_name
845 856 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
846 857 .user.username
847 858 p = perm.Permission.permission_name
848 859
849 860 if not self.explicit:
850 861 cur_perm = self.permissions_repository_groups.get(
851 862 rg_k, 'group.none')
852 863 p = self._choose_permission(p, cur_perm)
853 864
854 865 self.permissions_repository_groups[rg_k] = p, o
855 866
856 867 if perm.RepoGroup.user_id == self.user_id:
857 868 # set admin if owner
858 869 p = 'group.admin'
859 870 o = PermOrigin.REPOGROUP_OWNER
860 871 self.permissions_repository_groups[rg_k] = p, o
861 872
862 873 if self.user_is_admin:
863 874 p = 'group.admin'
864 875 o = PermOrigin.SUPER_ADMIN
865 876 self.permissions_repository_groups[rg_k] = p, o
866 877
867 878 def _calculate_user_group_permissions(self):
868 879 """
869 880 User group permissions for the current user.
870 881 """
871 882 # user group for user group permissions
872 883 user_group_from_user_group = Permission\
873 884 .get_default_user_group_perms_from_user_group(
874 885 self.user_id, self.scope_user_group_id)
875 886
876 887 multiple_counter = collections.defaultdict(int)
877 888 for perm in user_group_from_user_group:
878 889 ug_k = perm.UserGroupUserGroupToPerm\
879 890 .target_user_group.users_group_name
880 891 multiple_counter[ug_k] += 1
881 892 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
882 893 .user_group.users_group_name
883 894 p = perm.Permission.permission_name
884 895
885 896 if multiple_counter[ug_k] > 1:
886 897 cur_perm = self.permissions_user_groups[ug_k]
887 898 p = self._choose_permission(p, cur_perm)
888 899
889 900 self.permissions_user_groups[ug_k] = p, o
890 901
891 902 if perm.UserGroup.user_id == self.user_id:
892 903 # set admin if owner, even for member of other user group
893 904 p = 'usergroup.admin'
894 905 o = PermOrigin.USERGROUP_OWNER
895 906 self.permissions_user_groups[ug_k] = p, o
896 907
897 908 if self.user_is_admin:
898 909 p = 'usergroup.admin'
899 910 o = PermOrigin.SUPER_ADMIN
900 911 self.permissions_user_groups[ug_k] = p, o
901 912
902 913 # user explicit permission for user groups
903 914 user_user_groups_perms = Permission.get_default_user_group_perms(
904 915 self.user_id, self.scope_user_group_id)
905 916 for perm in user_user_groups_perms:
906 917 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
907 918 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
908 919 .user.username
909 920 p = perm.Permission.permission_name
910 921
911 922 if not self.explicit:
912 923 cur_perm = self.permissions_user_groups.get(
913 924 ug_k, 'usergroup.none')
914 925 p = self._choose_permission(p, cur_perm)
915 926
916 927 self.permissions_user_groups[ug_k] = p, o
917 928
918 929 if perm.UserGroup.user_id == self.user_id:
919 930 # set admin if owner
920 931 p = 'usergroup.admin'
921 932 o = PermOrigin.USERGROUP_OWNER
922 933 self.permissions_user_groups[ug_k] = p, o
923 934
924 935 if self.user_is_admin:
925 936 p = 'usergroup.admin'
926 937 o = PermOrigin.SUPER_ADMIN
927 938 self.permissions_user_groups[ug_k] = p, o
928 939
929 940 def _choose_permission(self, new_perm, cur_perm):
930 941 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
931 942 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
932 943 if self.algo == 'higherwin':
933 944 if new_perm_val > cur_perm_val:
934 945 return new_perm
935 946 return cur_perm
936 947 elif self.algo == 'lowerwin':
937 948 if new_perm_val < cur_perm_val:
938 949 return new_perm
939 950 return cur_perm
940 951
941 952 def _permission_structure(self):
942 953 return {
943 954 'global': self.permissions_global,
944 955 'repositories': self.permissions_repositories,
945 956 'repository_branches': self.permissions_repository_branches,
946 957 'repositories_groups': self.permissions_repository_groups,
947 958 'user_groups': self.permissions_user_groups,
948 959 }
949 960
950 961
951 962 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
952 963 """
953 964 Check if given controller_name is in whitelist of auth token access
954 965 """
955 966 if not whitelist:
956 967 from rhodecode import CONFIG
957 968 whitelist = aslist(
958 969 CONFIG.get('api_access_controllers_whitelist'), sep=',')
959 970 # backward compat translation
960 971 compat = {
961 972 # old controller, new VIEW
962 973 'ChangesetController:*': 'RepoCommitsView:*',
963 974 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
964 975 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
965 976 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
966 977 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
967 978 'GistsController:*': 'GistView:*',
968 979 }
969 980
970 981 log.debug(
971 982 'Allowed views for AUTH TOKEN access: %s' % (whitelist,))
972 983 auth_token_access_valid = False
973 984
974 985 for entry in whitelist:
975 986 token_match = True
976 987 if entry in compat:
977 988 # translate from old Controllers to Pyramid Views
978 989 entry = compat[entry]
979 990
980 991 if '@' in entry:
981 992 # specific AuthToken
982 993 entry, allowed_token = entry.split('@', 1)
983 994 token_match = auth_token == allowed_token
984 995
985 996 if fnmatch.fnmatch(view_name, entry) and token_match:
986 997 auth_token_access_valid = True
987 998 break
988 999
989 1000 if auth_token_access_valid:
990 1001 log.debug('view: `%s` matches entry in whitelist: %s'
991 1002 % (view_name, whitelist))
992 1003 else:
993 1004 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
994 1005 % (view_name, whitelist))
995 1006 if auth_token:
996 1007 # if we use auth token key and don't have access it's a warning
997 1008 log.warning(msg)
998 1009 else:
999 1010 log.debug(msg)
1000 1011
1001 1012 return auth_token_access_valid
1002 1013
1003 1014
1004 1015 class AuthUser(object):
1005 1016 """
1006 1017 A simple object that handles all attributes of user in RhodeCode
1007 1018
1008 1019 It does lookup based on API key,given user, or user present in session
1009 1020 Then it fills all required information for such user. It also checks if
1010 1021 anonymous access is enabled and if so, it returns default user as logged in
1011 1022 """
1012 1023 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
1013 1024
1014 1025 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
1015 1026
1016 1027 self.user_id = user_id
1017 1028 self._api_key = api_key
1018 1029
1019 1030 self.api_key = None
1020 1031 self.username = username
1021 1032 self.ip_addr = ip_addr
1022 1033 self.name = ''
1023 1034 self.lastname = ''
1024 1035 self.first_name = ''
1025 1036 self.last_name = ''
1026 1037 self.email = ''
1027 1038 self.is_authenticated = False
1028 1039 self.admin = False
1029 1040 self.inherit_default_permissions = False
1030 1041 self.password = ''
1031 1042
1032 1043 self.anonymous_user = None # propagated on propagate_data
1033 1044 self.propagate_data()
1034 1045 self._instance = None
1035 1046 self._permissions_scoped_cache = {} # used to bind scoped calculation
1036 1047
1037 1048 @LazyProperty
1038 1049 def permissions(self):
1039 return self.get_perms(user=self, cache=False)
1050 return self.get_perms(user=self, cache=None)
1040 1051
1041 1052 @LazyProperty
1042 1053 def permissions_safe(self):
1043 1054 """
1044 1055 Filtered permissions excluding not allowed repositories
1045 1056 """
1046 perms = self.get_perms(user=self, cache=False)
1057 perms = self.get_perms(user=self, cache=None)
1047 1058
1048 1059 perms['repositories'] = {
1049 1060 k: v for k, v in perms['repositories'].items()
1050 1061 if v != 'repository.none'}
1051 1062 perms['repositories_groups'] = {
1052 1063 k: v for k, v in perms['repositories_groups'].items()
1053 1064 if v != 'group.none'}
1054 1065 perms['user_groups'] = {
1055 1066 k: v for k, v in perms['user_groups'].items()
1056 1067 if v != 'usergroup.none'}
1057 1068 perms['repository_branches'] = {
1058 1069 k: v for k, v in perms['repository_branches'].iteritems()
1059 1070 if v != 'branch.none'}
1060 1071 return perms
1061 1072
1062 1073 @LazyProperty
1063 1074 def permissions_full_details(self):
1064 1075 return self.get_perms(
1065 user=self, cache=False, calculate_super_admin=True)
1076 user=self, cache=None, calculate_super_admin=True)
1066 1077
1067 1078 def permissions_with_scope(self, scope):
1068 1079 """
1069 1080 Call the get_perms function with scoped data. The scope in that function
1070 1081 narrows the SQL calls to the given ID of objects resulting in fetching
1071 1082 Just particular permission we want to obtain. If scope is an empty dict
1072 1083 then it basically narrows the scope to GLOBAL permissions only.
1073 1084
1074 1085 :param scope: dict
1075 1086 """
1076 1087 if 'repo_name' in scope:
1077 1088 obj = Repository.get_by_repo_name(scope['repo_name'])
1078 1089 if obj:
1079 1090 scope['repo_id'] = obj.repo_id
1080 1091 _scope = collections.OrderedDict()
1081 1092 _scope['repo_id'] = -1
1082 1093 _scope['user_group_id'] = -1
1083 1094 _scope['repo_group_id'] = -1
1084 1095
1085 1096 for k in sorted(scope.keys()):
1086 1097 _scope[k] = scope[k]
1087 1098
1088 1099 # store in cache to mimic how the @LazyProperty works,
1089 1100 # the difference here is that we use the unique key calculated
1090 1101 # from params and values
1091 return self.get_perms(user=self, cache=False, scope=_scope)
1102 return self.get_perms(user=self, cache=None, scope=_scope)
1092 1103
1093 1104 def get_instance(self):
1094 1105 return User.get(self.user_id)
1095 1106
1096 1107 def propagate_data(self):
1097 1108 """
1098 1109 Fills in user data and propagates values to this instance. Maps fetched
1099 1110 user attributes to this class instance attributes
1100 1111 """
1101 1112 log.debug('AuthUser: starting data propagation for new potential user')
1102 1113 user_model = UserModel()
1103 1114 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1104 1115 is_user_loaded = False
1105 1116
1106 1117 # lookup by userid
1107 1118 if self.user_id is not None and self.user_id != anon_user.user_id:
1108 1119 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1109 1120 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1110 1121
1111 1122 # try go get user by api key
1112 1123 elif self._api_key and self._api_key != anon_user.api_key:
1113 1124 log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
1114 1125 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1115 1126
1116 1127 # lookup by username
1117 1128 elif self.username:
1118 1129 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1119 1130 is_user_loaded = user_model.fill_data(self, username=self.username)
1120 1131 else:
1121 1132 log.debug('No data in %s that could been used to log in', self)
1122 1133
1123 1134 if not is_user_loaded:
1124 1135 log.debug(
1125 1136 'Failed to load user. Fallback to default user %s', anon_user)
1126 1137 # if we cannot authenticate user try anonymous
1127 1138 if anon_user.active:
1128 1139 log.debug('default user is active, using it as a session user')
1129 1140 user_model.fill_data(self, user_id=anon_user.user_id)
1130 1141 # then we set this user is logged in
1131 1142 self.is_authenticated = True
1132 1143 else:
1133 1144 log.debug('default user is NOT active')
1134 1145 # in case of disabled anonymous user we reset some of the
1135 1146 # parameters so such user is "corrupted", skipping the fill_data
1136 1147 for attr in ['user_id', 'username', 'admin', 'active']:
1137 1148 setattr(self, attr, None)
1138 1149 self.is_authenticated = False
1139 1150
1140 1151 if not self.username:
1141 1152 self.username = 'None'
1142 1153
1143 1154 log.debug('AuthUser: propagated user is now %s', self)
1144 1155
1145 1156 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1146 calculate_super_admin=False, cache=False):
1157 calculate_super_admin=False, cache=None):
1147 1158 """
1148 1159 Fills user permission attribute with permissions taken from database
1149 1160 works for permissions given for repositories, and for permissions that
1150 1161 are granted to groups
1151 1162
1152 1163 :param user: instance of User object from database
1153 1164 :param explicit: In case there are permissions both for user and a group
1154 1165 that user is part of, explicit flag will defiine if user will
1155 1166 explicitly override permissions from group, if it's False it will
1156 1167 make decision based on the algo
1157 1168 :param algo: algorithm to decide what permission should be choose if
1158 1169 it's multiple defined, eg user in two different groups. It also
1159 1170 decides if explicit flag is turned off how to specify the permission
1160 1171 for case when user is in a group + have defined separate permission
1172 :param calculate_super_admin: calculate permissions for super-admin in the
1173 same way as for regular user without speedups
1174 :param cache: Use caching for calculation, None = let the cache backend decide
1161 1175 """
1162 1176 user_id = user.user_id
1163 1177 user_is_admin = user.is_admin
1164 1178
1165 1179 # inheritance of global permissions like create repo/fork repo etc
1166 1180 user_inherit_default_permissions = user.inherit_default_permissions
1167 1181
1168 1182 cache_seconds = safe_int(
1169 1183 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1170 1184
1171 cache_on = cache or cache_seconds > 0
1185 if cache is None:
1186 # let the backend cache decide
1187 cache_on = cache_seconds > 0
1188 else:
1189 cache_on = cache
1190
1172 1191 log.debug(
1173 1192 'Computing PERMISSION tree for user %s scope `%s` '
1174 1193 'with caching: %s[TTL: %ss]' % (user, scope, cache_on, cache_seconds or 0))
1175 1194
1176 1195 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1177 1196 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1178 1197
1179 1198 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
1180 1199 condition=cache_on)
1181 1200 def compute_perm_tree(cache_name,
1182 1201 user_id, scope, user_is_admin,user_inherit_default_permissions,
1183 1202 explicit, algo, calculate_super_admin):
1184 1203 return _cached_perms_data(
1185 1204 user_id, scope, user_is_admin, user_inherit_default_permissions,
1186 1205 explicit, algo, calculate_super_admin)
1187 1206
1188 1207 start = time.time()
1189 result = compute_perm_tree('permissions', user_id, scope, user_is_admin,
1190 user_inherit_default_permissions, explicit, algo,
1191 calculate_super_admin)
1208 result = compute_perm_tree(
1209 'permissions', user_id, scope, user_is_admin,
1210 user_inherit_default_permissions, explicit, algo,
1211 calculate_super_admin)
1192 1212
1193 1213 result_repr = []
1194 1214 for k in result:
1195 1215 result_repr.append((k, len(result[k])))
1196 1216 total = time.time() - start
1197 1217 log.debug('PERMISSION tree for user %s computed in %.3fs: %s' % (
1198 1218 user, total, result_repr))
1199 1219
1200 1220 return result
1201 1221
1202 1222 @property
1203 1223 def is_default(self):
1204 1224 return self.username == User.DEFAULT_USER
1205 1225
1206 1226 @property
1207 1227 def is_admin(self):
1208 1228 return self.admin
1209 1229
1210 1230 @property
1211 1231 def is_user_object(self):
1212 1232 return self.user_id is not None
1213 1233
1214 1234 @property
1215 1235 def repositories_admin(self):
1216 1236 """
1217 1237 Returns list of repositories you're an admin of
1218 1238 """
1219 1239 return [
1220 1240 x[0] for x in self.permissions['repositories'].items()
1221 1241 if x[1] == 'repository.admin']
1222 1242
1223 1243 @property
1224 1244 def repository_groups_admin(self):
1225 1245 """
1226 1246 Returns list of repository groups you're an admin of
1227 1247 """
1228 1248 return [
1229 1249 x[0] for x in self.permissions['repositories_groups'].items()
1230 1250 if x[1] == 'group.admin']
1231 1251
1232 1252 @property
1233 1253 def user_groups_admin(self):
1234 1254 """
1235 1255 Returns list of user groups you're an admin of
1236 1256 """
1237 1257 return [
1238 1258 x[0] for x in self.permissions['user_groups'].items()
1239 1259 if x[1] == 'usergroup.admin']
1240 1260
1241 1261 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1242 1262 """
1243 1263 Returns list of repository ids that user have access to based on given
1244 1264 perms. The cache flag should be only used in cases that are used for
1245 1265 display purposes, NOT IN ANY CASE for permission checks.
1246 1266 """
1247 1267 from rhodecode.model.scm import RepoList
1248 1268 if not perms:
1249 1269 perms = [
1250 1270 'repository.read', 'repository.write', 'repository.admin']
1251 1271
1252 1272 def _cached_repo_acl(user_id, perm_def, _name_filter):
1253 1273 qry = Repository.query()
1254 1274 if _name_filter:
1255 1275 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1256 1276 qry = qry.filter(
1257 1277 Repository.repo_name.ilike(ilike_expression))
1258 1278
1259 1279 return [x.repo_id for x in
1260 1280 RepoList(qry, perm_set=perm_def)]
1261 1281
1262 1282 return _cached_repo_acl(self.user_id, perms, name_filter)
1263 1283
1264 1284 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1265 1285 """
1266 1286 Returns list of repository group ids that user have access to based on given
1267 1287 perms. The cache flag should be only used in cases that are used for
1268 1288 display purposes, NOT IN ANY CASE for permission checks.
1269 1289 """
1270 1290 from rhodecode.model.scm import RepoGroupList
1271 1291 if not perms:
1272 1292 perms = [
1273 1293 'group.read', 'group.write', 'group.admin']
1274 1294
1275 1295 def _cached_repo_group_acl(user_id, perm_def, _name_filter):
1276 1296 qry = RepoGroup.query()
1277 1297 if _name_filter:
1278 1298 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1279 1299 qry = qry.filter(
1280 1300 RepoGroup.group_name.ilike(ilike_expression))
1281 1301
1282 1302 return [x.group_id for x in
1283 1303 RepoGroupList(qry, perm_set=perm_def)]
1284 1304
1285 1305 return _cached_repo_group_acl(self.user_id, perms, name_filter)
1286 1306
1287 1307 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1288 1308 """
1289 1309 Returns list of user group ids that user have access to based on given
1290 1310 perms. The cache flag should be only used in cases that are used for
1291 1311 display purposes, NOT IN ANY CASE for permission checks.
1292 1312 """
1293 1313 from rhodecode.model.scm import UserGroupList
1294 1314 if not perms:
1295 1315 perms = [
1296 1316 'usergroup.read', 'usergroup.write', 'usergroup.admin']
1297 1317
1298 1318 def _cached_user_group_acl(user_id, perm_def, name_filter):
1299 1319 qry = UserGroup.query()
1300 1320 if name_filter:
1301 1321 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1302 1322 qry = qry.filter(
1303 1323 UserGroup.users_group_name.ilike(ilike_expression))
1304 1324
1305 1325 return [x.users_group_id for x in
1306 1326 UserGroupList(qry, perm_set=perm_def)]
1307 1327
1308 1328 return _cached_user_group_acl(self.user_id, perms, name_filter)
1309 1329
1310 1330 @property
1311 1331 def ip_allowed(self):
1312 1332 """
1313 1333 Checks if ip_addr used in constructor is allowed from defined list of
1314 1334 allowed ip_addresses for user
1315 1335
1316 1336 :returns: boolean, True if ip is in allowed ip range
1317 1337 """
1318 1338 # check IP
1319 1339 inherit = self.inherit_default_permissions
1320 1340 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1321 1341 inherit_from_default=inherit)
1322 1342 @property
1323 1343 def personal_repo_group(self):
1324 1344 return RepoGroup.get_user_personal_repo_group(self.user_id)
1325 1345
1326 1346 @LazyProperty
1327 1347 def feed_token(self):
1328 1348 return self.get_instance().feed_token
1329 1349
1330 1350 @classmethod
1331 1351 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1332 1352 allowed_ips = AuthUser.get_allowed_ips(
1333 1353 user_id, cache=True, inherit_from_default=inherit_from_default)
1334 1354 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1335 1355 log.debug('IP:%s for user %s is in range of %s' % (
1336 1356 ip_addr, user_id, allowed_ips))
1337 1357 return True
1338 1358 else:
1339 1359 log.info('Access for IP:%s forbidden for user %s, '
1340 1360 'not in %s' % (ip_addr, user_id, allowed_ips))
1341 1361 return False
1342 1362
1363 def get_branch_permissions(self, repo_name, perms=None):
1364 perms = perms or self.permissions_with_scope({'repo_name': repo_name})
1365 branch_perms = perms.get('repository_branches')
1366 return branch_perms
1367
1368 def get_rule_and_branch_permission(self, repo_name, branch_name):
1369 """
1370 Check if this AuthUser has defined any permissions for branches. If any of
1371 the rules match in order, we return the matching permissions
1372 """
1373
1374 rule = default_perm = ''
1375
1376 branch_perms = self.get_branch_permissions(repo_name=repo_name)
1377 if not branch_perms:
1378 return rule, default_perm
1379
1380 repo_branch_perms = branch_perms.get(repo_name)
1381 if not repo_branch_perms:
1382 return rule, default_perm
1383
1384 # now calculate the permissions
1385 for pattern, branch_perm in repo_branch_perms.items():
1386 if fnmatch.fnmatch(branch_name, pattern):
1387 rule = '`{}`=>{}'.format(pattern, branch_perm)
1388 return rule, branch_perm
1389
1390 return rule, default_perm
1391
1343 1392 def __repr__(self):
1344 1393 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1345 1394 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1346 1395
1347 1396 def set_authenticated(self, authenticated=True):
1348 1397 if self.user_id != self.anonymous_user.user_id:
1349 1398 self.is_authenticated = authenticated
1350 1399
1351 1400 def get_cookie_store(self):
1352 1401 return {
1353 1402 'username': self.username,
1354 1403 'password': md5(self.password or ''),
1355 1404 'user_id': self.user_id,
1356 1405 'is_authenticated': self.is_authenticated
1357 1406 }
1358 1407
1359 1408 @classmethod
1360 1409 def from_cookie_store(cls, cookie_store):
1361 1410 """
1362 1411 Creates AuthUser from a cookie store
1363 1412
1364 1413 :param cls:
1365 1414 :param cookie_store:
1366 1415 """
1367 1416 user_id = cookie_store.get('user_id')
1368 1417 username = cookie_store.get('username')
1369 1418 api_key = cookie_store.get('api_key')
1370 1419 return AuthUser(user_id, api_key, username)
1371 1420
1372 1421 @classmethod
1373 1422 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1374 1423 _set = set()
1375 1424
1376 1425 if inherit_from_default:
1377 1426 def_user_id = User.get_default_user(cache=True).user_id
1378 1427 default_ips = UserIpMap.query().filter(UserIpMap.user_id == def_user_id)
1379 1428 if cache:
1380 1429 default_ips = default_ips.options(
1381 1430 FromCache("sql_cache_short", "get_user_ips_default"))
1382 1431
1383 1432 # populate from default user
1384 1433 for ip in default_ips:
1385 1434 try:
1386 1435 _set.add(ip.ip_addr)
1387 1436 except ObjectDeletedError:
1388 1437 # since we use heavy caching sometimes it happens that
1389 1438 # we get deleted objects here, we just skip them
1390 1439 pass
1391 1440
1392 1441 # NOTE:(marcink) we don't want to load any rules for empty
1393 1442 # user_id which is the case of access of non logged users when anonymous
1394 1443 # access is disabled
1395 1444 user_ips = []
1396 1445 if user_id:
1397 1446 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1398 1447 if cache:
1399 1448 user_ips = user_ips.options(
1400 1449 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1401 1450
1402 1451 for ip in user_ips:
1403 1452 try:
1404 1453 _set.add(ip.ip_addr)
1405 1454 except ObjectDeletedError:
1406 1455 # since we use heavy caching sometimes it happens that we get
1407 1456 # deleted objects here, we just skip them
1408 1457 pass
1409 1458 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1410 1459
1411 1460
1412 1461 def set_available_permissions(settings):
1413 1462 """
1414 1463 This function will propagate pyramid settings with all available defined
1415 1464 permission given in db. We don't want to check each time from db for new
1416 1465 permissions since adding a new permission also requires application restart
1417 1466 ie. to decorate new views with the newly created permission
1418 1467
1419 1468 :param settings: current pyramid registry.settings
1420 1469
1421 1470 """
1422 1471 log.debug('auth: getting information about all available permissions')
1423 1472 try:
1424 1473 sa = meta.Session
1425 1474 all_perms = sa.query(Permission).all()
1426 1475 settings.setdefault('available_permissions',
1427 1476 [x.permission_name for x in all_perms])
1428 1477 log.debug('auth: set available permissions')
1429 1478 except Exception:
1430 1479 log.exception('Failed to fetch permissions from the database.')
1431 1480 raise
1432 1481
1433 1482
1434 1483 def get_csrf_token(session, force_new=False, save_if_missing=True):
1435 1484 """
1436 1485 Return the current authentication token, creating one if one doesn't
1437 1486 already exist and the save_if_missing flag is present.
1438 1487
1439 1488 :param session: pass in the pyramid session, else we use the global ones
1440 1489 :param force_new: force to re-generate the token and store it in session
1441 1490 :param save_if_missing: save the newly generated token if it's missing in
1442 1491 session
1443 1492 """
1444 1493 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1445 1494 # from pyramid.csrf import get_csrf_token
1446 1495
1447 1496 if (csrf_token_key not in session and save_if_missing) or force_new:
1448 1497 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1449 1498 session[csrf_token_key] = token
1450 1499 if hasattr(session, 'save'):
1451 1500 session.save()
1452 1501 return session.get(csrf_token_key)
1453 1502
1454 1503
1455 1504 def get_request(perm_class_instance):
1456 1505 from pyramid.threadlocal import get_current_request
1457 1506 pyramid_request = get_current_request()
1458 1507 return pyramid_request
1459 1508
1460 1509
1461 1510 # CHECK DECORATORS
1462 1511 class CSRFRequired(object):
1463 1512 """
1464 1513 Decorator for authenticating a form
1465 1514
1466 1515 This decorator uses an authorization token stored in the client's
1467 1516 session for prevention of certain Cross-site request forgery (CSRF)
1468 1517 attacks (See
1469 1518 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1470 1519 information).
1471 1520
1472 1521 For use with the ``webhelpers.secure_form`` helper functions.
1473 1522
1474 1523 """
1475 1524 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1476 1525 except_methods=None):
1477 1526 self.token = token
1478 1527 self.header = header
1479 1528 self.except_methods = except_methods or []
1480 1529
1481 1530 def __call__(self, func):
1482 1531 return get_cython_compat_decorator(self.__wrapper, func)
1483 1532
1484 1533 def _get_csrf(self, _request):
1485 1534 return _request.POST.get(self.token, _request.headers.get(self.header))
1486 1535
1487 1536 def check_csrf(self, _request, cur_token):
1488 1537 supplied_token = self._get_csrf(_request)
1489 1538 return supplied_token and supplied_token == cur_token
1490 1539
1491 1540 def _get_request(self):
1492 1541 return get_request(self)
1493 1542
1494 1543 def __wrapper(self, func, *fargs, **fkwargs):
1495 1544 request = self._get_request()
1496 1545
1497 1546 if request.method in self.except_methods:
1498 1547 return func(*fargs, **fkwargs)
1499 1548
1500 1549 cur_token = get_csrf_token(request.session, save_if_missing=False)
1501 1550 if self.check_csrf(request, cur_token):
1502 1551 if request.POST.get(self.token):
1503 1552 del request.POST[self.token]
1504 1553 return func(*fargs, **fkwargs)
1505 1554 else:
1506 1555 reason = 'token-missing'
1507 1556 supplied_token = self._get_csrf(request)
1508 1557 if supplied_token and cur_token != supplied_token:
1509 1558 reason = 'token-mismatch [%s:%s]' % (
1510 1559 cur_token or ''[:6], supplied_token or ''[:6])
1511 1560
1512 1561 csrf_message = \
1513 1562 ("Cross-site request forgery detected, request denied. See "
1514 1563 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1515 1564 "more information.")
1516 1565 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1517 1566 'REMOTE_ADDR:%s, HEADERS:%s' % (
1518 1567 request, reason, request.remote_addr, request.headers))
1519 1568
1520 1569 raise HTTPForbidden(explanation=csrf_message)
1521 1570
1522 1571
1523 1572 class LoginRequired(object):
1524 1573 """
1525 1574 Must be logged in to execute this function else
1526 1575 redirect to login page
1527 1576
1528 1577 :param api_access: if enabled this checks only for valid auth token
1529 1578 and grants access based on valid token
1530 1579 """
1531 1580 def __init__(self, auth_token_access=None):
1532 1581 self.auth_token_access = auth_token_access
1533 1582
1534 1583 def __call__(self, func):
1535 1584 return get_cython_compat_decorator(self.__wrapper, func)
1536 1585
1537 1586 def _get_request(self):
1538 1587 return get_request(self)
1539 1588
1540 1589 def __wrapper(self, func, *fargs, **fkwargs):
1541 1590 from rhodecode.lib import helpers as h
1542 1591 cls = fargs[0]
1543 1592 user = cls._rhodecode_user
1544 1593 request = self._get_request()
1545 1594 _ = request.translate
1546 1595
1547 1596 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1548 1597 log.debug('Starting login restriction checks for user: %s' % (user,))
1549 1598 # check if our IP is allowed
1550 1599 ip_access_valid = True
1551 1600 if not user.ip_allowed:
1552 1601 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1553 1602 category='warning')
1554 1603 ip_access_valid = False
1555 1604
1556 1605 # check if we used an APIKEY and it's a valid one
1557 1606 # defined white-list of controllers which API access will be enabled
1558 1607 _auth_token = request.GET.get(
1559 1608 'auth_token', '') or request.GET.get('api_key', '')
1560 1609 auth_token_access_valid = allowed_auth_token_access(
1561 1610 loc, auth_token=_auth_token)
1562 1611
1563 1612 # explicit controller is enabled or API is in our whitelist
1564 1613 if self.auth_token_access or auth_token_access_valid:
1565 1614 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1566 1615 db_user = user.get_instance()
1567 1616
1568 1617 if db_user:
1569 1618 if self.auth_token_access:
1570 1619 roles = self.auth_token_access
1571 1620 else:
1572 1621 roles = [UserApiKeys.ROLE_HTTP]
1573 1622 token_match = db_user.authenticate_by_token(
1574 1623 _auth_token, roles=roles)
1575 1624 else:
1576 1625 log.debug('Unable to fetch db instance for auth user: %s', user)
1577 1626 token_match = False
1578 1627
1579 1628 if _auth_token and token_match:
1580 1629 auth_token_access_valid = True
1581 1630 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1582 1631 else:
1583 1632 auth_token_access_valid = False
1584 1633 if not _auth_token:
1585 1634 log.debug("AUTH TOKEN *NOT* present in request")
1586 1635 else:
1587 1636 log.warning(
1588 1637 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1589 1638
1590 1639 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1591 1640 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1592 1641 else 'AUTH_TOKEN_AUTH'
1593 1642
1594 1643 if ip_access_valid and (
1595 1644 user.is_authenticated or auth_token_access_valid):
1596 1645 log.info(
1597 1646 'user %s authenticating with:%s IS authenticated on func %s'
1598 1647 % (user, reason, loc))
1599 1648
1600 1649 return func(*fargs, **fkwargs)
1601 1650 else:
1602 1651 log.warning(
1603 1652 'user %s authenticating with:%s NOT authenticated on '
1604 1653 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1605 1654 % (user, reason, loc, ip_access_valid,
1606 1655 auth_token_access_valid))
1607 1656 # we preserve the get PARAM
1608 1657 came_from = get_came_from(request)
1609 1658
1610 1659 log.debug('redirecting to login page with %s' % (came_from,))
1611 1660 raise HTTPFound(
1612 1661 h.route_path('login', _query={'came_from': came_from}))
1613 1662
1614 1663
1615 1664 class NotAnonymous(object):
1616 1665 """
1617 1666 Must be logged in to execute this function else
1618 1667 redirect to login page
1619 1668 """
1620 1669
1621 1670 def __call__(self, func):
1622 1671 return get_cython_compat_decorator(self.__wrapper, func)
1623 1672
1624 1673 def _get_request(self):
1625 1674 return get_request(self)
1626 1675
1627 1676 def __wrapper(self, func, *fargs, **fkwargs):
1628 1677 import rhodecode.lib.helpers as h
1629 1678 cls = fargs[0]
1630 1679 self.user = cls._rhodecode_user
1631 1680 request = self._get_request()
1632 1681 _ = request.translate
1633 1682 log.debug('Checking if user is not anonymous @%s' % cls)
1634 1683
1635 1684 anonymous = self.user.username == User.DEFAULT_USER
1636 1685
1637 1686 if anonymous:
1638 1687 came_from = get_came_from(request)
1639 1688 h.flash(_('You need to be a registered user to '
1640 1689 'perform this action'),
1641 1690 category='warning')
1642 1691 raise HTTPFound(
1643 1692 h.route_path('login', _query={'came_from': came_from}))
1644 1693 else:
1645 1694 return func(*fargs, **fkwargs)
1646 1695
1647 1696
1648 1697 class PermsDecorator(object):
1649 1698 """
1650 1699 Base class for controller decorators, we extract the current user from
1651 1700 the class itself, which has it stored in base controllers
1652 1701 """
1653 1702
1654 1703 def __init__(self, *required_perms):
1655 1704 self.required_perms = set(required_perms)
1656 1705
1657 1706 def __call__(self, func):
1658 1707 return get_cython_compat_decorator(self.__wrapper, func)
1659 1708
1660 1709 def _get_request(self):
1661 1710 return get_request(self)
1662 1711
1663 1712 def __wrapper(self, func, *fargs, **fkwargs):
1664 1713 import rhodecode.lib.helpers as h
1665 1714 cls = fargs[0]
1666 1715 _user = cls._rhodecode_user
1667 1716 request = self._get_request()
1668 1717 _ = request.translate
1669 1718
1670 1719 log.debug('checking %s permissions %s for %s %s',
1671 1720 self.__class__.__name__, self.required_perms, cls, _user)
1672 1721
1673 1722 if self.check_permissions(_user):
1674 1723 log.debug('Permission granted for %s %s', cls, _user)
1675 1724 return func(*fargs, **fkwargs)
1676 1725
1677 1726 else:
1678 1727 log.debug('Permission denied for %s %s', cls, _user)
1679 1728 anonymous = _user.username == User.DEFAULT_USER
1680 1729
1681 1730 if anonymous:
1682 1731 came_from = get_came_from(self._get_request())
1683 1732 h.flash(_('You need to be signed in to view this page'),
1684 1733 category='warning')
1685 1734 raise HTTPFound(
1686 1735 h.route_path('login', _query={'came_from': came_from}))
1687 1736
1688 1737 else:
1689 1738 # redirect with 404 to prevent resource discovery
1690 1739 raise HTTPNotFound()
1691 1740
1692 1741 def check_permissions(self, user):
1693 1742 """Dummy function for overriding"""
1694 1743 raise NotImplementedError(
1695 1744 'You have to write this function in child class')
1696 1745
1697 1746
1698 1747 class HasPermissionAllDecorator(PermsDecorator):
1699 1748 """
1700 1749 Checks for access permission for all given predicates. All of them
1701 1750 have to be meet in order to fulfill the request
1702 1751 """
1703 1752
1704 1753 def check_permissions(self, user):
1705 1754 perms = user.permissions_with_scope({})
1706 1755 if self.required_perms.issubset(perms['global']):
1707 1756 return True
1708 1757 return False
1709 1758
1710 1759
1711 1760 class HasPermissionAnyDecorator(PermsDecorator):
1712 1761 """
1713 1762 Checks for access permission for any of given predicates. In order to
1714 1763 fulfill the request any of predicates must be meet
1715 1764 """
1716 1765
1717 1766 def check_permissions(self, user):
1718 1767 perms = user.permissions_with_scope({})
1719 1768 if self.required_perms.intersection(perms['global']):
1720 1769 return True
1721 1770 return False
1722 1771
1723 1772
1724 1773 class HasRepoPermissionAllDecorator(PermsDecorator):
1725 1774 """
1726 1775 Checks for access permission for all given predicates for specific
1727 1776 repository. All of them have to be meet in order to fulfill the request
1728 1777 """
1729 1778 def _get_repo_name(self):
1730 1779 _request = self._get_request()
1731 1780 return get_repo_slug(_request)
1732 1781
1733 1782 def check_permissions(self, user):
1734 1783 perms = user.permissions
1735 1784 repo_name = self._get_repo_name()
1736 1785
1737 1786 try:
1738 1787 user_perms = {perms['repositories'][repo_name]}
1739 1788 except KeyError:
1740 1789 log.debug('cannot locate repo with name: `%s` in permissions defs',
1741 1790 repo_name)
1742 1791 return False
1743 1792
1744 1793 log.debug('checking `%s` permissions for repo `%s`',
1745 1794 user_perms, repo_name)
1746 1795 if self.required_perms.issubset(user_perms):
1747 1796 return True
1748 1797 return False
1749 1798
1750 1799
1751 1800 class HasRepoPermissionAnyDecorator(PermsDecorator):
1752 1801 """
1753 1802 Checks for access permission for any of given predicates for specific
1754 1803 repository. In order to fulfill the request any of predicates must be meet
1755 1804 """
1756 1805 def _get_repo_name(self):
1757 1806 _request = self._get_request()
1758 1807 return get_repo_slug(_request)
1759 1808
1760 1809 def check_permissions(self, user):
1761 1810 perms = user.permissions
1762 1811 repo_name = self._get_repo_name()
1763 1812
1764 1813 try:
1765 1814 user_perms = {perms['repositories'][repo_name]}
1766 1815 except KeyError:
1767 1816 log.debug(
1768 1817 'cannot locate repo with name: `%s` in permissions defs',
1769 1818 repo_name)
1770 1819 return False
1771 1820
1772 1821 log.debug('checking `%s` permissions for repo `%s`',
1773 1822 user_perms, repo_name)
1774 1823 if self.required_perms.intersection(user_perms):
1775 1824 return True
1776 1825 return False
1777 1826
1778 1827
1779 1828 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1780 1829 """
1781 1830 Checks for access permission for all given predicates for specific
1782 1831 repository group. All of them have to be meet in order to
1783 1832 fulfill the request
1784 1833 """
1785 1834 def _get_repo_group_name(self):
1786 1835 _request = self._get_request()
1787 1836 return get_repo_group_slug(_request)
1788 1837
1789 1838 def check_permissions(self, user):
1790 1839 perms = user.permissions
1791 1840 group_name = self._get_repo_group_name()
1792 1841 try:
1793 1842 user_perms = {perms['repositories_groups'][group_name]}
1794 1843 except KeyError:
1795 1844 log.debug(
1796 1845 'cannot locate repo group with name: `%s` in permissions defs',
1797 1846 group_name)
1798 1847 return False
1799 1848
1800 1849 log.debug('checking `%s` permissions for repo group `%s`',
1801 1850 user_perms, group_name)
1802 1851 if self.required_perms.issubset(user_perms):
1803 1852 return True
1804 1853 return False
1805 1854
1806 1855
1807 1856 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1808 1857 """
1809 1858 Checks for access permission for any of given predicates for specific
1810 1859 repository group. In order to fulfill the request any
1811 1860 of predicates must be met
1812 1861 """
1813 1862 def _get_repo_group_name(self):
1814 1863 _request = self._get_request()
1815 1864 return get_repo_group_slug(_request)
1816 1865
1817 1866 def check_permissions(self, user):
1818 1867 perms = user.permissions
1819 1868 group_name = self._get_repo_group_name()
1820 1869
1821 1870 try:
1822 1871 user_perms = {perms['repositories_groups'][group_name]}
1823 1872 except KeyError:
1824 1873 log.debug(
1825 1874 'cannot locate repo group with name: `%s` in permissions defs',
1826 1875 group_name)
1827 1876 return False
1828 1877
1829 1878 log.debug('checking `%s` permissions for repo group `%s`',
1830 1879 user_perms, group_name)
1831 1880 if self.required_perms.intersection(user_perms):
1832 1881 return True
1833 1882 return False
1834 1883
1835 1884
1836 1885 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1837 1886 """
1838 1887 Checks for access permission for all given predicates for specific
1839 1888 user group. All of them have to be meet in order to fulfill the request
1840 1889 """
1841 1890 def _get_user_group_name(self):
1842 1891 _request = self._get_request()
1843 1892 return get_user_group_slug(_request)
1844 1893
1845 1894 def check_permissions(self, user):
1846 1895 perms = user.permissions
1847 1896 group_name = self._get_user_group_name()
1848 1897 try:
1849 1898 user_perms = {perms['user_groups'][group_name]}
1850 1899 except KeyError:
1851 1900 return False
1852 1901
1853 1902 if self.required_perms.issubset(user_perms):
1854 1903 return True
1855 1904 return False
1856 1905
1857 1906
1858 1907 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1859 1908 """
1860 1909 Checks for access permission for any of given predicates for specific
1861 1910 user group. In order to fulfill the request any of predicates must be meet
1862 1911 """
1863 1912 def _get_user_group_name(self):
1864 1913 _request = self._get_request()
1865 1914 return get_user_group_slug(_request)
1866 1915
1867 1916 def check_permissions(self, user):
1868 1917 perms = user.permissions
1869 1918 group_name = self._get_user_group_name()
1870 1919 try:
1871 1920 user_perms = {perms['user_groups'][group_name]}
1872 1921 except KeyError:
1873 1922 return False
1874 1923
1875 1924 if self.required_perms.intersection(user_perms):
1876 1925 return True
1877 1926 return False
1878 1927
1879 1928
1880 1929 # CHECK FUNCTIONS
1881 1930 class PermsFunction(object):
1882 1931 """Base function for other check functions"""
1883 1932
1884 1933 def __init__(self, *perms):
1885 1934 self.required_perms = set(perms)
1886 1935 self.repo_name = None
1887 1936 self.repo_group_name = None
1888 1937 self.user_group_name = None
1889 1938
1890 1939 def __bool__(self):
1891 1940 frame = inspect.currentframe()
1892 1941 stack_trace = traceback.format_stack(frame)
1893 1942 log.error('Checking bool value on a class instance of perm '
1894 1943 'function is not allowed: %s' % ''.join(stack_trace))
1895 1944 # rather than throwing errors, here we always return False so if by
1896 1945 # accident someone checks truth for just an instance it will always end
1897 1946 # up in returning False
1898 1947 return False
1899 1948 __nonzero__ = __bool__
1900 1949
1901 1950 def __call__(self, check_location='', user=None):
1902 1951 if not user:
1903 1952 log.debug('Using user attribute from global request')
1904 1953 request = self._get_request()
1905 1954 user = request.user
1906 1955
1907 1956 # init auth user if not already given
1908 1957 if not isinstance(user, AuthUser):
1909 1958 log.debug('Wrapping user %s into AuthUser', user)
1910 1959 user = AuthUser(user.user_id)
1911 1960
1912 1961 cls_name = self.__class__.__name__
1913 1962 check_scope = self._get_check_scope(cls_name)
1914 1963 check_location = check_location or 'unspecified location'
1915 1964
1916 1965 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1917 1966 self.required_perms, user, check_scope, check_location)
1918 1967 if not user:
1919 1968 log.warning('Empty user given for permission check')
1920 1969 return False
1921 1970
1922 1971 if self.check_permissions(user):
1923 1972 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1924 1973 check_scope, user, check_location)
1925 1974 return True
1926 1975
1927 1976 else:
1928 1977 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1929 1978 check_scope, user, check_location)
1930 1979 return False
1931 1980
1932 1981 def _get_request(self):
1933 1982 return get_request(self)
1934 1983
1935 1984 def _get_check_scope(self, cls_name):
1936 1985 return {
1937 1986 'HasPermissionAll': 'GLOBAL',
1938 1987 'HasPermissionAny': 'GLOBAL',
1939 1988 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1940 1989 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1941 1990 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1942 1991 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1943 1992 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1944 1993 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1945 1994 }.get(cls_name, '?:%s' % cls_name)
1946 1995
1947 1996 def check_permissions(self, user):
1948 1997 """Dummy function for overriding"""
1949 1998 raise Exception('You have to write this function in child class')
1950 1999
1951 2000
1952 2001 class HasPermissionAll(PermsFunction):
1953 2002 def check_permissions(self, user):
1954 2003 perms = user.permissions_with_scope({})
1955 2004 if self.required_perms.issubset(perms.get('global')):
1956 2005 return True
1957 2006 return False
1958 2007
1959 2008
1960 2009 class HasPermissionAny(PermsFunction):
1961 2010 def check_permissions(self, user):
1962 2011 perms = user.permissions_with_scope({})
1963 2012 if self.required_perms.intersection(perms.get('global')):
1964 2013 return True
1965 2014 return False
1966 2015
1967 2016
1968 2017 class HasRepoPermissionAll(PermsFunction):
1969 2018 def __call__(self, repo_name=None, check_location='', user=None):
1970 2019 self.repo_name = repo_name
1971 2020 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1972 2021
1973 2022 def _get_repo_name(self):
1974 2023 if not self.repo_name:
1975 2024 _request = self._get_request()
1976 2025 self.repo_name = get_repo_slug(_request)
1977 2026 return self.repo_name
1978 2027
1979 2028 def check_permissions(self, user):
1980 2029 self.repo_name = self._get_repo_name()
1981 2030 perms = user.permissions
1982 2031 try:
1983 2032 user_perms = {perms['repositories'][self.repo_name]}
1984 2033 except KeyError:
1985 2034 return False
1986 2035 if self.required_perms.issubset(user_perms):
1987 2036 return True
1988 2037 return False
1989 2038
1990 2039
1991 2040 class HasRepoPermissionAny(PermsFunction):
1992 2041 def __call__(self, repo_name=None, check_location='', user=None):
1993 2042 self.repo_name = repo_name
1994 2043 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1995 2044
1996 2045 def _get_repo_name(self):
1997 2046 if not self.repo_name:
1998 2047 _request = self._get_request()
1999 2048 self.repo_name = get_repo_slug(_request)
2000 2049 return self.repo_name
2001 2050
2002 2051 def check_permissions(self, user):
2003 2052 self.repo_name = self._get_repo_name()
2004 2053 perms = user.permissions
2005 2054 try:
2006 2055 user_perms = {perms['repositories'][self.repo_name]}
2007 2056 except KeyError:
2008 2057 return False
2009 2058 if self.required_perms.intersection(user_perms):
2010 2059 return True
2011 2060 return False
2012 2061
2013 2062
2014 2063 class HasRepoGroupPermissionAny(PermsFunction):
2015 2064 def __call__(self, group_name=None, check_location='', user=None):
2016 2065 self.repo_group_name = group_name
2017 2066 return super(HasRepoGroupPermissionAny, self).__call__(
2018 2067 check_location, user)
2019 2068
2020 2069 def check_permissions(self, user):
2021 2070 perms = user.permissions
2022 2071 try:
2023 2072 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2024 2073 except KeyError:
2025 2074 return False
2026 2075 if self.required_perms.intersection(user_perms):
2027 2076 return True
2028 2077 return False
2029 2078
2030 2079
2031 2080 class HasRepoGroupPermissionAll(PermsFunction):
2032 2081 def __call__(self, group_name=None, check_location='', user=None):
2033 2082 self.repo_group_name = group_name
2034 2083 return super(HasRepoGroupPermissionAll, self).__call__(
2035 2084 check_location, user)
2036 2085
2037 2086 def check_permissions(self, user):
2038 2087 perms = user.permissions
2039 2088 try:
2040 2089 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2041 2090 except KeyError:
2042 2091 return False
2043 2092 if self.required_perms.issubset(user_perms):
2044 2093 return True
2045 2094 return False
2046 2095
2047 2096
2048 2097 class HasUserGroupPermissionAny(PermsFunction):
2049 2098 def __call__(self, user_group_name=None, check_location='', user=None):
2050 2099 self.user_group_name = user_group_name
2051 2100 return super(HasUserGroupPermissionAny, self).__call__(
2052 2101 check_location, user)
2053 2102
2054 2103 def check_permissions(self, user):
2055 2104 perms = user.permissions
2056 2105 try:
2057 2106 user_perms = {perms['user_groups'][self.user_group_name]}
2058 2107 except KeyError:
2059 2108 return False
2060 2109 if self.required_perms.intersection(user_perms):
2061 2110 return True
2062 2111 return False
2063 2112
2064 2113
2065 2114 class HasUserGroupPermissionAll(PermsFunction):
2066 2115 def __call__(self, user_group_name=None, check_location='', user=None):
2067 2116 self.user_group_name = user_group_name
2068 2117 return super(HasUserGroupPermissionAll, self).__call__(
2069 2118 check_location, user)
2070 2119
2071 2120 def check_permissions(self, user):
2072 2121 perms = user.permissions
2073 2122 try:
2074 2123 user_perms = {perms['user_groups'][self.user_group_name]}
2075 2124 except KeyError:
2076 2125 return False
2077 2126 if self.required_perms.issubset(user_perms):
2078 2127 return True
2079 2128 return False
2080 2129
2081 2130
2082 2131 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
2083 2132 class HasPermissionAnyMiddleware(object):
2084 2133 def __init__(self, *perms):
2085 2134 self.required_perms = set(perms)
2086 2135
2087 def __call__(self, user, repo_name):
2136 def __call__(self, auth_user, repo_name):
2088 2137 # repo_name MUST be unicode, since we handle keys in permission
2089 2138 # dict by unicode
2090 2139 repo_name = safe_unicode(repo_name)
2091 user = AuthUser(user.user_id)
2092 2140 log.debug(
2093 2141 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2094 self.required_perms, user, repo_name)
2095
2096 if self.check_permissions(user, repo_name):
2142 self.required_perms, auth_user, repo_name)
2143
2144 if self.check_permissions(auth_user, repo_name):
2097 2145 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2098 repo_name, user, 'PermissionMiddleware')
2146 repo_name, auth_user, 'PermissionMiddleware')
2099 2147 return True
2100 2148
2101 2149 else:
2102 2150 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2103 repo_name, user, 'PermissionMiddleware')
2151 repo_name, auth_user, 'PermissionMiddleware')
2104 2152 return False
2105 2153
2106 2154 def check_permissions(self, user, repo_name):
2107 2155 perms = user.permissions_with_scope({'repo_name': repo_name})
2108 2156
2109 2157 try:
2110 2158 user_perms = {perms['repositories'][repo_name]}
2111 2159 except Exception:
2112 2160 log.exception('Error while accessing user permissions')
2113 2161 return False
2114 2162
2115 2163 if self.required_perms.intersection(user_perms):
2116 2164 return True
2117 2165 return False
2118 2166
2119 2167
2120 2168 # SPECIAL VERSION TO HANDLE API AUTH
2121 2169 class _BaseApiPerm(object):
2122 2170 def __init__(self, *perms):
2123 2171 self.required_perms = set(perms)
2124 2172
2125 2173 def __call__(self, check_location=None, user=None, repo_name=None,
2126 2174 group_name=None, user_group_name=None):
2127 2175 cls_name = self.__class__.__name__
2128 2176 check_scope = 'global:%s' % (self.required_perms,)
2129 2177 if repo_name:
2130 2178 check_scope += ', repo_name:%s' % (repo_name,)
2131 2179
2132 2180 if group_name:
2133 2181 check_scope += ', repo_group_name:%s' % (group_name,)
2134 2182
2135 2183 if user_group_name:
2136 2184 check_scope += ', user_group_name:%s' % (user_group_name,)
2137 2185
2138 2186 log.debug(
2139 2187 'checking cls:%s %s %s @ %s'
2140 2188 % (cls_name, self.required_perms, check_scope, check_location))
2141 2189 if not user:
2142 2190 log.debug('Empty User passed into arguments')
2143 2191 return False
2144 2192
2145 2193 # process user
2146 2194 if not isinstance(user, AuthUser):
2147 2195 user = AuthUser(user.user_id)
2148 2196 if not check_location:
2149 2197 check_location = 'unspecified'
2150 2198 if self.check_permissions(user.permissions, repo_name, group_name,
2151 2199 user_group_name):
2152 2200 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2153 2201 check_scope, user, check_location)
2154 2202 return True
2155 2203
2156 2204 else:
2157 2205 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2158 2206 check_scope, user, check_location)
2159 2207 return False
2160 2208
2161 2209 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2162 2210 user_group_name=None):
2163 2211 """
2164 2212 implement in child class should return True if permissions are ok,
2165 2213 False otherwise
2166 2214
2167 2215 :param perm_defs: dict with permission definitions
2168 2216 :param repo_name: repo name
2169 2217 """
2170 2218 raise NotImplementedError()
2171 2219
2172 2220
2173 2221 class HasPermissionAllApi(_BaseApiPerm):
2174 2222 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2175 2223 user_group_name=None):
2176 2224 if self.required_perms.issubset(perm_defs.get('global')):
2177 2225 return True
2178 2226 return False
2179 2227
2180 2228
2181 2229 class HasPermissionAnyApi(_BaseApiPerm):
2182 2230 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2183 2231 user_group_name=None):
2184 2232 if self.required_perms.intersection(perm_defs.get('global')):
2185 2233 return True
2186 2234 return False
2187 2235
2188 2236
2189 2237 class HasRepoPermissionAllApi(_BaseApiPerm):
2190 2238 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2191 2239 user_group_name=None):
2192 2240 try:
2193 2241 _user_perms = {perm_defs['repositories'][repo_name]}
2194 2242 except KeyError:
2195 2243 log.warning(traceback.format_exc())
2196 2244 return False
2197 2245 if self.required_perms.issubset(_user_perms):
2198 2246 return True
2199 2247 return False
2200 2248
2201 2249
2202 2250 class HasRepoPermissionAnyApi(_BaseApiPerm):
2203 2251 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2204 2252 user_group_name=None):
2205 2253 try:
2206 2254 _user_perms = {perm_defs['repositories'][repo_name]}
2207 2255 except KeyError:
2208 2256 log.warning(traceback.format_exc())
2209 2257 return False
2210 2258 if self.required_perms.intersection(_user_perms):
2211 2259 return True
2212 2260 return False
2213 2261
2214 2262
2215 2263 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2216 2264 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2217 2265 user_group_name=None):
2218 2266 try:
2219 2267 _user_perms = {perm_defs['repositories_groups'][group_name]}
2220 2268 except KeyError:
2221 2269 log.warning(traceback.format_exc())
2222 2270 return False
2223 2271 if self.required_perms.intersection(_user_perms):
2224 2272 return True
2225 2273 return False
2226 2274
2227 2275
2228 2276 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2229 2277 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2230 2278 user_group_name=None):
2231 2279 try:
2232 2280 _user_perms = {perm_defs['repositories_groups'][group_name]}
2233 2281 except KeyError:
2234 2282 log.warning(traceback.format_exc())
2235 2283 return False
2236 2284 if self.required_perms.issubset(_user_perms):
2237 2285 return True
2238 2286 return False
2239 2287
2240 2288
2241 2289 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2242 2290 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2243 2291 user_group_name=None):
2244 2292 try:
2245 2293 _user_perms = {perm_defs['user_groups'][user_group_name]}
2246 2294 except KeyError:
2247 2295 log.warning(traceback.format_exc())
2248 2296 return False
2249 2297 if self.required_perms.intersection(_user_perms):
2250 2298 return True
2251 2299 return False
2252 2300
2253 2301
2254 2302 def check_ip_access(source_ip, allowed_ips=None):
2255 2303 """
2256 2304 Checks if source_ip is a subnet of any of allowed_ips.
2257 2305
2258 2306 :param source_ip:
2259 2307 :param allowed_ips: list of allowed ips together with mask
2260 2308 """
2261 2309 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
2262 2310 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2263 2311 if isinstance(allowed_ips, (tuple, list, set)):
2264 2312 for ip in allowed_ips:
2265 2313 ip = safe_unicode(ip)
2266 2314 try:
2267 2315 network_address = ipaddress.ip_network(ip, strict=False)
2268 2316 if source_ip_address in network_address:
2269 2317 log.debug('IP %s is network %s' %
2270 2318 (source_ip_address, network_address))
2271 2319 return True
2272 2320 # for any case we cannot determine the IP, don't crash just
2273 2321 # skip it and log as error, we want to say forbidden still when
2274 2322 # sending bad IP
2275 2323 except Exception:
2276 2324 log.error(traceback.format_exc())
2277 2325 continue
2278 2326 return False
2279 2327
2280 2328
2281 2329 def get_cython_compat_decorator(wrapper, func):
2282 2330 """
2283 2331 Creates a cython compatible decorator. The previously used
2284 2332 decorator.decorator() function seems to be incompatible with cython.
2285 2333
2286 2334 :param wrapper: __wrapper method of the decorator class
2287 2335 :param func: decorated function
2288 2336 """
2289 2337 @wraps(func)
2290 2338 def local_wrapper(*args, **kwds):
2291 2339 return wrapper(func, *args, **kwds)
2292 2340 local_wrapper.__wrapped__ = func
2293 2341 return local_wrapper
2294 2342
2295 2343
@@ -1,546 +1,548 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 38 from rhodecode.authentication.base import VCS_TYPE
39 39 from rhodecode.lib import auth, utils2
40 40 from rhodecode.lib import helpers as h
41 41 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
42 42 from rhodecode.lib.exceptions import UserCreationError
43 43 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
44 44 from rhodecode.lib.utils2 import (
45 45 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
46 46 from rhodecode.model.db import Repository, User, ChangesetComment
47 47 from rhodecode.model.notification import NotificationModel
48 48 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 def _filter_proxy(ip):
54 54 """
55 55 Passed in IP addresses in HEADERS can be in a special format of multiple
56 56 ips. Those comma separated IPs are passed from various proxies in the
57 57 chain of request processing. The left-most being the original client.
58 58 We only care about the first IP which came from the org. client.
59 59
60 60 :param ip: ip string from headers
61 61 """
62 62 if ',' in ip:
63 63 _ips = ip.split(',')
64 64 _first_ip = _ips[0].strip()
65 65 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
66 66 return _first_ip
67 67 return ip
68 68
69 69
70 70 def _filter_port(ip):
71 71 """
72 72 Removes a port from ip, there are 4 main cases to handle here.
73 73 - ipv4 eg. 127.0.0.1
74 74 - ipv6 eg. ::1
75 75 - ipv4+port eg. 127.0.0.1:8080
76 76 - ipv6+port eg. [::1]:8080
77 77
78 78 :param ip:
79 79 """
80 80 def is_ipv6(ip_addr):
81 81 if hasattr(socket, 'inet_pton'):
82 82 try:
83 83 socket.inet_pton(socket.AF_INET6, ip_addr)
84 84 except socket.error:
85 85 return False
86 86 else:
87 87 # fallback to ipaddress
88 88 try:
89 89 ipaddress.IPv6Address(safe_unicode(ip_addr))
90 90 except Exception:
91 91 return False
92 92 return True
93 93
94 94 if ':' not in ip: # must be ipv4 pure ip
95 95 return ip
96 96
97 97 if '[' in ip and ']' in ip: # ipv6 with port
98 98 return ip.split(']')[0][1:].lower()
99 99
100 100 # must be ipv6 or ipv4 with port
101 101 if is_ipv6(ip):
102 102 return ip
103 103 else:
104 104 ip, _port = ip.split(':')[:2] # means ipv4+port
105 105 return ip
106 106
107 107
108 108 def get_ip_addr(environ):
109 109 proxy_key = 'HTTP_X_REAL_IP'
110 110 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
111 111 def_key = 'REMOTE_ADDR'
112 112 _filters = lambda x: _filter_port(_filter_proxy(x))
113 113
114 114 ip = environ.get(proxy_key)
115 115 if ip:
116 116 return _filters(ip)
117 117
118 118 ip = environ.get(proxy_key2)
119 119 if ip:
120 120 return _filters(ip)
121 121
122 122 ip = environ.get(def_key, '0.0.0.0')
123 123 return _filters(ip)
124 124
125 125
126 126 def get_server_ip_addr(environ, log_errors=True):
127 127 hostname = environ.get('SERVER_NAME')
128 128 try:
129 129 return socket.gethostbyname(hostname)
130 130 except Exception as e:
131 131 if log_errors:
132 132 # in some cases this lookup is not possible, and we don't want to
133 133 # make it an exception in logs
134 134 log.exception('Could not retrieve server ip address: %s', e)
135 135 return hostname
136 136
137 137
138 138 def get_server_port(environ):
139 139 return environ.get('SERVER_PORT')
140 140
141 141
142 142 def get_access_path(environ):
143 143 path = environ.get('PATH_INFO')
144 144 org_req = environ.get('pylons.original_request')
145 145 if org_req:
146 146 path = org_req.environ.get('PATH_INFO')
147 147 return path
148 148
149 149
150 150 def get_user_agent(environ):
151 151 return environ.get('HTTP_USER_AGENT')
152 152
153 153
154 154 def vcs_operation_context(
155 155 environ, repo_name, username, action, scm, check_locking=True,
156 is_shadow_repo=False):
156 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
157 157 """
158 158 Generate the context for a vcs operation, e.g. push or pull.
159 159
160 160 This context is passed over the layers so that hooks triggered by the
161 161 vcs operation know details like the user, the user's IP address etc.
162 162
163 163 :param check_locking: Allows to switch of the computation of the locking
164 164 data. This serves mainly the need of the simplevcs middleware to be
165 165 able to disable this for certain operations.
166 166
167 167 """
168 168 # Tri-state value: False: unlock, None: nothing, True: lock
169 169 make_lock = None
170 170 locked_by = [None, None, None]
171 171 is_anonymous = username == User.DEFAULT_USER
172 172 user = User.get_by_username(username)
173 173 if not is_anonymous and check_locking:
174 174 log.debug('Checking locking on repository "%s"', repo_name)
175 175 repo = Repository.get_by_repo_name(repo_name)
176 176 make_lock, __, locked_by = repo.get_locking_state(
177 177 action, user.user_id)
178 178 user_id = user.user_id
179 179 settings_model = VcsSettingsModel(repo=repo_name)
180 180 ui_settings = settings_model.get_ui_settings()
181 181
182 182 extras = {
183 183 'ip': get_ip_addr(environ),
184 184 'username': username,
185 185 'user_id': user_id,
186 186 'action': action,
187 187 'repository': repo_name,
188 188 'scm': scm,
189 189 'config': rhodecode.CONFIG['__file__'],
190 190 'make_lock': make_lock,
191 191 'locked_by': locked_by,
192 192 'server_url': utils2.get_server_url(environ),
193 193 'user_agent': get_user_agent(environ),
194 194 'hooks': get_enabled_hook_classes(ui_settings),
195 195 'is_shadow_repo': is_shadow_repo,
196 'detect_force_push': detect_force_push,
197 'check_branch_perms': check_branch_perms,
196 198 }
197 199 return extras
198 200
199 201
200 202 class BasicAuth(AuthBasicAuthenticator):
201 203
202 204 def __init__(self, realm, authfunc, registry, auth_http_code=None,
203 205 initial_call_detection=False, acl_repo_name=None):
204 206 self.realm = realm
205 207 self.initial_call = initial_call_detection
206 208 self.authfunc = authfunc
207 209 self.registry = registry
208 210 self.acl_repo_name = acl_repo_name
209 211 self._rc_auth_http_code = auth_http_code
210 212
211 213 def _get_response_from_code(self, http_code):
212 214 try:
213 215 return get_exception(safe_int(http_code))
214 216 except Exception:
215 217 log.exception('Failed to fetch response for code %s' % http_code)
216 218 return HTTPForbidden
217 219
218 220 def get_rc_realm(self):
219 221 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
220 222
221 223 def build_authentication(self):
222 224 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
223 225 if self._rc_auth_http_code and not self.initial_call:
224 226 # return alternative HTTP code if alternative http return code
225 227 # is specified in RhodeCode config, but ONLY if it's not the
226 228 # FIRST call
227 229 custom_response_klass = self._get_response_from_code(
228 230 self._rc_auth_http_code)
229 231 return custom_response_klass(headers=head)
230 232 return HTTPUnauthorized(headers=head)
231 233
232 234 def authenticate(self, environ):
233 235 authorization = AUTHORIZATION(environ)
234 236 if not authorization:
235 237 return self.build_authentication()
236 238 (authmeth, auth) = authorization.split(' ', 1)
237 239 if 'basic' != authmeth.lower():
238 240 return self.build_authentication()
239 241 auth = auth.strip().decode('base64')
240 242 _parts = auth.split(':', 1)
241 243 if len(_parts) == 2:
242 244 username, password = _parts
243 245 auth_data = self.authfunc(
244 246 username, password, environ, VCS_TYPE,
245 247 registry=self.registry, acl_repo_name=self.acl_repo_name)
246 248 if auth_data:
247 249 return {'username': username, 'auth_data': auth_data}
248 250 if username and password:
249 251 # we mark that we actually executed authentication once, at
250 252 # that point we can use the alternative auth code
251 253 self.initial_call = False
252 254
253 255 return self.build_authentication()
254 256
255 257 __call__ = authenticate
256 258
257 259
258 260 def calculate_version_hash(config):
259 261 return sha1(
260 262 config.get('beaker.session.secret', '') +
261 263 rhodecode.__version__)[:8]
262 264
263 265
264 266 def get_current_lang(request):
265 267 # NOTE(marcink): remove after pyramid move
266 268 try:
267 269 return translation.get_lang()[0]
268 270 except:
269 271 pass
270 272
271 273 return getattr(request, '_LOCALE_', request.locale_name)
272 274
273 275
274 276 def attach_context_attributes(context, request, user_id):
275 277 """
276 278 Attach variables into template context called `c`.
277 279 """
278 280 config = request.registry.settings
279 281
280 282
281 283 rc_config = SettingsModel().get_all_settings(cache=True)
282 284
283 285 context.rhodecode_version = rhodecode.__version__
284 286 context.rhodecode_edition = config.get('rhodecode.edition')
285 287 # unique secret + version does not leak the version but keep consistency
286 288 context.rhodecode_version_hash = calculate_version_hash(config)
287 289
288 290 # Default language set for the incoming request
289 291 context.language = get_current_lang(request)
290 292
291 293 # Visual options
292 294 context.visual = AttributeDict({})
293 295
294 296 # DB stored Visual Items
295 297 context.visual.show_public_icon = str2bool(
296 298 rc_config.get('rhodecode_show_public_icon'))
297 299 context.visual.show_private_icon = str2bool(
298 300 rc_config.get('rhodecode_show_private_icon'))
299 301 context.visual.stylify_metatags = str2bool(
300 302 rc_config.get('rhodecode_stylify_metatags'))
301 303 context.visual.dashboard_items = safe_int(
302 304 rc_config.get('rhodecode_dashboard_items', 100))
303 305 context.visual.admin_grid_items = safe_int(
304 306 rc_config.get('rhodecode_admin_grid_items', 100))
305 307 context.visual.repository_fields = str2bool(
306 308 rc_config.get('rhodecode_repository_fields'))
307 309 context.visual.show_version = str2bool(
308 310 rc_config.get('rhodecode_show_version'))
309 311 context.visual.use_gravatar = str2bool(
310 312 rc_config.get('rhodecode_use_gravatar'))
311 313 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
312 314 context.visual.default_renderer = rc_config.get(
313 315 'rhodecode_markup_renderer', 'rst')
314 316 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
315 317 context.visual.rhodecode_support_url = \
316 318 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
317 319
318 320 context.visual.affected_files_cut_off = 60
319 321
320 322 context.pre_code = rc_config.get('rhodecode_pre_code')
321 323 context.post_code = rc_config.get('rhodecode_post_code')
322 324 context.rhodecode_name = rc_config.get('rhodecode_title')
323 325 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
324 326 # if we have specified default_encoding in the request, it has more
325 327 # priority
326 328 if request.GET.get('default_encoding'):
327 329 context.default_encodings.insert(0, request.GET.get('default_encoding'))
328 330 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
329 331 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
330 332
331 333 # INI stored
332 334 context.labs_active = str2bool(
333 335 config.get('labs_settings_active', 'false'))
334 336 context.ssh_enabled = str2bool(
335 337 config.get('ssh.generate_authorized_keyfile', 'false'))
336 338
337 339 context.visual.allow_repo_location_change = str2bool(
338 340 config.get('allow_repo_location_change', True))
339 341 context.visual.allow_custom_hooks_settings = str2bool(
340 342 config.get('allow_custom_hooks_settings', True))
341 343 context.debug_style = str2bool(config.get('debug_style', False))
342 344
343 345 context.rhodecode_instanceid = config.get('instance_id')
344 346
345 347 context.visual.cut_off_limit_diff = safe_int(
346 348 config.get('cut_off_limit_diff'))
347 349 context.visual.cut_off_limit_file = safe_int(
348 350 config.get('cut_off_limit_file'))
349 351
350 352 # AppEnlight
351 353 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
352 354 context.appenlight_api_public_key = config.get(
353 355 'appenlight.api_public_key', '')
354 356 context.appenlight_server_url = config.get('appenlight.server_url', '')
355 357
356 358 # JS template context
357 359 context.template_context = {
358 360 'repo_name': None,
359 361 'repo_type': None,
360 362 'repo_landing_commit': None,
361 363 'rhodecode_user': {
362 364 'username': None,
363 365 'email': None,
364 366 'notification_status': False
365 367 },
366 368 'visual': {
367 369 'default_renderer': None
368 370 },
369 371 'commit_data': {
370 372 'commit_id': None
371 373 },
372 374 'pull_request_data': {'pull_request_id': None},
373 375 'timeago': {
374 376 'refresh_time': 120 * 1000,
375 377 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
376 378 },
377 379 'pyramid_dispatch': {
378 380
379 381 },
380 382 'extra': {'plugins': {}}
381 383 }
382 384 # END CONFIG VARS
383 385
384 386 diffmode = 'sideside'
385 387 if request.GET.get('diffmode'):
386 388 if request.GET['diffmode'] == 'unified':
387 389 diffmode = 'unified'
388 390 elif request.session.get('diffmode'):
389 391 diffmode = request.session['diffmode']
390 392
391 393 context.diffmode = diffmode
392 394
393 395 if request.session.get('diffmode') != diffmode:
394 396 request.session['diffmode'] = diffmode
395 397
396 398 context.csrf_token = auth.get_csrf_token(session=request.session)
397 399 context.backends = rhodecode.BACKENDS.keys()
398 400 context.backends.sort()
399 401 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
400 402
401 403 # web case
402 404 if hasattr(request, 'user'):
403 405 context.auth_user = request.user
404 406 context.rhodecode_user = request.user
405 407
406 408 # api case
407 409 if hasattr(request, 'rpc_user'):
408 410 context.auth_user = request.rpc_user
409 411 context.rhodecode_user = request.rpc_user
410 412
411 413 # attach the whole call context to the request
412 414 request.call_context = context
413 415
414 416
415 417 def get_auth_user(request):
416 418 environ = request.environ
417 419 session = request.session
418 420
419 421 ip_addr = get_ip_addr(environ)
420 422 # make sure that we update permissions each time we call controller
421 423 _auth_token = (request.GET.get('auth_token', '') or
422 424 request.GET.get('api_key', ''))
423 425
424 426 if _auth_token:
425 427 # when using API_KEY we assume user exists, and
426 428 # doesn't need auth based on cookies.
427 429 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
428 430 authenticated = False
429 431 else:
430 432 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
431 433 try:
432 434 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
433 435 ip_addr=ip_addr)
434 436 except UserCreationError as e:
435 437 h.flash(e, 'error')
436 438 # container auth or other auth functions that create users
437 439 # on the fly can throw this exception signaling that there's
438 440 # issue with user creation, explanation should be provided
439 441 # in Exception itself. We then create a simple blank
440 442 # AuthUser
441 443 auth_user = AuthUser(ip_addr=ip_addr)
442 444
443 445 # in case someone changes a password for user it triggers session
444 446 # flush and forces a re-login
445 447 if password_changed(auth_user, session):
446 448 session.invalidate()
447 449 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
448 450 auth_user = AuthUser(ip_addr=ip_addr)
449 451
450 452 authenticated = cookie_store.get('is_authenticated')
451 453
452 454 if not auth_user.is_authenticated and auth_user.is_user_object:
453 455 # user is not authenticated and not empty
454 456 auth_user.set_authenticated(authenticated)
455 457
456 458 return auth_user
457 459
458 460
459 461 def h_filter(s):
460 462 """
461 463 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
462 464 we wrap this with additional functionality that converts None to empty
463 465 strings
464 466 """
465 467 if s is None:
466 468 return markupsafe.Markup()
467 469 return markupsafe.escape(s)
468 470
469 471
470 472 def add_events_routes(config):
471 473 """
472 474 Adds routing that can be used in events. Because some events are triggered
473 475 outside of pyramid context, we need to bootstrap request with some
474 476 routing registered
475 477 """
476 478
477 479 from rhodecode.apps._base import ADMIN_PREFIX
478 480
479 481 config.add_route(name='home', pattern='/')
480 482
481 483 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
482 484 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
483 485 config.add_route(name='repo_summary', pattern='/{repo_name}')
484 486 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
485 487 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
486 488
487 489 config.add_route(name='pullrequest_show',
488 490 pattern='/{repo_name}/pull-request/{pull_request_id}')
489 491 config.add_route(name='pull_requests_global',
490 492 pattern='/pull-request/{pull_request_id}')
491 493 config.add_route(name='repo_commit',
492 494 pattern='/{repo_name}/changeset/{commit_id}')
493 495
494 496 config.add_route(name='repo_files',
495 497 pattern='/{repo_name}/files/{commit_id}/{f_path}')
496 498
497 499
498 500 def bootstrap_config(request):
499 501 import pyramid.testing
500 502 registry = pyramid.testing.Registry('RcTestRegistry')
501 503
502 504 config = pyramid.testing.setUp(registry=registry, request=request)
503 505
504 506 # allow pyramid lookup in testing
505 507 config.include('pyramid_mako')
506 508 config.include('pyramid_beaker')
507 509 config.include('rhodecode.lib.rc_cache')
508 510
509 511 add_events_routes(config)
510 512
511 513 return config
512 514
513 515
514 516 def bootstrap_request(**kwargs):
515 517 import pyramid.testing
516 518
517 519 class TestRequest(pyramid.testing.DummyRequest):
518 520 application_url = kwargs.pop('application_url', 'http://example.com')
519 521 host = kwargs.pop('host', 'example.com:80')
520 522 domain = kwargs.pop('domain', 'example.com')
521 523
522 524 def translate(self, msg):
523 525 return msg
524 526
525 527 def plularize(self, singular, plural, n):
526 528 return singular
527 529
528 530 def get_partial_renderer(self, tmpl_name):
529 531
530 532 from rhodecode.lib.partial_renderer import get_partial_renderer
531 533 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
532 534
533 535 _call_context = {}
534 536 @property
535 537 def call_context(self):
536 538 return self._call_context
537 539
538 540 class TestDummySession(pyramid.testing.DummySession):
539 541 def save(*arg, **kw):
540 542 pass
541 543
542 544 request = TestRequest(**kwargs)
543 545 request.session = TestDummySession()
544 546
545 547 return request
546 548
@@ -1,140 +1,155 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 Set of custom exceptions used in RhodeCode
23 23 """
24 24
25 25 from webob.exc import HTTPClientError
26 26 from pyramid.httpexceptions import HTTPBadGateway
27 27
28 28
29 29 class LdapUsernameError(Exception):
30 30 pass
31 31
32 32
33 33 class LdapPasswordError(Exception):
34 34 pass
35 35
36 36
37 37 class LdapConnectionError(Exception):
38 38 pass
39 39
40 40
41 41 class LdapImportError(Exception):
42 42 pass
43 43
44 44
45 45 class DefaultUserException(Exception):
46 46 pass
47 47
48 48
49 49 class UserOwnsReposException(Exception):
50 50 pass
51 51
52 52
53 53 class UserOwnsRepoGroupsException(Exception):
54 54 pass
55 55
56 56
57 57 class UserOwnsUserGroupsException(Exception):
58 58 pass
59 59
60 60
61 61 class UserGroupAssignedException(Exception):
62 62 pass
63 63
64 64
65 65 class StatusChangeOnClosedPullRequestError(Exception):
66 66 pass
67 67
68 68
69 69 class AttachedForksError(Exception):
70 70 pass
71 71
72 72
73 73 class RepoGroupAssignmentError(Exception):
74 74 pass
75 75
76 76
77 77 class NonRelativePathError(Exception):
78 78 pass
79 79
80 80
81 81 class HTTPRequirementError(HTTPClientError):
82 82 title = explanation = 'Repository Requirement Missing'
83 83 reason = None
84 84
85 85 def __init__(self, message, *args, **kwargs):
86 86 self.title = self.explanation = message
87 87 super(HTTPRequirementError, self).__init__(*args, **kwargs)
88 88 self.args = (message, )
89 89
90 90
91 91 class HTTPLockedRC(HTTPClientError):
92 92 """
93 93 Special Exception For locked Repos in RhodeCode, the return code can
94 94 be overwritten by _code keyword argument passed into constructors
95 95 """
96 96 code = 423
97 97 title = explanation = 'Repository Locked'
98 98 reason = None
99 99
100 100 def __init__(self, message, *args, **kwargs):
101 101 from rhodecode import CONFIG
102 102 from rhodecode.lib.utils2 import safe_int
103 103 _code = CONFIG.get('lock_ret_code')
104 104 self.code = safe_int(_code, self.code)
105 105 self.title = self.explanation = message
106 106 super(HTTPLockedRC, self).__init__(*args, **kwargs)
107 107 self.args = (message, )
108 108
109 109
110 class HTTPBranchProtected(HTTPClientError):
111 """
112 Special Exception For Indicating that branch is protected in RhodeCode, the
113 return code can be overwritten by _code keyword argument passed into constructors
114 """
115 code = 403
116 title = explanation = 'Branch Protected'
117 reason = None
118
119 def __init__(self, message, *args, **kwargs):
120 self.title = self.explanation = message
121 super(HTTPBranchProtected, self).__init__(*args, **kwargs)
122 self.args = (message, )
123
124
110 125 class IMCCommitError(Exception):
111 126 pass
112 127
113 128
114 129 class UserCreationError(Exception):
115 130 pass
116 131
117 132
118 133 class NotAllowedToCreateUserError(Exception):
119 134 pass
120 135
121 136
122 137 class RepositoryCreationError(Exception):
123 138 pass
124 139
125 140
126 141 class VCSServerUnavailable(HTTPBadGateway):
127 142 """ HTTP Exception class for VCS Server errors """
128 143 code = 502
129 144 title = 'VCS Server Error'
130 145 causes = [
131 146 'VCS Server is not running',
132 147 'Incorrect vcs.server=host:port',
133 148 'Incorrect vcs.server.protocol',
134 149 ]
135 150
136 151 def __init__(self, message=''):
137 152 self.explanation = 'Could not connect to VCS Server'
138 153 if message:
139 154 self.explanation += ': ' + message
140 155 super(VCSServerUnavailable, self).__init__()
@@ -1,425 +1,465 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2013-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Set of hooks run by RhodeCode Enterprise
24 24 """
25 25
26 26 import os
27 27 import collections
28 28 import logging
29 29
30 30 import rhodecode
31 31 from rhodecode import events
32 32 from rhodecode.lib import helpers as h
33 33 from rhodecode.lib import audit_logger
34 34 from rhodecode.lib.utils2 import safe_str
35 from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError
35 from rhodecode.lib.exceptions import (
36 HTTPLockedRC, HTTPBranchProtected, UserCreationError)
36 37 from rhodecode.model.db import Repository, User
37 38
38 39 log = logging.getLogger(__name__)
39 40
40 41
41 42 HookResponse = collections.namedtuple('HookResponse', ('status', 'output'))
42 43
43 44
44 45 def is_shadow_repo(extras):
45 46 """
46 47 Returns ``True`` if this is an action executed against a shadow repository.
47 48 """
48 49 return extras['is_shadow_repo']
49 50
50 51
51 52 def _get_scm_size(alias, root_path):
52 53
53 54 if not alias.startswith('.'):
54 55 alias += '.'
55 56
56 57 size_scm, size_root = 0, 0
57 58 for path, unused_dirs, files in os.walk(safe_str(root_path)):
58 59 if path.find(alias) != -1:
59 60 for f in files:
60 61 try:
61 62 size_scm += os.path.getsize(os.path.join(path, f))
62 63 except OSError:
63 64 pass
64 65 else:
65 66 for f in files:
66 67 try:
67 68 size_root += os.path.getsize(os.path.join(path, f))
68 69 except OSError:
69 70 pass
70 71
71 72 size_scm_f = h.format_byte_size_binary(size_scm)
72 73 size_root_f = h.format_byte_size_binary(size_root)
73 74 size_total_f = h.format_byte_size_binary(size_root + size_scm)
74 75
75 76 return size_scm_f, size_root_f, size_total_f
76 77
77 78
78 79 # actual hooks called by Mercurial internally, and GIT by our Python Hooks
79 80 def repo_size(extras):
80 81 """Present size of repository after push."""
81 82 repo = Repository.get_by_repo_name(extras.repository)
82 83 vcs_part = safe_str(u'.%s' % repo.repo_type)
83 84 size_vcs, size_root, size_total = _get_scm_size(vcs_part,
84 85 repo.repo_full_path)
85 86 msg = ('Repository `%s` size summary %s:%s repo:%s total:%s\n'
86 87 % (repo.repo_name, vcs_part, size_vcs, size_root, size_total))
87 88 return HookResponse(0, msg)
88 89
89 90
90 91 def pre_push(extras):
91 92 """
92 93 Hook executed before pushing code.
93 94
94 95 It bans pushing when the repository is locked.
95 96 """
96 97
97 usr = User.get_by_username(extras.username)
98 user = User.get_by_username(extras.username)
98 99 output = ''
99 if extras.locked_by[0] and usr.user_id != int(extras.locked_by[0]):
100 if extras.locked_by[0] and user.user_id != int(extras.locked_by[0]):
100 101 locked_by = User.get(extras.locked_by[0]).username
101 102 reason = extras.locked_by[2]
102 103 # this exception is interpreted in git/hg middlewares and based
103 104 # on that proper return code is server to client
104 105 _http_ret = HTTPLockedRC(
105 106 _locked_by_explanation(extras.repository, locked_by, reason))
106 107 if str(_http_ret.code).startswith('2'):
107 108 # 2xx Codes don't raise exceptions
108 109 output = _http_ret.title
109 110 else:
110 111 raise _http_ret
111 112
112 # Propagate to external components. This is done after checking the
113 # lock, for consistent behavior.
114 113 if not is_shadow_repo(extras):
114 if extras.commit_ids and extras.check_branch_perms:
115
116 auth_user = user.AuthUser()
117 repo = Repository.get_by_repo_name(extras.repository)
118 affected_branches = []
119 if repo.repo_type == 'hg':
120 for entry in extras.commit_ids:
121 if entry['type'] == 'branch':
122 is_forced = bool(entry['multiple_heads'])
123 affected_branches.append([entry['name'], is_forced])
124 elif repo.repo_type == 'git':
125 for entry in extras.commit_ids:
126 if entry['type'] == 'heads':
127 is_forced = bool(entry['pruned_sha'])
128 affected_branches.append([entry['name'], is_forced])
129
130 for branch_name, is_forced in affected_branches:
131
132 rule, branch_perm = auth_user.get_rule_and_branch_permission(
133 extras.repository, branch_name)
134 if not branch_perm:
135 # no branch permission found for this branch, just keep checking
136 continue
137
138 if branch_perm == 'branch.push_force':
139 continue
140 elif branch_perm == 'branch.push' and is_forced is False:
141 continue
142 elif branch_perm == 'branch.push' and is_forced is True:
143 halt_message = 'Branch `{}` changes rejected by rule {}. ' \
144 'FORCE PUSH FORBIDDEN.'.format(branch_name, rule)
145 else:
146 halt_message = 'Branch `{}` changes rejected by rule {}.'.format(
147 branch_name, rule)
148
149 if halt_message:
150 _http_ret = HTTPBranchProtected(halt_message)
151 raise _http_ret
152
153 # Propagate to external components. This is done after checking the
154 # lock, for consistent behavior.
115 155 pre_push_extension(repo_store_path=Repository.base_path(), **extras)
116 156 events.trigger(events.RepoPrePushEvent(
117 157 repo_name=extras.repository, extras=extras))
118 158
119 159 return HookResponse(0, output)
120 160
121 161
122 162 def pre_pull(extras):
123 163 """
124 164 Hook executed before pulling the code.
125 165
126 166 It bans pulling when the repository is locked.
127 167 """
128 168
129 169 output = ''
130 170 if extras.locked_by[0]:
131 171 locked_by = User.get(extras.locked_by[0]).username
132 172 reason = extras.locked_by[2]
133 173 # this exception is interpreted in git/hg middlewares and based
134 174 # on that proper return code is server to client
135 175 _http_ret = HTTPLockedRC(
136 176 _locked_by_explanation(extras.repository, locked_by, reason))
137 177 if str(_http_ret.code).startswith('2'):
138 178 # 2xx Codes don't raise exceptions
139 179 output = _http_ret.title
140 180 else:
141 181 raise _http_ret
142 182
143 183 # Propagate to external components. This is done after checking the
144 184 # lock, for consistent behavior.
145 185 if not is_shadow_repo(extras):
146 186 pre_pull_extension(**extras)
147 187 events.trigger(events.RepoPrePullEvent(
148 188 repo_name=extras.repository, extras=extras))
149 189
150 190 return HookResponse(0, output)
151 191
152 192
153 193 def post_pull(extras):
154 194 """Hook executed after client pulls the code."""
155 195
156 196 audit_user = audit_logger.UserWrap(
157 197 username=extras.username,
158 198 ip_addr=extras.ip)
159 199 repo = audit_logger.RepoWrap(repo_name=extras.repository)
160 200 audit_logger.store(
161 201 'user.pull', action_data={
162 202 'user_agent': extras.user_agent},
163 203 user=audit_user, repo=repo, commit=True)
164 204
165 205 # Propagate to external components.
166 206 if not is_shadow_repo(extras):
167 207 post_pull_extension(**extras)
168 208 events.trigger(events.RepoPullEvent(
169 209 repo_name=extras.repository, extras=extras))
170 210
171 211 output = ''
172 212 # make lock is a tri state False, True, None. We only make lock on True
173 213 if extras.make_lock is True and not is_shadow_repo(extras):
174 214 user = User.get_by_username(extras.username)
175 215 Repository.lock(Repository.get_by_repo_name(extras.repository),
176 216 user.user_id,
177 217 lock_reason=Repository.LOCK_PULL)
178 218 msg = 'Made lock on repo `%s`' % (extras.repository,)
179 219 output += msg
180 220
181 221 if extras.locked_by[0]:
182 222 locked_by = User.get(extras.locked_by[0]).username
183 223 reason = extras.locked_by[2]
184 224 _http_ret = HTTPLockedRC(
185 225 _locked_by_explanation(extras.repository, locked_by, reason))
186 226 if str(_http_ret.code).startswith('2'):
187 227 # 2xx Codes don't raise exceptions
188 228 output += _http_ret.title
189 229
190 230 return HookResponse(0, output)
191 231
192 232
193 233 def post_push(extras):
194 234 """Hook executed after user pushes to the repository."""
195 235 commit_ids = extras.commit_ids
196 236
197 237 # log the push call
198 238 audit_user = audit_logger.UserWrap(
199 239 username=extras.username, ip_addr=extras.ip)
200 240 repo = audit_logger.RepoWrap(repo_name=extras.repository)
201 241 audit_logger.store(
202 242 'user.push', action_data={
203 243 'user_agent': extras.user_agent,
204 244 'commit_ids': commit_ids[:400]},
205 245 user=audit_user, repo=repo, commit=True)
206 246
207 247 # Propagate to external components.
208 248 if not is_shadow_repo(extras):
209 249 post_push_extension(
210 250 repo_store_path=Repository.base_path(),
211 251 pushed_revs=commit_ids,
212 252 **extras)
213 253 events.trigger(events.RepoPushEvent(
214 254 repo_name=extras.repository,
215 255 pushed_commit_ids=commit_ids,
216 256 extras=extras))
217 257
218 258 output = ''
219 259 # make lock is a tri state False, True, None. We only release lock on False
220 260 if extras.make_lock is False and not is_shadow_repo(extras):
221 261 Repository.unlock(Repository.get_by_repo_name(extras.repository))
222 262 msg = 'Released lock on repo `%s`\n' % extras.repository
223 263 output += msg
224 264
225 265 if extras.locked_by[0]:
226 266 locked_by = User.get(extras.locked_by[0]).username
227 267 reason = extras.locked_by[2]
228 268 _http_ret = HTTPLockedRC(
229 269 _locked_by_explanation(extras.repository, locked_by, reason))
230 270 # TODO: johbo: if not?
231 271 if str(_http_ret.code).startswith('2'):
232 272 # 2xx Codes don't raise exceptions
233 273 output += _http_ret.title
234 274
235 275 if extras.new_refs:
236 276 tmpl = \
237 277 extras.server_url + '/' + \
238 278 extras.repository + \
239 279 "/pull-request/new?{ref_type}={ref_name}"
240 280 for branch_name in extras.new_refs['branches']:
241 281 output += 'RhodeCode: open pull request link: {}\n'.format(
242 282 tmpl.format(ref_type='branch', ref_name=branch_name))
243 283
244 284 for book_name in extras.new_refs['bookmarks']:
245 285 output += 'RhodeCode: open pull request link: {}\n'.format(
246 286 tmpl.format(ref_type='bookmark', ref_name=book_name))
247 287
248 288 output += 'RhodeCode: push completed\n'
249 289 return HookResponse(0, output)
250 290
251 291
252 292 def _locked_by_explanation(repo_name, user_name, reason):
253 293 message = (
254 294 'Repository `%s` locked by user `%s`. Reason:`%s`'
255 295 % (repo_name, user_name, reason))
256 296 return message
257 297
258 298
259 299 def check_allowed_create_user(user_dict, created_by, **kwargs):
260 300 # pre create hooks
261 301 if pre_create_user.is_active():
262 302 allowed, reason = pre_create_user(created_by=created_by, **user_dict)
263 303 if not allowed:
264 304 raise UserCreationError(reason)
265 305
266 306
267 307 class ExtensionCallback(object):
268 308 """
269 309 Forwards a given call to rcextensions, sanitizes keyword arguments.
270 310
271 311 Does check if there is an extension active for that hook. If it is
272 312 there, it will forward all `kwargs_keys` keyword arguments to the
273 313 extension callback.
274 314 """
275 315
276 316 def __init__(self, hook_name, kwargs_keys):
277 317 self._hook_name = hook_name
278 318 self._kwargs_keys = set(kwargs_keys)
279 319
280 320 def __call__(self, *args, **kwargs):
281 321 log.debug('Calling extension callback for %s', self._hook_name)
282 322
283 323 kwargs_to_pass = dict((key, kwargs[key]) for key in self._kwargs_keys)
284 324 # backward compat for removed api_key for old hooks. THis was it works
285 325 # with older rcextensions that require api_key present
286 326 if self._hook_name in ['CREATE_USER_HOOK', 'DELETE_USER_HOOK']:
287 327 kwargs_to_pass['api_key'] = '_DEPRECATED_'
288 328
289 329 callback = self._get_callback()
290 330 if callback:
291 331 return callback(**kwargs_to_pass)
292 332 else:
293 333 log.debug('extensions callback not found skipping...')
294 334
295 335 def is_active(self):
296 336 return hasattr(rhodecode.EXTENSIONS, self._hook_name)
297 337
298 338 def _get_callback(self):
299 339 return getattr(rhodecode.EXTENSIONS, self._hook_name, None)
300 340
301 341
302 342 pre_pull_extension = ExtensionCallback(
303 343 hook_name='PRE_PULL_HOOK',
304 344 kwargs_keys=(
305 345 'server_url', 'config', 'scm', 'username', 'ip', 'action',
306 346 'repository'))
307 347
308 348
309 349 post_pull_extension = ExtensionCallback(
310 350 hook_name='PULL_HOOK',
311 351 kwargs_keys=(
312 352 'server_url', 'config', 'scm', 'username', 'ip', 'action',
313 353 'repository'))
314 354
315 355
316 356 pre_push_extension = ExtensionCallback(
317 357 hook_name='PRE_PUSH_HOOK',
318 358 kwargs_keys=(
319 359 'server_url', 'config', 'scm', 'username', 'ip', 'action',
320 360 'repository', 'repo_store_path', 'commit_ids'))
321 361
322 362
323 363 post_push_extension = ExtensionCallback(
324 364 hook_name='PUSH_HOOK',
325 365 kwargs_keys=(
326 366 'server_url', 'config', 'scm', 'username', 'ip', 'action',
327 367 'repository', 'repo_store_path', 'pushed_revs'))
328 368
329 369
330 370 pre_create_user = ExtensionCallback(
331 371 hook_name='PRE_CREATE_USER_HOOK',
332 372 kwargs_keys=(
333 373 'username', 'password', 'email', 'firstname', 'lastname', 'active',
334 374 'admin', 'created_by'))
335 375
336 376
337 377 log_create_pull_request = ExtensionCallback(
338 378 hook_name='CREATE_PULL_REQUEST',
339 379 kwargs_keys=(
340 380 'server_url', 'config', 'scm', 'username', 'ip', 'action',
341 381 'repository', 'pull_request_id', 'url', 'title', 'description',
342 382 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
343 383 'mergeable', 'source', 'target', 'author', 'reviewers'))
344 384
345 385
346 386 log_merge_pull_request = ExtensionCallback(
347 387 hook_name='MERGE_PULL_REQUEST',
348 388 kwargs_keys=(
349 389 'server_url', 'config', 'scm', 'username', 'ip', 'action',
350 390 'repository', 'pull_request_id', 'url', 'title', 'description',
351 391 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
352 392 'mergeable', 'source', 'target', 'author', 'reviewers'))
353 393
354 394
355 395 log_close_pull_request = ExtensionCallback(
356 396 hook_name='CLOSE_PULL_REQUEST',
357 397 kwargs_keys=(
358 398 'server_url', 'config', 'scm', 'username', 'ip', 'action',
359 399 'repository', 'pull_request_id', 'url', 'title', 'description',
360 400 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
361 401 'mergeable', 'source', 'target', 'author', 'reviewers'))
362 402
363 403
364 404 log_review_pull_request = ExtensionCallback(
365 405 hook_name='REVIEW_PULL_REQUEST',
366 406 kwargs_keys=(
367 407 'server_url', 'config', 'scm', 'username', 'ip', 'action',
368 408 'repository', 'pull_request_id', 'url', 'title', 'description',
369 409 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
370 410 'mergeable', 'source', 'target', 'author', 'reviewers'))
371 411
372 412
373 413 log_update_pull_request = ExtensionCallback(
374 414 hook_name='UPDATE_PULL_REQUEST',
375 415 kwargs_keys=(
376 416 'server_url', 'config', 'scm', 'username', 'ip', 'action',
377 417 'repository', 'pull_request_id', 'url', 'title', 'description',
378 418 'status', 'created_on', 'updated_on', 'commit_ids', 'review_status',
379 419 'mergeable', 'source', 'target', 'author', 'reviewers'))
380 420
381 421
382 422 log_create_user = ExtensionCallback(
383 423 hook_name='CREATE_USER_HOOK',
384 424 kwargs_keys=(
385 425 'username', 'full_name_or_username', 'full_contact', 'user_id',
386 426 'name', 'firstname', 'short_contact', 'admin', 'lastname',
387 427 'ip_addresses', 'extern_type', 'extern_name',
388 428 'email', 'api_keys', 'last_login',
389 429 'full_name', 'active', 'password', 'emails',
390 430 'inherit_default_permissions', 'created_by', 'created_on'))
391 431
392 432
393 433 log_delete_user = ExtensionCallback(
394 434 hook_name='DELETE_USER_HOOK',
395 435 kwargs_keys=(
396 436 'username', 'full_name_or_username', 'full_contact', 'user_id',
397 437 'name', 'firstname', 'short_contact', 'admin', 'lastname',
398 438 'ip_addresses',
399 439 'email', 'last_login',
400 440 'full_name', 'active', 'password', 'emails',
401 441 'inherit_default_permissions', 'deleted_by'))
402 442
403 443
404 444 log_create_repository = ExtensionCallback(
405 445 hook_name='CREATE_REPO_HOOK',
406 446 kwargs_keys=(
407 447 'repo_name', 'repo_type', 'description', 'private', 'created_on',
408 448 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
409 449 'clone_uri', 'fork_id', 'group_id', 'created_by'))
410 450
411 451
412 452 log_delete_repository = ExtensionCallback(
413 453 hook_name='DELETE_REPO_HOOK',
414 454 kwargs_keys=(
415 455 'repo_name', 'repo_type', 'description', 'private', 'created_on',
416 456 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
417 457 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
418 458
419 459
420 460 log_create_repository_group = ExtensionCallback(
421 461 hook_name='CREATE_REPO_GROUP_HOOK',
422 462 kwargs_keys=(
423 463 'group_name', 'group_parent_id', 'group_description',
424 464 'group_id', 'user_id', 'created_by', 'created_on',
425 465 'enable_locking'))
@@ -1,312 +1,324 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 os
22 22 import time
23 23 import logging
24 24 import tempfile
25 25 import traceback
26 26 import threading
27 27
28 28 from BaseHTTPServer import BaseHTTPRequestHandler
29 29 from SocketServer import TCPServer
30 30
31 31 import rhodecode
32 from rhodecode.lib.exceptions import HTTPLockedRC, HTTPBranchProtected
32 33 from rhodecode.model import meta
33 34 from rhodecode.lib.base import bootstrap_request, bootstrap_config
34 35 from rhodecode.lib import hooks_base
35 36 from rhodecode.lib.utils2 import AttributeDict
36 37 from rhodecode.lib.ext_json import json
37 38 from rhodecode.lib import rc_cache
38 39
39 40 log = logging.getLogger(__name__)
40 41
41 42
42 43 class HooksHttpHandler(BaseHTTPRequestHandler):
43 44
44 45 def do_POST(self):
45 46 method, extras = self._read_request()
46 47 txn_id = getattr(self.server, 'txn_id', None)
47 48 if txn_id:
48 49 log.debug('Computing TXN_ID based on `%s`:`%s`',
49 50 extras['repository'], extras['txn_id'])
50 51 computed_txn_id = rc_cache.utils.compute_key_from_params(
51 52 extras['repository'], extras['txn_id'])
52 53 if txn_id != computed_txn_id:
53 54 raise Exception(
54 55 'TXN ID fail: expected {} got {} instead'.format(
55 56 txn_id, computed_txn_id))
56 57
57 58 try:
58 59 result = self._call_hook(method, extras)
59 60 except Exception as e:
60 61 exc_tb = traceback.format_exc()
61 62 result = {
62 63 'exception': e.__class__.__name__,
63 64 'exception_traceback': exc_tb,
64 65 'exception_args': e.args
65 66 }
66 67 self._write_response(result)
67 68
68 69 def _read_request(self):
69 70 length = int(self.headers['Content-Length'])
70 71 body = self.rfile.read(length).decode('utf-8')
71 72 data = json.loads(body)
72 73 return data['method'], data['extras']
73 74
74 75 def _write_response(self, result):
75 76 self.send_response(200)
76 77 self.send_header("Content-type", "text/json")
77 78 self.end_headers()
78 79 self.wfile.write(json.dumps(result))
79 80
80 81 def _call_hook(self, method, extras):
81 82 hooks = Hooks()
82 83 try:
83 84 result = getattr(hooks, method)(extras)
84 85 finally:
85 86 meta.Session.remove()
86 87 return result
87 88
88 89 def log_message(self, format, *args):
89 90 """
90 91 This is an overridden method of BaseHTTPRequestHandler which logs using
91 92 logging library instead of writing directly to stderr.
92 93 """
93 94
94 95 message = format % args
95 96
96 97 log.debug(
97 98 "%s - - [%s] %s", self.client_address[0],
98 99 self.log_date_time_string(), message)
99 100
100 101
101 102 class DummyHooksCallbackDaemon(object):
102 103 hooks_uri = ''
103 104
104 105 def __init__(self):
105 106 self.hooks_module = Hooks.__module__
106 107
107 108 def __enter__(self):
108 109 log.debug('Running dummy hooks callback daemon')
109 110 return self
110 111
111 112 def __exit__(self, exc_type, exc_val, exc_tb):
112 113 log.debug('Exiting dummy hooks callback daemon')
113 114
114 115
115 116 class ThreadedHookCallbackDaemon(object):
116 117
117 118 _callback_thread = None
118 119 _daemon = None
119 120 _done = False
120 121
121 122 def __init__(self, txn_id=None, host=None, port=None):
122 123 self._prepare(txn_id=txn_id, host=None, port=port)
123 124
124 125 def __enter__(self):
125 126 self._run()
126 127 return self
127 128
128 129 def __exit__(self, exc_type, exc_val, exc_tb):
129 130 log.debug('Callback daemon exiting now...')
130 131 self._stop()
131 132
132 133 def _prepare(self, txn_id=None, host=None, port=None):
133 134 raise NotImplementedError()
134 135
135 136 def _run(self):
136 137 raise NotImplementedError()
137 138
138 139 def _stop(self):
139 140 raise NotImplementedError()
140 141
141 142
142 143 class HttpHooksCallbackDaemon(ThreadedHookCallbackDaemon):
143 144 """
144 145 Context manager which will run a callback daemon in a background thread.
145 146 """
146 147
147 148 hooks_uri = None
148 149
149 150 # From Python docs: Polling reduces our responsiveness to a shutdown
150 151 # request and wastes cpu at all other times.
151 152 POLL_INTERVAL = 0.01
152 153
153 154 def _prepare(self, txn_id=None, host=None, port=None):
154 155 host = host or '127.0.0.1'
155 156 self._done = False
156 157 self._daemon = TCPServer((host, port or 0), HooksHttpHandler)
157 158 _, port = self._daemon.server_address
158 159 self.hooks_uri = '{}:{}'.format(host, port)
159 160 self.txn_id = txn_id
160 161 # inject transaction_id for later verification
161 162 self._daemon.txn_id = self.txn_id
162 163
163 164 log.debug(
164 165 "Preparing HTTP callback daemon at `%s` and registering hook object",
165 166 self.hooks_uri)
166 167
167 168 def _run(self):
168 169 log.debug("Running event loop of callback daemon in background thread")
169 170 callback_thread = threading.Thread(
170 171 target=self._daemon.serve_forever,
171 172 kwargs={'poll_interval': self.POLL_INTERVAL})
172 173 callback_thread.daemon = True
173 174 callback_thread.start()
174 175 self._callback_thread = callback_thread
175 176
176 177 def _stop(self):
177 178 log.debug("Waiting for background thread to finish.")
178 179 self._daemon.shutdown()
179 180 self._callback_thread.join()
180 181 self._daemon = None
181 182 self._callback_thread = None
182 183 if self.txn_id:
183 184 txn_id_file = get_txn_id_data_path(self.txn_id)
184 185 log.debug('Cleaning up TXN ID %s', txn_id_file)
185 186 if os.path.isfile(txn_id_file):
186 187 os.remove(txn_id_file)
187 188
188 189 log.debug("Background thread done.")
189 190
190 191
191 192 def get_txn_id_data_path(txn_id):
192 193 root = tempfile.gettempdir()
193 194 return os.path.join(root, 'rc_txn_id_{}'.format(txn_id))
194 195
195 196
196 197 def store_txn_id_data(txn_id, data_dict):
197 198 if not txn_id:
198 199 log.warning('Cannot store txn_id because it is empty')
199 200 return
200 201
201 202 path = get_txn_id_data_path(txn_id)
202 203 try:
203 204 with open(path, 'wb') as f:
204 205 f.write(json.dumps(data_dict))
205 206 except Exception:
206 207 log.exception('Failed to write txn_id metadata')
207 208
208 209
209 210 def get_txn_id_from_store(txn_id):
210 211 """
211 212 Reads txn_id from store and if present returns the data for callback manager
212 213 """
213 214 path = get_txn_id_data_path(txn_id)
214 215 try:
215 216 with open(path, 'rb') as f:
216 217 return json.loads(f.read())
217 218 except Exception:
218 219 return {}
219 220
220 221
221 222 def prepare_callback_daemon(extras, protocol, host, use_direct_calls, txn_id=None):
222 223 txn_details = get_txn_id_from_store(txn_id)
223 224 port = txn_details.get('port', 0)
224 225 if use_direct_calls:
225 226 callback_daemon = DummyHooksCallbackDaemon()
226 227 extras['hooks_module'] = callback_daemon.hooks_module
227 228 else:
228 229 if protocol == 'http':
229 230 callback_daemon = HttpHooksCallbackDaemon(
230 231 txn_id=txn_id, host=host, port=port)
231 232 else:
232 233 log.error('Unsupported callback daemon protocol "%s"', protocol)
233 234 raise Exception('Unsupported callback daemon protocol.')
234 235
235 236 extras['hooks_uri'] = callback_daemon.hooks_uri
236 237 extras['hooks_protocol'] = protocol
237 238 extras['time'] = time.time()
238 239
239 240 # register txn_id
240 241 extras['txn_id'] = txn_id
241 242
242 243 log.debug('Prepared a callback daemon: %s at url `%s`',
243 244 callback_daemon.__class__.__name__, callback_daemon.hooks_uri)
244 245 return callback_daemon, extras
245 246
246 247
247 248 class Hooks(object):
248 249 """
249 250 Exposes the hooks for remote call backs
250 251 """
251 252
252 253 def repo_size(self, extras):
253 254 log.debug("Called repo_size of %s object", self)
254 255 return self._call_hook(hooks_base.repo_size, extras)
255 256
256 257 def pre_pull(self, extras):
257 258 log.debug("Called pre_pull of %s object", self)
258 259 return self._call_hook(hooks_base.pre_pull, extras)
259 260
260 261 def post_pull(self, extras):
261 262 log.debug("Called post_pull of %s object", self)
262 263 return self._call_hook(hooks_base.post_pull, extras)
263 264
264 265 def pre_push(self, extras):
265 266 log.debug("Called pre_push of %s object", self)
266 267 return self._call_hook(hooks_base.pre_push, extras)
267 268
268 269 def post_push(self, extras):
269 270 log.debug("Called post_push of %s object", self)
270 271 return self._call_hook(hooks_base.post_push, extras)
271 272
272 273 def _call_hook(self, hook, extras):
273 274 extras = AttributeDict(extras)
274 275 server_url = extras['server_url']
275 276 request = bootstrap_request(application_url=server_url)
276 277
277 278 bootstrap_config(request) # inject routes and other interfaces
278 279
279 280 # inject the user for usage in hooks
280 281 request.user = AttributeDict({'username': extras.username,
281 282 'ip_addr': extras.ip,
282 283 'user_id': extras.user_id})
283 284
284 285 extras.request = request
285 286
286 287 try:
287 288 result = hook(extras)
288 except Exception as error:
289 exc_tb = traceback.format_exc()
290 log.exception('Exception when handling hook %s', hook)
289 except HTTPBranchProtected as handled_error:
290 # Those special cases doesn't need error reporting. It's a case of
291 # locked repo or protected branch
292 result = AttributeDict({
293 'status': handled_error.code,
294 'output': handled_error.explanation
295 })
296 except (HTTPLockedRC, Exception) as error:
297 # locked needs different handling since we need to also
298 # handle PULL operations
299 exc_tb = ''
300 if not isinstance(error, HTTPLockedRC):
301 exc_tb = traceback.format_exc()
302 log.exception('Exception when handling hook %s', hook)
291 303 error_args = error.args
292 304 return {
293 305 'status': 128,
294 306 'output': '',
295 307 'exception': type(error).__name__,
296 308 'exception_traceback': exc_tb,
297 309 'exception_args': error_args,
298 310 }
299 311 finally:
300 312 meta.Session.remove()
301 313
302 314 log.debug('Got hook call response %s', result)
303 315 return {
304 316 'status': result.status,
305 317 'output': result.output,
306 318 }
307 319
308 320 def __enter__(self):
309 321 return self
310 322
311 323 def __exit__(self, exc_type, exc_val, exc_tb):
312 324 pass
@@ -1,661 +1,673 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2014-2018 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 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 23 It's implemented with basic auth function
24 24 """
25 25
26 26 import os
27 27 import re
28 28 import logging
29 29 import importlib
30 30 from functools import wraps
31 31 from StringIO import StringIO
32 32 from lxml import etree
33 33
34 34 import time
35 35 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
36 36
37 37 from pyramid.httpexceptions import (
38 38 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
39 39 from zope.cachedescriptors.property import Lazy as LazyProperty
40 40
41 41 import rhodecode
42 42 from rhodecode.authentication.base import authenticate, VCS_TYPE, loadplugin
43 43 from rhodecode.lib import rc_cache
44 44 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
45 45 from rhodecode.lib.base import (
46 46 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
47 47 from rhodecode.lib.exceptions import (UserCreationError, NotAllowedToCreateUserError)
48 48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 49 from rhodecode.lib.middleware import appenlight
50 50 from rhodecode.lib.middleware.utils import scm_app_http
51 51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 54 from rhodecode.lib.vcs.backends import base
55 55
56 56 from rhodecode.model import meta
57 57 from rhodecode.model.db import User, Repository, PullRequest
58 58 from rhodecode.model.scm import ScmModel
59 59 from rhodecode.model.pull_request import PullRequestModel
60 60 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def extract_svn_txn_id(acl_repo_name, data):
66 66 """
67 67 Helper method for extraction of svn txn_id from submitted XML data during
68 68 POST operations
69 69 """
70 70 try:
71 71 root = etree.fromstring(data)
72 72 pat = re.compile(r'/txn/(?P<txn_id>.*)')
73 73 for el in root:
74 74 if el.tag == '{DAV:}source':
75 75 for sub_el in el:
76 76 if sub_el.tag == '{DAV:}href':
77 77 match = pat.search(sub_el.text)
78 78 if match:
79 79 svn_tx_id = match.groupdict()['txn_id']
80 80 txn_id = rc_cache.utils.compute_key_from_params(
81 81 acl_repo_name, svn_tx_id)
82 82 return txn_id
83 83 except Exception:
84 84 log.exception('Failed to extract txn_id')
85 85
86 86
87 87 def initialize_generator(factory):
88 88 """
89 89 Initializes the returned generator by draining its first element.
90 90
91 91 This can be used to give a generator an initializer, which is the code
92 92 up to the first yield statement. This decorator enforces that the first
93 93 produced element has the value ``"__init__"`` to make its special
94 94 purpose very explicit in the using code.
95 95 """
96 96
97 97 @wraps(factory)
98 98 def wrapper(*args, **kwargs):
99 99 gen = factory(*args, **kwargs)
100 100 try:
101 101 init = gen.next()
102 102 except StopIteration:
103 103 raise ValueError('Generator must yield at least one element.')
104 104 if init != "__init__":
105 105 raise ValueError('First yielded element must be "__init__".')
106 106 return gen
107 107 return wrapper
108 108
109 109
110 110 class SimpleVCS(object):
111 111 """Common functionality for SCM HTTP handlers."""
112 112
113 113 SCM = 'unknown'
114 114
115 115 acl_repo_name = None
116 116 url_repo_name = None
117 117 vcs_repo_name = None
118 118 rc_extras = {}
119 119
120 120 # We have to handle requests to shadow repositories different than requests
121 121 # to normal repositories. Therefore we have to distinguish them. To do this
122 122 # we use this regex which will match only on URLs pointing to shadow
123 123 # repositories.
124 124 shadow_repo_re = re.compile(
125 125 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
126 126 '(?P<target>{slug_pat})/' # target repo
127 127 'pull-request/(?P<pr_id>\d+)/' # pull request
128 128 'repository$' # shadow repo
129 129 .format(slug_pat=SLUG_RE.pattern))
130 130
131 131 def __init__(self, config, registry):
132 132 self.registry = registry
133 133 self.config = config
134 134 # re-populated by specialized middleware
135 135 self.repo_vcs_config = base.Config()
136 136 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
137 137
138 138 registry.rhodecode_settings = self.rhodecode_settings
139 139 # authenticate this VCS request using authfunc
140 140 auth_ret_code_detection = \
141 141 str2bool(self.config.get('auth_ret_code_detection', False))
142 142 self.authenticate = BasicAuth(
143 143 '', authenticate, registry, config.get('auth_ret_code'),
144 144 auth_ret_code_detection)
145 145 self.ip_addr = '0.0.0.0'
146 146
147 147 @LazyProperty
148 148 def global_vcs_config(self):
149 149 try:
150 150 return VcsSettingsModel().get_ui_settings_as_config_obj()
151 151 except Exception:
152 152 return base.Config()
153 153
154 154 @property
155 155 def base_path(self):
156 156 settings_path = self.repo_vcs_config.get(
157 157 *VcsSettingsModel.PATH_SETTING)
158 158
159 159 if not settings_path:
160 160 settings_path = self.global_vcs_config.get(
161 161 *VcsSettingsModel.PATH_SETTING)
162 162
163 163 if not settings_path:
164 164 # try, maybe we passed in explicitly as config option
165 165 settings_path = self.config.get('base_path')
166 166
167 167 if not settings_path:
168 168 raise ValueError('FATAL: base_path is empty')
169 169 return settings_path
170 170
171 171 def set_repo_names(self, environ):
172 172 """
173 173 This will populate the attributes acl_repo_name, url_repo_name,
174 174 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
175 175 shadow) repositories all names are equal. In case of requests to a
176 176 shadow repository the acl-name points to the target repo of the pull
177 177 request and the vcs-name points to the shadow repo file system path.
178 178 The url-name is always the URL used by the vcs client program.
179 179
180 180 Example in case of a shadow repo:
181 181 acl_repo_name = RepoGroup/MyRepo
182 182 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
183 183 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
184 184 """
185 185 # First we set the repo name from URL for all attributes. This is the
186 186 # default if handling normal (non shadow) repo requests.
187 187 self.url_repo_name = self._get_repository_name(environ)
188 188 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
189 189 self.is_shadow_repo = False
190 190
191 191 # Check if this is a request to a shadow repository.
192 192 match = self.shadow_repo_re.match(self.url_repo_name)
193 193 if match:
194 194 match_dict = match.groupdict()
195 195
196 196 # Build acl repo name from regex match.
197 197 acl_repo_name = safe_unicode('{groups}{target}'.format(
198 198 groups=match_dict['groups'] or '',
199 199 target=match_dict['target']))
200 200
201 201 # Retrieve pull request instance by ID from regex match.
202 202 pull_request = PullRequest.get(match_dict['pr_id'])
203 203
204 204 # Only proceed if we got a pull request and if acl repo name from
205 205 # URL equals the target repo name of the pull request.
206 206 if pull_request and \
207 207 (acl_repo_name == pull_request.target_repo.repo_name):
208 208 repo_id = pull_request.target_repo.repo_id
209 209 # Get file system path to shadow repository.
210 210 workspace_id = PullRequestModel()._workspace_id(pull_request)
211 211 target_vcs = pull_request.target_repo.scm_instance()
212 212 vcs_repo_name = target_vcs._get_shadow_repository_path(
213 213 repo_id, workspace_id)
214 214
215 215 # Store names for later usage.
216 216 self.vcs_repo_name = vcs_repo_name
217 217 self.acl_repo_name = acl_repo_name
218 218 self.is_shadow_repo = True
219 219
220 220 log.debug('Setting all VCS repository names: %s', {
221 221 'acl_repo_name': self.acl_repo_name,
222 222 'url_repo_name': self.url_repo_name,
223 223 'vcs_repo_name': self.vcs_repo_name,
224 224 })
225 225
226 226 @property
227 227 def scm_app(self):
228 228 custom_implementation = self.config['vcs.scm_app_implementation']
229 229 if custom_implementation == 'http':
230 230 log.info('Using HTTP implementation of scm app.')
231 231 scm_app_impl = scm_app_http
232 232 else:
233 233 log.info('Using custom implementation of scm_app: "{}"'.format(
234 234 custom_implementation))
235 235 scm_app_impl = importlib.import_module(custom_implementation)
236 236 return scm_app_impl
237 237
238 238 def _get_by_id(self, repo_name):
239 239 """
240 240 Gets a special pattern _<ID> from clone url and tries to replace it
241 241 with a repository_name for support of _<ID> non changeable urls
242 242 """
243 243
244 244 data = repo_name.split('/')
245 245 if len(data) >= 2:
246 246 from rhodecode.model.repo import RepoModel
247 247 by_id_match = RepoModel().get_repo_by_id(repo_name)
248 248 if by_id_match:
249 249 data[1] = by_id_match.repo_name
250 250
251 251 return safe_str('/'.join(data))
252 252
253 253 def _invalidate_cache(self, repo_name):
254 254 """
255 255 Set's cache for this repository for invalidation on next access
256 256
257 257 :param repo_name: full repo name, also a cache key
258 258 """
259 259 ScmModel().mark_for_invalidation(repo_name)
260 260
261 261 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
262 262 db_repo = Repository.get_by_repo_name(repo_name)
263 263 if not db_repo:
264 264 log.debug('Repository `%s` not found inside the database.',
265 265 repo_name)
266 266 return False
267 267
268 268 if db_repo.repo_type != scm_type:
269 269 log.warning(
270 270 'Repository `%s` have incorrect scm_type, expected %s got %s',
271 271 repo_name, db_repo.repo_type, scm_type)
272 272 return False
273 273
274 274 config = db_repo._config
275 275 config.set('extensions', 'largefiles', '')
276 276 return is_valid_repo(
277 277 repo_name, base_path,
278 278 explicit_scm=scm_type, expect_scm=scm_type, config=config)
279 279
280 280 def valid_and_active_user(self, user):
281 281 """
282 282 Checks if that user is not empty, and if it's actually object it checks
283 283 if he's active.
284 284
285 285 :param user: user object or None
286 286 :return: boolean
287 287 """
288 288 if user is None:
289 289 return False
290 290
291 291 elif user.active:
292 292 return True
293 293
294 294 return False
295 295
296 296 @property
297 297 def is_shadow_repo_dir(self):
298 298 return os.path.isdir(self.vcs_repo_name)
299 299
300 def _check_permission(self, action, user, repo_name, ip_addr=None,
300 def _check_permission(self, action, user, auth_user, repo_name, ip_addr=None,
301 301 plugin_id='', plugin_cache_active=False, cache_ttl=0):
302 302 """
303 303 Checks permissions using action (push/pull) user and repository
304 304 name. If plugin_cache and ttl is set it will use the plugin which
305 305 authenticated the user to store the cached permissions result for N
306 306 amount of seconds as in cache_ttl
307 307
308 308 :param action: push or pull action
309 309 :param user: user instance
310 310 :param repo_name: repository name
311 311 """
312 312
313 313 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
314 314 plugin_id, plugin_cache_active, cache_ttl)
315 315
316 316 user_id = user.user_id
317 317 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
318 318 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
319 319
320 320 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
321 321 expiration_time=cache_ttl,
322 322 condition=plugin_cache_active)
323 323 def compute_perm_vcs(
324 324 cache_name, plugin_id, action, user_id, repo_name, ip_addr):
325 325
326 326 log.debug('auth: calculating permission access now...')
327 327 # check IP
328 328 inherit = user.inherit_default_permissions
329 329 ip_allowed = AuthUser.check_ip_allowed(
330 330 user_id, ip_addr, inherit_from_default=inherit)
331 331 if ip_allowed:
332 332 log.info('Access for IP:%s allowed', ip_addr)
333 333 else:
334 334 return False
335 335
336 336 if action == 'push':
337 337 perms = ('repository.write', 'repository.admin')
338 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
338 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
339 339 return False
340 340
341 341 else:
342 342 # any other action need at least read permission
343 343 perms = (
344 344 'repository.read', 'repository.write', 'repository.admin')
345 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
345 if not HasPermissionAnyMiddleware(*perms)(auth_user, repo_name):
346 346 return False
347 347
348 348 return True
349 349
350 350 start = time.time()
351 351 log.debug('Running plugin `%s` permissions check', plugin_id)
352 352
353 353 # for environ based auth, password can be empty, but then the validation is
354 354 # on the server that fills in the env data needed for authentication
355 355 perm_result = compute_perm_vcs(
356 356 'vcs_permissions', plugin_id, action, user.user_id, repo_name, ip_addr)
357 357
358 358 auth_time = time.time() - start
359 359 log.debug('Permissions for plugin `%s` completed in %.3fs, '
360 360 'expiration time of fetched cache %.1fs.',
361 361 plugin_id, auth_time, cache_ttl)
362 362
363 363 return perm_result
364 364
365 365 def _check_ssl(self, environ, start_response):
366 366 """
367 367 Checks the SSL check flag and returns False if SSL is not present
368 368 and required True otherwise
369 369 """
370 370 org_proto = environ['wsgi._org_proto']
371 371 # check if we have SSL required ! if not it's a bad request !
372 372 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
373 373 if require_ssl and org_proto == 'http':
374 374 log.debug(
375 375 'Bad request: detected protocol is `%s` and '
376 376 'SSL/HTTPS is required.', org_proto)
377 377 return False
378 378 return True
379 379
380 380 def _get_default_cache_ttl(self):
381 381 # take AUTH_CACHE_TTL from the `rhodecode` auth plugin
382 382 plugin = loadplugin('egg:rhodecode-enterprise-ce#rhodecode')
383 383 plugin_settings = plugin.get_settings()
384 384 plugin_cache_active, cache_ttl = plugin.get_ttl_cache(
385 385 plugin_settings) or (False, 0)
386 386 return plugin_cache_active, cache_ttl
387 387
388 388 def __call__(self, environ, start_response):
389 389 try:
390 390 return self._handle_request(environ, start_response)
391 391 except Exception:
392 392 log.exception("Exception while handling request")
393 393 appenlight.track_exception(environ)
394 394 return HTTPInternalServerError()(environ, start_response)
395 395 finally:
396 396 meta.Session.remove()
397 397
398 398 def _handle_request(self, environ, start_response):
399 399
400 400 if not self._check_ssl(environ, start_response):
401 401 reason = ('SSL required, while RhodeCode was unable '
402 402 'to detect this as SSL request')
403 403 log.debug('User not allowed to proceed, %s', reason)
404 404 return HTTPNotAcceptable(reason)(environ, start_response)
405 405
406 406 if not self.url_repo_name:
407 407 log.warning('Repository name is empty: %s', self.url_repo_name)
408 408 # failed to get repo name, we fail now
409 409 return HTTPNotFound()(environ, start_response)
410 410 log.debug('Extracted repo name is %s', self.url_repo_name)
411 411
412 412 ip_addr = get_ip_addr(environ)
413 413 user_agent = get_user_agent(environ)
414 414 username = None
415 415
416 416 # skip passing error to error controller
417 417 environ['pylons.status_code_redirect'] = True
418 418
419 419 # ======================================================================
420 420 # GET ACTION PULL or PUSH
421 421 # ======================================================================
422 422 action = self._get_action(environ)
423 423
424 424 # ======================================================================
425 425 # Check if this is a request to a shadow repository of a pull request.
426 426 # In this case only pull action is allowed.
427 427 # ======================================================================
428 428 if self.is_shadow_repo and action != 'pull':
429 429 reason = 'Only pull action is allowed for shadow repositories.'
430 430 log.debug('User not allowed to proceed, %s', reason)
431 431 return HTTPNotAcceptable(reason)(environ, start_response)
432 432
433 433 # Check if the shadow repo actually exists, in case someone refers
434 434 # to it, and it has been deleted because of successful merge.
435 435 if self.is_shadow_repo and not self.is_shadow_repo_dir:
436 436 log.debug(
437 437 'Shadow repo detected, and shadow repo dir `%s` is missing',
438 438 self.is_shadow_repo_dir)
439 439 return HTTPNotFound()(environ, start_response)
440 440
441 441 # ======================================================================
442 442 # CHECK ANONYMOUS PERMISSION
443 443 # ======================================================================
444 detect_force_push = False
445 check_branch_perms = False
444 446 if action in ['pull', 'push']:
445 anonymous_user = User.get_default_user()
447 user_obj = anonymous_user = User.get_default_user()
448 auth_user = user_obj.AuthUser()
446 449 username = anonymous_user.username
447 450 if anonymous_user.active:
448 451 plugin_cache_active, cache_ttl = self._get_default_cache_ttl()
449 452 # ONLY check permissions if the user is activated
450 453 anonymous_perm = self._check_permission(
451 action, anonymous_user, self.acl_repo_name, ip_addr,
454 action, anonymous_user, auth_user, self.acl_repo_name, ip_addr,
452 455 plugin_id='anonymous_access',
453 456 plugin_cache_active=plugin_cache_active,
454 457 cache_ttl=cache_ttl,
455 458 )
456 459 else:
457 460 anonymous_perm = False
458 461
459 462 if not anonymous_user.active or not anonymous_perm:
460 463 if not anonymous_user.active:
461 464 log.debug('Anonymous access is disabled, running '
462 465 'authentication')
463 466
464 467 if not anonymous_perm:
465 468 log.debug('Not enough credentials to access this '
466 469 'repository as anonymous user')
467 470
468 471 username = None
469 472 # ==============================================================
470 473 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
471 474 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
472 475 # ==============================================================
473 476
474 477 # try to auth based on environ, container auth methods
475 478 log.debug('Running PRE-AUTH for container based authentication')
476 479 pre_auth = authenticate(
477 480 '', '', environ, VCS_TYPE, registry=self.registry,
478 481 acl_repo_name=self.acl_repo_name)
479 482 if pre_auth and pre_auth.get('username'):
480 483 username = pre_auth['username']
481 484 log.debug('PRE-AUTH got %s as username', username)
482 485 if pre_auth:
483 486 log.debug('PRE-AUTH successful from %s',
484 487 pre_auth.get('auth_data', {}).get('_plugin'))
485 488
486 489 # If not authenticated by the container, running basic auth
487 490 # before inject the calling repo_name for special scope checks
488 491 self.authenticate.acl_repo_name = self.acl_repo_name
489 492
490 493 plugin_cache_active, cache_ttl = False, 0
491 494 plugin = None
492 495 if not username:
493 496 self.authenticate.realm = self.authenticate.get_rc_realm()
494 497
495 498 try:
496 499 auth_result = self.authenticate(environ)
497 500 except (UserCreationError, NotAllowedToCreateUserError) as e:
498 501 log.error(e)
499 502 reason = safe_str(e)
500 503 return HTTPNotAcceptable(reason)(environ, start_response)
501 504
502 505 if isinstance(auth_result, dict):
503 506 AUTH_TYPE.update(environ, 'basic')
504 507 REMOTE_USER.update(environ, auth_result['username'])
505 508 username = auth_result['username']
506 509 plugin = auth_result.get('auth_data', {}).get('_plugin')
507 510 log.info(
508 511 'MAIN-AUTH successful for user `%s` from %s plugin',
509 512 username, plugin)
510 513
511 514 plugin_cache_active, cache_ttl = auth_result.get(
512 515 'auth_data', {}).get('_ttl_cache') or (False, 0)
513 516 else:
514 517 return auth_result.wsgi_application(
515 518 environ, start_response)
516 519
517 520 # ==============================================================
518 521 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
519 522 # ==============================================================
520 523 user = User.get_by_username(username)
521 524 if not self.valid_and_active_user(user):
522 525 return HTTPForbidden()(environ, start_response)
523 526 username = user.username
524 527 user_id = user.user_id
525 528
526 529 # check user attributes for password change flag
527 530 user_obj = user
531 auth_user = user_obj.AuthUser()
528 532 if user_obj and user_obj.username != User.DEFAULT_USER and \
529 533 user_obj.user_data.get('force_password_change'):
530 534 reason = 'password change required'
531 535 log.debug('User not allowed to authenticate, %s', reason)
532 536 return HTTPNotAcceptable(reason)(environ, start_response)
533 537
534 538 # check permissions for this repository
535 539 perm = self._check_permission(
536 action, user, self.acl_repo_name, ip_addr,
540 action, user, auth_user, self.acl_repo_name, ip_addr,
537 541 plugin, plugin_cache_active, cache_ttl)
538 542 if not perm:
539 543 return HTTPForbidden()(environ, start_response)
540 544 environ['rc_auth_user_id'] = user_id
541 545
546 if action == 'push':
547 perms = auth_user.get_branch_permissions(self.acl_repo_name)
548 if perms:
549 check_branch_perms = True
550 detect_force_push = True
551
542 552 # extras are injected into UI object and later available
543 553 # in hooks executed by RhodeCode
544 554 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
555
545 556 extras = vcs_operation_context(
546 557 environ, repo_name=self.acl_repo_name, username=username,
547 558 action=action, scm=self.SCM, check_locking=check_locking,
548 is_shadow_repo=self.is_shadow_repo
559 is_shadow_repo=self.is_shadow_repo, check_branch_perms=check_branch_perms,
560 detect_force_push=detect_force_push
549 561 )
550 562
551 563 # ======================================================================
552 564 # REQUEST HANDLING
553 565 # ======================================================================
554 566 repo_path = os.path.join(
555 567 safe_str(self.base_path), safe_str(self.vcs_repo_name))
556 568 log.debug('Repository path is %s', repo_path)
557 569
558 570 fix_PATH()
559 571
560 572 log.info(
561 573 '%s action on %s repo "%s" by "%s" from %s %s',
562 574 action, self.SCM, safe_str(self.url_repo_name),
563 575 safe_str(username), ip_addr, user_agent)
564 576
565 577 return self._generate_vcs_response(
566 578 environ, start_response, repo_path, extras, action)
567 579
568 580 @initialize_generator
569 581 def _generate_vcs_response(
570 582 self, environ, start_response, repo_path, extras, action):
571 583 """
572 584 Returns a generator for the response content.
573 585
574 586 This method is implemented as a generator, so that it can trigger
575 587 the cache validation after all content sent back to the client. It
576 588 also handles the locking exceptions which will be triggered when
577 589 the first chunk is produced by the underlying WSGI application.
578 590 """
579 591 txn_id = ''
580 592 if 'CONTENT_LENGTH' in environ and environ['REQUEST_METHOD'] == 'MERGE':
581 593 # case for SVN, we want to re-use the callback daemon port
582 594 # so we use the txn_id, for this we peek the body, and still save
583 595 # it as wsgi.input
584 596 data = environ['wsgi.input'].read()
585 597 environ['wsgi.input'] = StringIO(data)
586 598 txn_id = extract_svn_txn_id(self.acl_repo_name, data)
587 599
588 600 callback_daemon, extras = self._prepare_callback_daemon(
589 601 extras, environ, action, txn_id=txn_id)
590 602 log.debug('HOOKS extras is %s', extras)
591 603
592 604 config = self._create_config(extras, self.acl_repo_name)
593 605 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
594 606 with callback_daemon:
595 607 app.rc_extras = extras
596 608
597 609 try:
598 610 response = app(environ, start_response)
599 611 finally:
600 612 # This statement works together with the decorator
601 613 # "initialize_generator" above. The decorator ensures that
602 614 # we hit the first yield statement before the generator is
603 615 # returned back to the WSGI server. This is needed to
604 616 # ensure that the call to "app" above triggers the
605 617 # needed callback to "start_response" before the
606 618 # generator is actually used.
607 619 yield "__init__"
608 620
609 621 # iter content
610 622 for chunk in response:
611 623 yield chunk
612 624
613 625 try:
614 626 # invalidate cache on push
615 627 if action == 'push':
616 628 self._invalidate_cache(self.url_repo_name)
617 629 finally:
618 630 meta.Session.remove()
619 631
620 632 def _get_repository_name(self, environ):
621 633 """Get repository name out of the environmnent
622 634
623 635 :param environ: WSGI environment
624 636 """
625 637 raise NotImplementedError()
626 638
627 639 def _get_action(self, environ):
628 640 """Map request commands into a pull or push command.
629 641
630 642 :param environ: WSGI environment
631 643 """
632 644 raise NotImplementedError()
633 645
634 646 def _create_wsgi_app(self, repo_path, repo_name, config):
635 647 """Return the WSGI app that will finally handle the request."""
636 648 raise NotImplementedError()
637 649
638 650 def _create_config(self, extras, repo_name):
639 651 """Create a safe config representation."""
640 652 raise NotImplementedError()
641 653
642 654 def _should_use_callback_daemon(self, extras, environ, action):
643 655 return True
644 656
645 657 def _prepare_callback_daemon(self, extras, environ, action, txn_id=None):
646 658 direct_calls = vcs_settings.HOOKS_DIRECT_CALLS
647 659 if not self._should_use_callback_daemon(extras, environ, action):
648 660 # disable callback daemon for actions that don't require it
649 661 direct_calls = True
650 662
651 663 return prepare_callback_daemon(
652 664 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
653 665 host=vcs_settings.HOOKS_HOST, use_direct_calls=direct_calls, txn_id=txn_id)
654 666
655 667
656 668 def _should_check_locking(query_string):
657 669 # this is kind of hacky, but due to how mercurial handles client-server
658 670 # server see all operation on commit; bookmarks, phases and
659 671 # obsolescence marker in different transaction, we don't want to check
660 672 # locking on those
661 673 return query_string not in ['cmd=listkeys']
@@ -1,617 +1,617 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 os
22 22 from hashlib import sha1
23 23
24 24 import pytest
25 25 from mock import patch
26 26
27 27 from rhodecode.lib import auth
28 28 from rhodecode.lib.utils2 import md5
29 29 from rhodecode.model.auth_token import AuthTokenModel
30 30 from rhodecode.model.db import User
31 31 from rhodecode.model.repo import RepoModel
32 32 from rhodecode.model.user import UserModel
33 33 from rhodecode.model.user_group import UserGroupModel
34 34
35 35
36 36 def test_perm_origin_dict():
37 37 pod = auth.PermOriginDict()
38 38 pod['thing'] = 'read', 'default'
39 39 assert pod['thing'] == 'read'
40 40
41 41 assert pod.perm_origin_stack == {
42 42 'thing': [('read', 'default')]}
43 43
44 44 pod['thing'] = 'write', 'admin'
45 45 assert pod['thing'] == 'write'
46 46
47 47 assert pod.perm_origin_stack == {
48 48 'thing': [('read', 'default'), ('write', 'admin')]}
49 49
50 50 pod['other'] = 'write', 'default'
51 51
52 52 assert pod.perm_origin_stack == {
53 53 'other': [('write', 'default')],
54 54 'thing': [('read', 'default'), ('write', 'admin')]}
55 55
56 56 pod['other'] = 'none', 'override'
57 57
58 58 assert pod.perm_origin_stack == {
59 59 'other': [('write', 'default'), ('none', 'override')],
60 60 'thing': [('read', 'default'), ('write', 'admin')]}
61 61
62 62 with pytest.raises(ValueError):
63 63 pod['thing'] = 'read'
64 64
65 65
66 66 def test_cached_perms_data(user_regular, backend_random):
67 67 permissions = get_permissions(user_regular)
68 68 repo_name = backend_random.repo.repo_name
69 69 expected_global_permissions = {
70 70 'repository.read', 'group.read', 'usergroup.read'}
71 71 assert expected_global_permissions.issubset(permissions['global'])
72 72 assert permissions['repositories'][repo_name] == 'repository.read'
73 73
74 74
75 75 def test_cached_perms_data_with_admin_user(user_regular, backend_random):
76 76 permissions = get_permissions(user_regular, user_is_admin=True)
77 77 repo_name = backend_random.repo.repo_name
78 78 assert 'hg.admin' in permissions['global']
79 79 assert permissions['repositories'][repo_name] == 'repository.admin'
80 80
81 81
82 82 def test_cached_perms_data_with_admin_user_extended_calculation(user_regular, backend_random):
83 83 permissions = get_permissions(user_regular, user_is_admin=True,
84 84 calculate_super_admin=True)
85 85 repo_name = backend_random.repo.repo_name
86 86 assert 'hg.admin' in permissions['global']
87 87 assert permissions['repositories'][repo_name] == 'repository.admin'
88 88
89 89
90 90 def test_cached_perms_data_user_group_global_permissions(user_util):
91 91 user, user_group = user_util.create_user_with_group()
92 92 user_group.inherit_default_permissions = False
93 93
94 94 granted_permission = 'repository.write'
95 95 UserGroupModel().grant_perm(user_group, granted_permission)
96 96
97 97 permissions = get_permissions(user)
98 98 assert granted_permission in permissions['global']
99 99
100 100
101 101 @pytest.mark.xfail(reason="Not implemented, see TODO note")
102 102 def test_cached_perms_data_user_group_global_permissions_(user_util):
103 103 user, user_group = user_util.create_user_with_group()
104 104
105 105 granted_permission = 'repository.write'
106 106 UserGroupModel().grant_perm(user_group, granted_permission)
107 107
108 108 permissions = get_permissions(user)
109 109 assert granted_permission in permissions['global']
110 110
111 111
112 112 def test_cached_perms_data_user_global_permissions(user_util):
113 113 user = user_util.create_user()
114 114 UserModel().grant_perm(user, 'repository.none')
115 115
116 116 permissions = get_permissions(user, user_inherit_default_permissions=True)
117 117 assert 'repository.read' in permissions['global']
118 118
119 119
120 120 def test_cached_perms_data_repository_permissions_on_private_repository(
121 121 backend_random, user_util):
122 122 user, user_group = user_util.create_user_with_group()
123 123
124 124 repo = backend_random.create_repo()
125 125 repo.private = True
126 126
127 127 granted_permission = 'repository.write'
128 128 RepoModel().grant_user_group_permission(
129 129 repo, user_group.users_group_name, granted_permission)
130 130
131 131 permissions = get_permissions(user)
132 132 assert permissions['repositories'][repo.repo_name] == granted_permission
133 133
134 134
135 135 def test_cached_perms_data_repository_permissions_for_owner(
136 136 backend_random, user_util):
137 137 user = user_util.create_user()
138 138
139 139 repo = backend_random.create_repo()
140 140 repo.user_id = user.user_id
141 141
142 142 permissions = get_permissions(user)
143 143 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
144 144
145 145 # TODO: johbo: Make cleanup in UserUtility smarter, then remove this hack
146 146 repo.user_id = User.get_default_user().user_id
147 147
148 148
149 149 def test_cached_perms_data_repository_permissions_not_inheriting_defaults(
150 150 backend_random, user_util):
151 151 user = user_util.create_user()
152 152 repo = backend_random.create_repo()
153 153
154 154 # Don't inherit default object permissions
155 155 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
156 156
157 157 permissions = get_permissions(user)
158 158 assert permissions['repositories'][repo.repo_name] == 'repository.none'
159 159
160 160
161 161 def test_cached_perms_data_default_permissions_on_repository_group(user_util):
162 162 # Have a repository group with default permissions set
163 163 repo_group = user_util.create_repo_group()
164 164 default_user = User.get_default_user()
165 165 user_util.grant_user_permission_to_repo_group(
166 166 repo_group, default_user, 'repository.write')
167 167 user = user_util.create_user()
168 168
169 169 permissions = get_permissions(user)
170 170 assert permissions['repositories_groups'][repo_group.group_name] == \
171 171 'repository.write'
172 172
173 173
174 174 def test_cached_perms_data_default_permissions_on_repository_group_owner(
175 175 user_util):
176 176 # Have a repository group
177 177 repo_group = user_util.create_repo_group()
178 178 default_user = User.get_default_user()
179 179
180 180 # Add a permission for the default user to hit the code path
181 181 user_util.grant_user_permission_to_repo_group(
182 182 repo_group, default_user, 'repository.write')
183 183
184 184 # Have an owner of the group
185 185 user = user_util.create_user()
186 186 repo_group.user_id = user.user_id
187 187
188 188 permissions = get_permissions(user)
189 189 assert permissions['repositories_groups'][repo_group.group_name] == \
190 190 'group.admin'
191 191
192 192
193 193 def test_cached_perms_data_default_permissions_on_repository_group_no_inherit(
194 194 user_util):
195 195 # Have a repository group
196 196 repo_group = user_util.create_repo_group()
197 197 default_user = User.get_default_user()
198 198
199 199 # Add a permission for the default user to hit the code path
200 200 user_util.grant_user_permission_to_repo_group(
201 201 repo_group, default_user, 'repository.write')
202 202
203 203 # Don't inherit default object permissions
204 204 user = user_util.create_user()
205 205 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
206 206
207 207 permissions = get_permissions(user)
208 208 assert permissions['repositories_groups'][repo_group.group_name] == \
209 209 'group.none'
210 210
211 211
212 212 def test_cached_perms_data_repository_permissions_from_user_group(
213 213 user_util, backend_random):
214 214 user, user_group = user_util.create_user_with_group()
215 215
216 216 # Needs a second user group to make sure that we select the right
217 217 # permissions.
218 218 user_group2 = user_util.create_user_group()
219 219 UserGroupModel().add_user_to_group(user_group2, user)
220 220
221 221 repo = backend_random.create_repo()
222 222
223 223 RepoModel().grant_user_group_permission(
224 224 repo, user_group.users_group_name, 'repository.read')
225 225 RepoModel().grant_user_group_permission(
226 226 repo, user_group2.users_group_name, 'repository.write')
227 227
228 228 permissions = get_permissions(user)
229 229 assert permissions['repositories'][repo.repo_name] == 'repository.write'
230 230
231 231
232 232 def test_cached_perms_data_repository_permissions_from_user_group_owner(
233 233 user_util, backend_random):
234 234 user, user_group = user_util.create_user_with_group()
235 235
236 236 repo = backend_random.create_repo()
237 237 repo.user_id = user.user_id
238 238
239 239 RepoModel().grant_user_group_permission(
240 240 repo, user_group.users_group_name, 'repository.write')
241 241
242 242 permissions = get_permissions(user)
243 243 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
244 244
245 245
246 246 def test_cached_perms_data_user_repository_permissions(
247 247 user_util, backend_random):
248 248 user = user_util.create_user()
249 249 repo = backend_random.create_repo()
250 250 granted_permission = 'repository.write'
251 251 RepoModel().grant_user_permission(repo, user, granted_permission)
252 252
253 253 permissions = get_permissions(user)
254 254 assert permissions['repositories'][repo.repo_name] == granted_permission
255 255
256 256
257 257 def test_cached_perms_data_user_repository_permissions_explicit(
258 258 user_util, backend_random):
259 259 user = user_util.create_user()
260 260 repo = backend_random.create_repo()
261 261 granted_permission = 'repository.none'
262 262 RepoModel().grant_user_permission(repo, user, granted_permission)
263 263
264 264 permissions = get_permissions(user, explicit=True)
265 265 assert permissions['repositories'][repo.repo_name] == granted_permission
266 266
267 267
268 268 def test_cached_perms_data_user_repository_permissions_owner(
269 269 user_util, backend_random):
270 270 user = user_util.create_user()
271 271 repo = backend_random.create_repo()
272 272 repo.user_id = user.user_id
273 273 RepoModel().grant_user_permission(repo, user, 'repository.write')
274 274
275 275 permissions = get_permissions(user)
276 276 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
277 277
278 278
279 279 def test_cached_perms_data_repository_groups_permissions_inherited(
280 280 user_util, backend_random):
281 281 user, user_group = user_util.create_user_with_group()
282 282
283 283 # Needs a second group to hit the last condition
284 284 user_group2 = user_util.create_user_group()
285 285 UserGroupModel().add_user_to_group(user_group2, user)
286 286
287 287 repo_group = user_util.create_repo_group()
288 288
289 289 user_util.grant_user_group_permission_to_repo_group(
290 290 repo_group, user_group, 'group.read')
291 291 user_util.grant_user_group_permission_to_repo_group(
292 292 repo_group, user_group2, 'group.write')
293 293
294 294 permissions = get_permissions(user)
295 295 assert permissions['repositories_groups'][repo_group.group_name] == \
296 296 'group.write'
297 297
298 298
299 299 def test_cached_perms_data_repository_groups_permissions_inherited_owner(
300 300 user_util, backend_random):
301 301 user, user_group = user_util.create_user_with_group()
302 302 repo_group = user_util.create_repo_group()
303 303 repo_group.user_id = user.user_id
304 304
305 305 granted_permission = 'group.write'
306 306 user_util.grant_user_group_permission_to_repo_group(
307 307 repo_group, user_group, granted_permission)
308 308
309 309 permissions = get_permissions(user)
310 310 assert permissions['repositories_groups'][repo_group.group_name] == \
311 311 'group.admin'
312 312
313 313
314 314 def test_cached_perms_data_repository_groups_permissions(
315 315 user_util, backend_random):
316 316 user = user_util.create_user()
317 317
318 318 repo_group = user_util.create_repo_group()
319 319
320 320 granted_permission = 'group.write'
321 321 user_util.grant_user_permission_to_repo_group(
322 322 repo_group, user, granted_permission)
323 323
324 324 permissions = get_permissions(user)
325 325 assert permissions['repositories_groups'][repo_group.group_name] == \
326 326 'group.write'
327 327
328 328
329 329 def test_cached_perms_data_repository_groups_permissions_explicit(
330 330 user_util, backend_random):
331 331 user = user_util.create_user()
332 332
333 333 repo_group = user_util.create_repo_group()
334 334
335 335 granted_permission = 'group.none'
336 336 user_util.grant_user_permission_to_repo_group(
337 337 repo_group, user, granted_permission)
338 338
339 339 permissions = get_permissions(user, explicit=True)
340 340 assert permissions['repositories_groups'][repo_group.group_name] == \
341 341 'group.none'
342 342
343 343
344 344 def test_cached_perms_data_repository_groups_permissions_owner(
345 345 user_util, backend_random):
346 346 user = user_util.create_user()
347 347
348 348 repo_group = user_util.create_repo_group()
349 349 repo_group.user_id = user.user_id
350 350
351 351 granted_permission = 'group.write'
352 352 user_util.grant_user_permission_to_repo_group(
353 353 repo_group, user, granted_permission)
354 354
355 355 permissions = get_permissions(user)
356 356 assert permissions['repositories_groups'][repo_group.group_name] == \
357 357 'group.admin'
358 358
359 359
360 360 def test_cached_perms_data_user_group_permissions_inherited(
361 361 user_util, backend_random):
362 362 user, user_group = user_util.create_user_with_group()
363 363 user_group2 = user_util.create_user_group()
364 364 UserGroupModel().add_user_to_group(user_group2, user)
365 365
366 366 target_user_group = user_util.create_user_group()
367 367
368 368 user_util.grant_user_group_permission_to_user_group(
369 369 target_user_group, user_group, 'usergroup.read')
370 370 user_util.grant_user_group_permission_to_user_group(
371 371 target_user_group, user_group2, 'usergroup.write')
372 372
373 373 permissions = get_permissions(user)
374 374 assert permissions['user_groups'][target_user_group.users_group_name] == \
375 375 'usergroup.write'
376 376
377 377
378 378 def test_cached_perms_data_user_group_permissions(
379 379 user_util, backend_random):
380 380 user = user_util.create_user()
381 381 user_group = user_util.create_user_group()
382 382 UserGroupModel().grant_user_permission(user_group, user, 'usergroup.write')
383 383
384 384 permissions = get_permissions(user)
385 385 assert permissions['user_groups'][user_group.users_group_name] == \
386 386 'usergroup.write'
387 387
388 388
389 389 def test_cached_perms_data_user_group_permissions_explicit(
390 390 user_util, backend_random):
391 391 user = user_util.create_user()
392 392 user_group = user_util.create_user_group()
393 393 UserGroupModel().grant_user_permission(user_group, user, 'usergroup.none')
394 394
395 395 permissions = get_permissions(user, explicit=True)
396 396 assert permissions['user_groups'][user_group.users_group_name] == \
397 397 'usergroup.none'
398 398
399 399
400 400 def test_cached_perms_data_user_group_permissions_not_inheriting_defaults(
401 401 user_util, backend_random):
402 402 user = user_util.create_user()
403 403 user_group = user_util.create_user_group()
404 404
405 405 # Don't inherit default object permissions
406 406 UserModel().grant_perm(user, 'hg.inherit_default_perms.false')
407 407
408 408 permissions = get_permissions(user)
409 409 assert permissions['user_groups'][user_group.users_group_name] == \
410 410 'usergroup.none'
411 411
412 412
413 413 def test_permission_calculator_admin_permissions(
414 414 user_util, backend_random):
415 415 user = user_util.create_user()
416 416 user_group = user_util.create_user_group()
417 417 repo = backend_random.repo
418 418 repo_group = user_util.create_repo_group()
419 419
420 420 calculator = auth.PermissionCalculator(
421 421 user.user_id, {}, False, False, True, 'higherwin')
422 permissions = calculator._admin_permissions()
422 permissions = calculator._calculate_admin_permissions()
423 423
424 424 assert permissions['repositories_groups'][repo_group.group_name] == \
425 425 'group.admin'
426 426 assert permissions['user_groups'][user_group.users_group_name] == \
427 427 'usergroup.admin'
428 428 assert permissions['repositories'][repo.repo_name] == 'repository.admin'
429 429 assert 'hg.admin' in permissions['global']
430 430
431 431
432 432 def test_permission_calculator_repository_permissions_robustness_from_group(
433 433 user_util, backend_random):
434 434 user, user_group = user_util.create_user_with_group()
435 435
436 436 RepoModel().grant_user_group_permission(
437 437 backend_random.repo, user_group.users_group_name, 'repository.write')
438 438
439 439 calculator = auth.PermissionCalculator(
440 440 user.user_id, {}, False, False, False, 'higherwin')
441 441 calculator._calculate_repository_permissions()
442 442
443 443
444 444 def test_permission_calculator_repository_permissions_robustness_from_user(
445 445 user_util, backend_random):
446 446 user = user_util.create_user()
447 447
448 448 RepoModel().grant_user_permission(
449 449 backend_random.repo, user, 'repository.write')
450 450
451 451 calculator = auth.PermissionCalculator(
452 452 user.user_id, {}, False, False, False, 'higherwin')
453 453 calculator._calculate_repository_permissions()
454 454
455 455
456 456 def test_permission_calculator_repo_group_permissions_robustness_from_group(
457 457 user_util, backend_random):
458 458 user, user_group = user_util.create_user_with_group()
459 459 repo_group = user_util.create_repo_group()
460 460
461 461 user_util.grant_user_group_permission_to_repo_group(
462 462 repo_group, user_group, 'group.write')
463 463
464 464 calculator = auth.PermissionCalculator(
465 465 user.user_id, {}, False, False, False, 'higherwin')
466 466 calculator._calculate_repository_group_permissions()
467 467
468 468
469 469 def test_permission_calculator_repo_group_permissions_robustness_from_user(
470 470 user_util, backend_random):
471 471 user = user_util.create_user()
472 472 repo_group = user_util.create_repo_group()
473 473
474 474 user_util.grant_user_permission_to_repo_group(
475 475 repo_group, user, 'group.write')
476 476
477 477 calculator = auth.PermissionCalculator(
478 478 user.user_id, {}, False, False, False, 'higherwin')
479 479 calculator._calculate_repository_group_permissions()
480 480
481 481
482 482 def test_permission_calculator_user_group_permissions_robustness_from_group(
483 483 user_util, backend_random):
484 484 user, user_group = user_util.create_user_with_group()
485 485 target_user_group = user_util.create_user_group()
486 486
487 487 user_util.grant_user_group_permission_to_user_group(
488 488 target_user_group, user_group, 'usergroup.write')
489 489
490 490 calculator = auth.PermissionCalculator(
491 491 user.user_id, {}, False, False, False, 'higherwin')
492 492 calculator._calculate_user_group_permissions()
493 493
494 494
495 495 def test_permission_calculator_user_group_permissions_robustness_from_user(
496 496 user_util, backend_random):
497 497 user = user_util.create_user()
498 498 target_user_group = user_util.create_user_group()
499 499
500 500 user_util.grant_user_permission_to_user_group(
501 501 target_user_group, user, 'usergroup.write')
502 502
503 503 calculator = auth.PermissionCalculator(
504 504 user.user_id, {}, False, False, False, 'higherwin')
505 505 calculator._calculate_user_group_permissions()
506 506
507 507
508 508 @pytest.mark.parametrize("algo, new_permission, old_permission, expected", [
509 509 ('higherwin', 'repository.none', 'repository.none', 'repository.none'),
510 510 ('higherwin', 'repository.read', 'repository.none', 'repository.read'),
511 511 ('lowerwin', 'repository.write', 'repository.write', 'repository.write'),
512 512 ('lowerwin', 'repository.read', 'repository.write', 'repository.read'),
513 513 ])
514 514 def test_permission_calculator_choose_permission(
515 515 user_regular, algo, new_permission, old_permission, expected):
516 516 calculator = auth.PermissionCalculator(
517 517 user_regular.user_id, {}, False, False, False, algo)
518 518 result = calculator._choose_permission(new_permission, old_permission)
519 519 assert result == expected
520 520
521 521
522 522 def test_permission_calculator_choose_permission_raises_on_wrong_algo(
523 523 user_regular):
524 524 calculator = auth.PermissionCalculator(
525 525 user_regular.user_id, {}, False, False, False, 'invalid')
526 526 result = calculator._choose_permission(
527 527 'repository.read', 'repository.read')
528 528 # TODO: johbo: This documents the existing behavior. Think of an
529 529 # improvement.
530 530 assert result is None
531 531
532 532
533 533 def test_auth_user_get_cookie_store_for_normal_user(user_util):
534 534 user = user_util.create_user()
535 535 auth_user = auth.AuthUser(user_id=user.user_id)
536 536 expected_data = {
537 537 'username': user.username,
538 538 'user_id': user.user_id,
539 539 'password': md5(user.password),
540 540 'is_authenticated': False
541 541 }
542 542 assert auth_user.get_cookie_store() == expected_data
543 543
544 544
545 545 def test_auth_user_get_cookie_store_for_default_user():
546 546 default_user = User.get_default_user()
547 547 auth_user = auth.AuthUser()
548 548 expected_data = {
549 549 'username': User.DEFAULT_USER,
550 550 'user_id': default_user.user_id,
551 551 'password': md5(default_user.password),
552 552 'is_authenticated': True
553 553 }
554 554 assert auth_user.get_cookie_store() == expected_data
555 555
556 556
557 557 def get_permissions(user, **kwargs):
558 558 """
559 559 Utility filling in useful defaults into the call to `_cached_perms_data`.
560 560
561 561 Fill in `**kwargs` if specific values are needed for a test.
562 562 """
563 563 call_args = {
564 564 'user_id': user.user_id,
565 565 'scope': {},
566 566 'user_is_admin': False,
567 567 'user_inherit_default_permissions': False,
568 568 'explicit': False,
569 569 'algo': 'higherwin',
570 570 'calculate_super_admin': False,
571 571 }
572 572 call_args.update(kwargs)
573 573 permissions = auth._cached_perms_data(**call_args)
574 574 return permissions
575 575
576 576
577 577 class TestGenerateAuthToken(object):
578 578 def test_salt_is_used_when_specified(self):
579 579 salt = 'abcde'
580 580 user_name = 'test_user'
581 581 result = auth.generate_auth_token(user_name, salt)
582 582 expected_result = sha1(user_name + salt).hexdigest()
583 583 assert result == expected_result
584 584
585 585 def test_salt_is_geneated_when_not_specified(self):
586 586 user_name = 'test_user'
587 587 random_salt = os.urandom(16)
588 588 with patch.object(auth, 'os') as os_mock:
589 589 os_mock.urandom.return_value = random_salt
590 590 result = auth.generate_auth_token(user_name)
591 591 expected_result = sha1(user_name + random_salt).hexdigest()
592 592 assert result == expected_result
593 593
594 594
595 595 @pytest.mark.parametrize("test_token, test_roles, auth_result, expected_tokens", [
596 596 ('', None, False,
597 597 []),
598 598 ('wrongtoken', None, False,
599 599 []),
600 600 ('abracadabra_vcs', [AuthTokenModel.cls.ROLE_API], False,
601 601 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
602 602 ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
603 603 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1)]),
604 604 ('abracadabra_api', [AuthTokenModel.cls.ROLE_API], True,
605 605 [('abracadabra_api', AuthTokenModel.cls.ROLE_API, -1),
606 606 ('abracadabra_http', AuthTokenModel.cls.ROLE_HTTP, -1)]),
607 607 ])
608 608 def test_auth_by_token(test_token, test_roles, auth_result, expected_tokens,
609 609 user_util):
610 610 user = user_util.create_user()
611 611 user_id = user.user_id
612 612 for token, role, expires in expected_tokens:
613 613 new_token = AuthTokenModel().create(user_id, 'test-token', expires, role)
614 614 new_token.api_key = token # inject known name for testing...
615 615
616 616 assert auth_result == user.authenticate_by_token(
617 617 test_token, roles=test_roles)
@@ -1,172 +1,193 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 Base for test suite for making push/pull operations.
23 23
24 24 .. important::
25 25
26 26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 27 to redirect things to stderr instead of stdout.
28 28 """
29 29
30 30 from os.path import join as jn
31 31 from subprocess32 import Popen, PIPE
32 32 import logging
33 33 import os
34 34 import tempfile
35 35
36 36 from rhodecode.tests import GIT_REPO, HG_REPO
37 37
38 38 DEBUG = True
39 39 RC_LOG = os.path.join(tempfile.gettempdir(), 'rc.log')
40 40 REPO_GROUP = 'a_repo_group'
41 41 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
42 42 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
43 43
44 44 log = logging.getLogger(__name__)
45 45
46 46
47 47 class Command(object):
48 48
49 49 def __init__(self, cwd):
50 50 self.cwd = cwd
51 51 self.process = None
52 52
53 53 def execute(self, cmd, *args):
54 54 """
55 55 Runs command on the system with given ``args``.
56 56 """
57 57
58 58 command = cmd + ' ' + ' '.join(args)
59 59 if DEBUG:
60 60 log.debug('*** CMD %s ***' % (command,))
61 61
62 62 env = dict(os.environ)
63 63 # Delete coverage variables, as they make the test fail for Mercurial
64 64 for key in env.keys():
65 65 if key.startswith('COV_CORE_'):
66 66 del env[key]
67 67
68 68 self.process = Popen(command, shell=True, stdout=PIPE, stderr=PIPE,
69 69 cwd=self.cwd, env=env)
70 70 stdout, stderr = self.process.communicate()
71 71 if DEBUG:
72 72 log.debug('STDOUT:%s' % (stdout,))
73 73 log.debug('STDERR:%s' % (stderr,))
74 74 return stdout, stderr
75 75
76 76 def assert_returncode_success(self):
77 77 assert self.process.returncode == 0
78 78
79 79
80 def _add_files_and_push(vcs, dest, clone_url=None, tags=None, **kwargs):
81 """
82 Generate some files, add it to DEST repo and push back
83 vcs is git or hg and defines what VCS we want to make those files for
84 """
85 # commit some stuff into this repo
80 def _add_files(vcs, dest, clone_url=None, tags=None, target_branch=None,
81 new_branch=False, **kwargs):
82 git_ident = "git config user.name {} && git config user.email {}".format(
83 'Marcin KuΕΊminski', 'me@email.com')
84 cwd = path = jn(dest)
85
86 86 tags = tags or []
87 cwd = path = jn(dest)
88 87 added_file = jn(path, '%ssetup.py' % tempfile._RandomNameSequence().next())
89 88 Command(cwd).execute('touch %s' % added_file)
90 89 Command(cwd).execute('%s add %s' % (vcs, added_file))
91 90 author_str = 'Marcin KuΕΊminski <me@email.com>'
92 91
93 git_ident = "git config user.name {} && git config user.email {}".format(
94 'Marcin KuΕΊminski', 'me@email.com')
95
96 92 for i in range(kwargs.get('files_no', 3)):
97 93 cmd = """echo 'added_line%s' >> %s""" % (i, added_file)
98 94 Command(cwd).execute(cmd)
99 95 if vcs == 'hg':
100 96 cmd = """hg commit -m 'commited new %s' -u '%s' %s """ % (
101 97 i, author_str, added_file
102 98 )
103 99 elif vcs == 'git':
104 100 cmd = """%s && git commit -m 'commited new %s' %s""" % (
105 101 git_ident, i, added_file)
106 102 Command(cwd).execute(cmd)
107 103
108 104 for tag in tags:
109 105 if vcs == 'hg':
110 stdout, stderr = Command(cwd).execute(
106 Command(cwd).execute(
111 107 'hg tag', tag['name'])
112 108 elif vcs == 'git':
113 109 if tag['commit']:
114 110 # annotated tag
115 stdout, stderr = Command(cwd).execute(
111 _stdout, _stderr = Command(cwd).execute(
116 112 """%s && git tag -a %s -m "%s" """ % (
117 113 git_ident, tag['name'], tag['commit']))
118 114 else:
119 115 # lightweight tag
120 stdout, stderr = Command(cwd).execute(
116 _stdout, _stderr = Command(cwd).execute(
121 117 """%s && git tag %s""" % (
122 118 git_ident, tag['name']))
123 119
120
121 def _add_files_and_push(vcs, dest, clone_url=None, tags=None, target_branch=None,
122 new_branch=False, **kwargs):
123 """
124 Generate some files, add it to DEST repo and push back
125 vcs is git or hg and defines what VCS we want to make those files for
126 """
127 git_ident = "git config user.name {} && git config user.email {}".format(
128 'Marcin KuΕΊminski', 'me@email.com')
129 cwd = path = jn(dest)
130
131 # commit some stuff into this repo
132 _add_files(vcs, dest, clone_url, tags, target_branch, new_branch, **kwargs)
133
134 default_target_branch = {
135 'git': 'master',
136 'hg': 'default'
137 }.get(vcs)
138
139 target_branch = target_branch or default_target_branch
140
124 141 # PUSH it back
125 142 stdout = stderr = None
126 143 if vcs == 'hg':
144 maybe_new_branch = ''
145 if new_branch:
146 maybe_new_branch = '--new-branch'
127 147 stdout, stderr = Command(cwd).execute(
128 'hg push --verbose', clone_url)
148 'hg push --verbose {} -r {} {}'.format(maybe_new_branch, target_branch, clone_url)
149 )
129 150 elif vcs == 'git':
130 151 stdout, stderr = Command(cwd).execute(
131 """%s &&
132 git push --verbose --tags %s master""" % (
133 git_ident, clone_url))
152 """{} &&
153 git push --verbose --tags {} {}""".format(git_ident, clone_url, target_branch)
154 )
134 155
135 156 return stdout, stderr
136 157
137 158
138 159 def _check_proper_git_push(
139 160 stdout, stderr, branch='master', should_set_default_branch=False):
140 161 # Note: Git is writing most information to stderr intentionally
141 162 assert 'fatal' not in stderr
142 163 assert 'rejected' not in stderr
143 164 assert 'Pushing to' in stderr
144 165 assert '%s -> %s' % (branch, branch) in stderr
145 166
146 167 if should_set_default_branch:
147 168 assert "Setting default branch to %s" % branch in stderr
148 169 else:
149 170 assert "Setting default branch" not in stderr
150 171
151 172
152 173 def _check_proper_hg_push(stdout, stderr, branch='default'):
153 174 assert 'pushing to' in stdout
154 175 assert 'searching for changes' in stdout
155 176
156 177 assert 'abort:' not in stderr
157 178
158 179
159 180 def _check_proper_clone(stdout, stderr, vcs):
160 181 if vcs == 'hg':
161 182 assert 'requesting all changes' in stdout
162 183 assert 'adding changesets' in stdout
163 184 assert 'adding manifests' in stdout
164 185 assert 'adding file changes' in stdout
165 186
166 187 assert stderr == ''
167 188
168 189 if vcs == 'git':
169 190 assert '' == stdout
170 191 assert 'Cloning into' in stderr
171 192 assert 'abort:' not in stderr
172 193 assert 'fatal:' not in stderr
@@ -1,269 +1,341 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2018 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 py.test config for test suite for making push/pull operations.
23 23
24 24 .. important::
25 25
26 26 You must have git >= 1.8.5 for tests to work fine. With 68b939b git started
27 27 to redirect things to stderr instead of stdout.
28 28 """
29 29
30 30 import os
31 31 import tempfile
32 32 import textwrap
33 33 import pytest
34 34
35 35 from rhodecode import events
36 from rhodecode.model.db import Integration
36 from rhodecode.model.db import Integration, UserRepoToPerm, Permission, \
37 UserToRepoBranchPermission, User
37 38 from rhodecode.model.integration import IntegrationModel
38 39 from rhodecode.model.db import Repository
39 40 from rhodecode.model.meta import Session
40 41 from rhodecode.model.settings import SettingsModel
41 42 from rhodecode.integrations.types.webhook import WebhookIntegrationType
42 43
43 44 from rhodecode.tests import GIT_REPO, HG_REPO
44 45 from rhodecode.tests.fixture import Fixture
45 46 from rhodecode.tests.server_utils import RcWebServer
46 47
47 48 REPO_GROUP = 'a_repo_group'
48 49 HG_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, HG_REPO)
49 50 GIT_REPO_WITH_GROUP = '%s/%s' % (REPO_GROUP, GIT_REPO)
50 51
51 52
52 53 @pytest.fixture(scope="module")
53 54 def rcextensions(request, db_connection, tmpdir_factory):
54 55 """
55 56 Installs a testing rcextensions pack to ensure they work as expected.
56 57 """
57 58 init_content = textwrap.dedent("""
58 59 # Forward import the example rcextensions to make it
59 60 # active for our tests.
60 61 from rhodecode.tests.other.example_rcextensions import *
61 62 """)
62 63
63 64 # Note: rcextensions are looked up based on the path of the ini file
64 65 root_path = tmpdir_factory.getbasetemp()
65 66 rcextensions_path = root_path.join('rcextensions')
66 67 init_path = rcextensions_path.join('__init__.py')
67 68
68 69 if rcextensions_path.check():
69 70 pytest.fail(
70 71 "Path for rcextensions already exists, please clean up before "
71 72 "test run this path: %s" % (rcextensions_path, ))
72 73 return
73 74
74 75 request.addfinalizer(rcextensions_path.remove)
75 76 init_path.write_binary(init_content, ensure=True)
76 77
77 78
78 79 @pytest.fixture(scope="module")
79 80 def repos(request, db_connection):
80 81 """Create a copy of each test repo in a repo group."""
81 82 fixture = Fixture()
82 83 repo_group = fixture.create_repo_group(REPO_GROUP)
83 84 repo_group_id = repo_group.group_id
84 85 fixture.create_fork(HG_REPO, HG_REPO,
85 86 repo_name_full=HG_REPO_WITH_GROUP,
86 87 repo_group=repo_group_id)
87 88 fixture.create_fork(GIT_REPO, GIT_REPO,
88 89 repo_name_full=GIT_REPO_WITH_GROUP,
89 90 repo_group=repo_group_id)
90 91
91 92 @request.addfinalizer
92 93 def cleanup():
93 94 fixture.destroy_repo(HG_REPO_WITH_GROUP)
94 95 fixture.destroy_repo(GIT_REPO_WITH_GROUP)
95 96 fixture.destroy_repo_group(repo_group_id)
96 97
97 98
98 99 @pytest.fixture(scope="module")
99 100 def rc_web_server_config_modification():
100 101 return []
101 102
102 103
103 104 @pytest.fixture(scope="module")
104 105 def rc_web_server_config_factory(testini_factory, rc_web_server_config_modification):
105 106 """
106 107 Configuration file used for the fixture `rc_web_server`.
107 108 """
108 109
109 110 def factory(rcweb_port, vcsserver_port):
110 111 custom_params = [
111 112 {'handler_console': {'level': 'DEBUG'}},
112 113 {'server:main': {'port': rcweb_port}},
113 114 {'app:main': {'vcs.server': 'localhost:%s' % vcsserver_port}}
114 115 ]
115 116 custom_params.extend(rc_web_server_config_modification)
116 117 return testini_factory(custom_params)
117 118 return factory
118 119
119 120
120 121 @pytest.fixture(scope="module")
121 122 def rc_web_server(
122 123 request, vcsserver_factory, available_port_factory,
123 124 rc_web_server_config_factory, repos, rcextensions):
124 125 """
125 126 Run the web server as a subprocess. with it's own instance of vcsserver
126 127 """
127 128 rcweb_port = available_port_factory()
128 129 print('Using rcweb ops test port {}'.format(rcweb_port))
129 130
130 131 vcsserver_port = available_port_factory()
131 132 print('Using vcsserver ops test port {}'.format(vcsserver_port))
132 133
133 134 vcs_log = os.path.join(tempfile.gettempdir(), 'rc_op_vcs.log')
134 135 vcsserver_factory(
135 136 request, vcsserver_port=vcsserver_port,
136 137 log_file=vcs_log,
137 138 overrides=(
138 139 {'server:main': {'workers': 2}},
139 140 {'server:main': {'graceful_timeout': 10}},
140 141 ))
141 142
142 143 rc_log = os.path.join(tempfile.gettempdir(), 'rc_op_web.log')
143 144 rc_web_server_config = rc_web_server_config_factory(
144 145 rcweb_port=rcweb_port,
145 146 vcsserver_port=vcsserver_port)
146 147 server = RcWebServer(rc_web_server_config, log_file=rc_log)
147 148 server.start()
148 149
149 150 @request.addfinalizer
150 151 def cleanup():
151 152 server.shutdown()
152 153
153 154 server.wait_until_ready()
154 155 return server
155 156
156 157
157 158 @pytest.fixture
158 159 def disable_locking(baseapp):
159 160 r = Repository.get_by_repo_name(GIT_REPO)
160 161 Repository.unlock(r)
161 162 r.enable_locking = False
162 163 Session().add(r)
163 164 Session().commit()
164 165
165 166 r = Repository.get_by_repo_name(HG_REPO)
166 167 Repository.unlock(r)
167 168 r.enable_locking = False
168 169 Session().add(r)
169 170 Session().commit()
170 171
171 172
172 173 @pytest.fixture
173 174 def enable_auth_plugins(request, baseapp, csrf_token):
174 175 """
175 176 Return a factory object that when called, allows to control which
176 177 authentication plugins are enabled.
177 178 """
178 179 def _enable_plugins(plugins_list, override=None):
179 180 override = override or {}
180 181 params = {
181 182 'auth_plugins': ','.join(plugins_list),
182 183 }
183 184
184 185 # helper translate some names to others
185 186 name_map = {
186 187 'token': 'authtoken'
187 188 }
188 189
189 190 for module in plugins_list:
190 191 plugin_name = module.partition('#')[-1]
191 192 if plugin_name in name_map:
192 193 plugin_name = name_map[plugin_name]
193 194 enabled_plugin = 'auth_%s_enabled' % plugin_name
194 195 cache_ttl = 'auth_%s_cache_ttl' % plugin_name
195 196
196 197 # default params that are needed for each plugin,
197 198 # `enabled` and `cache_ttl`
198 199 params.update({
199 200 enabled_plugin: True,
200 201 cache_ttl: 0
201 202 })
202 203 if override.get:
203 204 params.update(override.get(module, {}))
204 205
205 206 validated_params = params
206 207 for k, v in validated_params.items():
207 208 setting = SettingsModel().create_or_update_setting(k, v)
208 209 Session().add(setting)
209 210 Session().commit()
210 211
211 212 def cleanup():
212 213 _enable_plugins(['egg:rhodecode-enterprise-ce#rhodecode'])
213 214
214 215 request.addfinalizer(cleanup)
215 216
216 217 return _enable_plugins
217 218
218 219
219 220 @pytest.fixture
220 221 def fs_repo_only(request, rhodecode_fixtures):
221 222 def fs_repo_fabric(repo_name, repo_type):
222 223 rhodecode_fixtures.create_repo(repo_name, repo_type=repo_type)
223 224 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=False)
224 225
225 226 def cleanup():
226 227 rhodecode_fixtures.destroy_repo(repo_name, fs_remove=True)
227 228 rhodecode_fixtures.destroy_repo_on_filesystem(repo_name)
228 229
229 230 request.addfinalizer(cleanup)
230 231
231 232 return fs_repo_fabric
232 233
233 234
234 235 @pytest.fixture
235 236 def enable_webhook_push_integration(request):
236 237 integration = Integration()
237 238 integration.integration_type = WebhookIntegrationType.key
238 239 Session().add(integration)
239 240
240 241 settings = dict(
241 242 url='http://httpbin.org/post',
242 243 secret_token='secret',
243 244 username=None,
244 245 password=None,
245 246 custom_header_key=None,
246 247 custom_header_val=None,
247 248 method_type='post',
248 249 events=[events.RepoPushEvent.name],
249 250 log_data=True
250 251 )
251 252
252 253 IntegrationModel().update_integration(
253 254 integration,
254 255 name='IntegrationWebhookTest',
255 256 enabled=True,
256 257 settings=settings,
257 258 repo=None,
258 259 repo_group=None,
259 260 child_repos_only=False,
260 261 )
261 262 Session().commit()
262 263 integration_id = integration.integration_id
263 264
264 265 @request.addfinalizer
265 266 def cleanup():
266 267 integration = Integration.get(integration_id)
267 268 Session().delete(integration)
268 269 Session().commit()
269 270
271
272 @pytest.fixture
273 def branch_permission_setter(request):
274 """
275
276 def my_test(branch_permission_setter)
277 branch_permission_setter(repo_name, username, pattern='*', permission='branch.push')
278
279 """
280
281 rule_id = None
282 write_perm_id = None
283
284 def _branch_permissions_setter(
285 repo_name, username, pattern='*', permission='branch.push_force'):
286 global rule_id, write_perm_id
287
288 repo = Repository.get_by_repo_name(repo_name)
289 repo_id = repo.repo_id
290
291 user = User.get_by_username(username)
292 user_id = user.user_id
293
294 rule_perm_obj = Permission.get_by_key(permission)
295
296 write_perm = None
297
298 # add new entry, based on existing perm entry
299 perm = UserRepoToPerm.query() \
300 .filter(UserRepoToPerm.repository_id == repo_id) \
301 .filter(UserRepoToPerm.user_id == user_id) \
302 .first()
303
304 if not perm:
305 # such user isn't defined in Permissions for repository
306 # we now on-the-fly add new permission
307
308 write_perm = UserRepoToPerm()
309 write_perm.permission = Permission.get_by_key('repository.write')
310 write_perm.repository_id = repo_id
311 write_perm.user_id = user_id
312 Session().add(write_perm)
313 Session().flush()
314
315 perm = write_perm
316
317 rule = UserToRepoBranchPermission()
318 rule.rule_to_perm_id = perm.repo_to_perm_id
319 rule.branch_pattern = pattern
320 rule.rule_order = 10
321 rule.permission = rule_perm_obj
322 rule.repository_id = repo_id
323 Session().add(rule)
324 Session().commit()
325
326 global rule, write_perm
327
328 return rule
329
330 @request.addfinalizer
331 def cleanup():
332 if rule:
333 Session().delete(rule)
334 Session().commit()
335 if write_perm:
336 Session().delete(write_perm)
337 Session().commit()
338
339 return _branch_permissions_setter
340
341
General Comments 0
You need to be logged in to leave comments. Login now