##// 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')
@@ -404,14 +404,14 b' class PermissionCalculator(object):'
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')
@@ -440,8 +440,8 b' class PermissionCalculator(object):'
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()
@@ -452,7 +452,7 b' class PermissionCalculator(object):'
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
@@ -479,13 +479,11 b' class PermissionCalculator(object):'
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
@@ -571,27 +569,7 b' class PermissionCalculator(object):'
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
@@ -624,7 +602,7 b' class PermissionCalculator(object):'
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
@@ -641,7 +619,7 b' class PermissionCalculator(object):'
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
@@ -666,7 +644,7 b' class PermissionCalculator(object):'
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
@@ -691,6 +669,39 b' class PermissionCalculator(object):'
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.
@@ -783,6 +794,7 b' class PermissionCalculator(object):'
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
@@ -799,7 +811,6 b' class PermissionCalculator(object):'
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.
@@ -1036,14 +1047,14 b' class AuthUser(object):'
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()
@@ -1062,7 +1073,7 b' class AuthUser(object):'
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 """
@@ -1088,7 +1099,7 b' class AuthUser(object):'
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)
@@ -1143,7 +1154,7 b' class AuthUser(object):'
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
@@ -1158,6 +1169,9 b' class AuthUser(object):'
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
@@ -1168,7 +1182,12 b' class AuthUser(object):'
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))
@@ -1186,9 +1205,10 b' class AuthUser(object):'
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:
@@ -1340,6 +1360,35 b' class AuthUser(object):'
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)
@@ -2084,23 +2133,22 b' 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):
@@ -153,7 +153,7 b' def get_user_agent(environ):'
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
@@ -193,6 +193,8 b' def vcs_operation_context('
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
@@ -107,6 +107,21 b' class HTTPLockedRC(HTTPClientError):'
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
@@ -32,7 +32,8 b' 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__)
@@ -94,9 +95,9 b' def pre_push(extras):'
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
@@ -109,9 +110,48 b' def pre_push(extras):'
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))
@@ -29,6 +29,7 b' from BaseHTTPServer import BaseHTTPReque'
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
@@ -285,9 +286,20 b' class Hooks(object):'
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,
@@ -297,7 +297,7 b' class SimpleVCS(object):'
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
@@ -335,14 +335,14 b' class SimpleVCS(object):'
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
@@ -441,14 +441,17 b' class SimpleVCS(object):'
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,
@@ -525,6 +528,7 b' class SimpleVCS(object):'
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'
@@ -533,19 +537,27 b' class SimpleVCS(object):'
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 # ======================================================================
@@ -419,7 +419,7 b' def test_permission_calculator_admin_per'
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'
@@ -77,22 +77,18 b' class Command(object):'
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)
@@ -107,30 +103,55 b' def _add_files_and_push(vcs, dest, clone'
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
@@ -33,7 +33,8 b' 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
@@ -267,3 +268,74 b' def enable_webhook_push_integration(requ'
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