##// END OF EJS Templates
branch-permissions: enabled branch permissions checks for SSH backend.
marcink -
r2982:9342a381 default
parent child Browse files
Show More
@@ -1,208 +1,223 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 re
23 23 import logging
24 24 import datetime
25 25 from pyramid.compat import configparser
26 26
27 27 from rhodecode.model.db import Session, User, UserSshKeys
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 from .hg import MercurialServer
31 31 from .git import GitServer
32 32 from .svn import SubversionServer
33 33 log = logging.getLogger(__name__)
34 34
35 35
36 36 class SshWrapper(object):
37 37
38 38 def __init__(self, command, connection_info, mode,
39 39 user, user_id, key_id, shell, ini_path, env):
40 40 self.command = command
41 41 self.connection_info = connection_info
42 42 self.mode = mode
43 43 self.user = user
44 44 self.user_id = user_id
45 45 self.key_id = key_id
46 46 self.shell = shell
47 47 self.ini_path = ini_path
48 48 self.env = env
49 49
50 50 self.config = self.parse_config(ini_path)
51 51 self.server_impl = None
52 52
53 53 def parse_config(self, config_path):
54 54 parser = configparser.ConfigParser()
55 55 parser.read(config_path)
56 56 return parser
57 57
58 58 def update_key_access_time(self, key_id):
59 59 key = UserSshKeys().query().filter(
60 60 UserSshKeys.ssh_key_id == key_id).scalar()
61 61 if key:
62 62 key.accessed_on = datetime.datetime.utcnow()
63 63 Session().add(key)
64 64 Session().commit()
65 65 log.debug('Update key id:`%s` fingerprint:`%s` access time',
66 66 key_id, key.ssh_key_fingerprint)
67 67
68 68 def get_connection_info(self):
69 69 """
70 70 connection_info
71 71
72 72 Identifies the client and server ends of the connection.
73 73 The variable contains four space-separated values: client IP address,
74 74 client port number, server IP address, and server port number.
75 75 """
76 76 conn = dict(
77 77 client_ip=None,
78 78 client_port=None,
79 79 server_ip=None,
80 80 server_port=None,
81 81 )
82 82
83 83 info = self.connection_info.split(' ')
84 84 if len(info) == 4:
85 85 conn['client_ip'] = info[0]
86 86 conn['client_port'] = info[1]
87 87 conn['server_ip'] = info[2]
88 88 conn['server_port'] = info[3]
89 89
90 90 return conn
91 91
92 92 def get_repo_details(self, mode):
93 93 vcs_type = mode if mode in ['svn', 'hg', 'git'] else None
94 94 mode = mode
95 95 repo_name = None
96 96
97 97 hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$'
98 98 hg_match = re.match(hg_pattern, self.command)
99 99 if hg_match is not None:
100 100 vcs_type = 'hg'
101 101 repo_name = hg_match.group(1).strip('/')
102 102 return vcs_type, repo_name, mode
103 103
104 104 git_pattern = (
105 105 r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$')
106 106 git_match = re.match(git_pattern, self.command)
107 107 if git_match is not None:
108 108 vcs_type = 'git'
109 109 repo_name = git_match.group(2).strip('/')
110 110 mode = git_match.group(1)
111 111 return vcs_type, repo_name, mode
112 112
113 113 svn_pattern = r'^svnserve -t'
114 114 svn_match = re.match(svn_pattern, self.command)
115 115
116 116 if svn_match is not None:
117 117 vcs_type = 'svn'
118 118 # Repo name should be extracted from the input stream
119 119 return vcs_type, repo_name, mode
120 120
121 121 return vcs_type, repo_name, mode
122 122
123 def serve(self, vcs, repo, mode, user, permissions):
123 def serve(self, vcs, repo, mode, user, permissions, branch_permissions):
124 124 store = ScmModel().repos_path
125 125
126 check_branch_perms = False
127 detect_force_push = False
128
129 if branch_permissions:
130 check_branch_perms = True
131 detect_force_push = True
132
126 133 log.debug(
127 'VCS detected:`%s` mode: `%s` repo_name: %s', vcs, mode, repo)
134 'VCS detected:`%s` mode: `%s` repo_name: %s, branch_permission_checks:%s',
135 vcs, mode, repo, check_branch_perms)
136
137 # detect if we have to check branch permissions
138 extras = {
139 'detect_force_push': detect_force_push,
140 'check_branch_perms': check_branch_perms,
141 }
128 142
129 143 if vcs == 'hg':
130 144 server = MercurialServer(
131 145 store=store, ini_path=self.ini_path,
132 146 repo_name=repo, user=user,
133 147 user_permissions=permissions, config=self.config, env=self.env)
134 148 self.server_impl = server
135 return server.run()
149 return server.run(tunnel_extras=extras)
136 150
137 151 elif vcs == 'git':
138 152 server = GitServer(
139 153 store=store, ini_path=self.ini_path,
140 154 repo_name=repo, repo_mode=mode, user=user,
141 155 user_permissions=permissions, config=self.config, env=self.env)
142 156 self.server_impl = server
143 return server.run()
157 return server.run(tunnel_extras=extras)
144 158
145 159 elif vcs == 'svn':
146 160 server = SubversionServer(
147 161 store=store, ini_path=self.ini_path,
148 162 repo_name=None, user=user,
149 163 user_permissions=permissions, config=self.config, env=self.env)
150 164 self.server_impl = server
151 return server.run()
165 return server.run(tunnel_extras=extras)
152 166
153 167 else:
154 168 raise Exception('Unrecognised VCS: {}'.format(vcs))
155 169
156 170 def wrap(self):
157 171 mode = self.mode
158 172 user = self.user
159 173 user_id = self.user_id
160 174 key_id = self.key_id
161 175 shell = self.shell
162 176
163 177 scm_detected, scm_repo, scm_mode = self.get_repo_details(mode)
164 178
165 179 log.debug(
166 180 'Mode: `%s` User: `%s:%s` Shell: `%s` SSH Command: `\"%s\"` '
167 181 'SCM_DETECTED: `%s` SCM Mode: `%s` SCM Repo: `%s`',
168 182 mode, user, user_id, shell, self.command,
169 183 scm_detected, scm_mode, scm_repo)
170 184
171 185 # update last access time for this key
172 186 self.update_key_access_time(key_id)
173 187
174 188 log.debug('SSH Connection info %s', self.get_connection_info())
175 189
176 190 if shell and self.command is None:
177 191 log.info(
178 192 'Dropping to shell, no command given and shell is allowed')
179 193 os.execl('/bin/bash', '-l')
180 194 exit_code = 1
181 195
182 196 elif scm_detected:
183 197 user = User.get(user_id)
184 198 if not user:
185 199 log.warning('User with id %s not found', user_id)
186 200 exit_code = -1
187 201 return exit_code
188 202
189 203 auth_user = user.AuthUser()
190 204 permissions = auth_user.permissions['repositories']
191
205 repo_branch_permissions = auth_user.get_branch_permissions(scm_repo)
192 206 try:
193 207 exit_code, is_updated = self.serve(
194 scm_detected, scm_repo, scm_mode, user, permissions)
208 scm_detected, scm_repo, scm_mode, user, permissions,
209 repo_branch_permissions)
195 210 except Exception:
196 211 log.exception('Error occurred during execution of SshWrapper')
197 212 exit_code = -1
198 213
199 214 elif self.command is None and shell is False:
200 215 log.error('No Command given.')
201 216 exit_code = -1
202 217
203 218 else:
204 219 log.error(
205 220 'Unhandled Command: "%s" Aborting.', self.command)
206 221 exit_code = -1
207 222
208 223 return exit_code
@@ -1,156 +1,162 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 sys
23 23 import json
24 24 import logging
25 25
26 26 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
27 27 from rhodecode.lib.vcs.conf import settings as vcs_settings
28 28 from rhodecode.model.scm import ScmModel
29 29
30 30 log = logging.getLogger(__name__)
31 31
32 32
33 33 class VcsServer(object):
34 34 _path = None # set executable path for hg/git/svn binary
35 35 backend = None # set in child classes
36 36 tunnel = None # subprocess handling tunnel
37 37 write_perms = ['repository.admin', 'repository.write']
38 38 read_perms = ['repository.read', 'repository.admin', 'repository.write']
39 39
40 40 def __init__(self, user, user_permissions, config, env):
41 41 self.user = user
42 42 self.user_permissions = user_permissions
43 43 self.config = config
44 44 self.env = env
45 45 self.stdin = sys.stdin
46 46
47 47 self.repo_name = None
48 48 self.repo_mode = None
49 49 self.store = ''
50 50 self.ini_path = ''
51 51
52 52 def _invalidate_cache(self, repo_name):
53 53 """
54 54 Set's cache for this repository for invalidation on next access
55 55
56 56 :param repo_name: full repo name, also a cache key
57 57 """
58 58 ScmModel().mark_for_invalidation(repo_name)
59 59
60 60 def has_write_perm(self):
61 61 permission = self.user_permissions.get(self.repo_name)
62 62 if permission in ['repository.write', 'repository.admin']:
63 63 return True
64 64
65 65 return False
66 66
67 67 def _check_permissions(self, action):
68 68 permission = self.user_permissions.get(self.repo_name)
69 69 log.debug(
70 70 'permission for %s on %s are: %s',
71 71 self.user, self.repo_name, permission)
72 72
73 73 if not permission:
74 74 log.error('user `%s` permissions to repo:%s are empty. Forbidding access.',
75 75 self.user, self.repo_name)
76 76 return -2
77 77
78 78 if action == 'pull':
79 79 if permission in self.read_perms:
80 80 log.info(
81 81 'READ Permissions for User "%s" detected to repo "%s"!',
82 82 self.user, self.repo_name)
83 83 return 0
84 84 else:
85 85 if permission in self.write_perms:
86 86 log.info(
87 87 'WRITE+ Permissions for User "%s" detected to repo "%s"!',
88 88 self.user, self.repo_name)
89 89 return 0
90 90
91 91 log.error('Cannot properly fetch or verify user `%s` permissions. '
92 92 'Permissions: %s, vcs action: %s',
93 93 self.user, permission, action)
94 94 return -2
95 95
96 96 def update_environment(self, action, extras=None):
97 97
98 98 scm_data = {
99 99 'ip': os.environ['SSH_CLIENT'].split()[0],
100 100 'username': self.user.username,
101 101 'user_id': self.user.user_id,
102 102 'action': action,
103 103 'repository': self.repo_name,
104 104 'scm': self.backend,
105 105 'config': self.ini_path,
106 106 'make_lock': None,
107 107 'locked_by': [None, None],
108 108 'server_url': None,
109 'is_shadow_repo': False,
110 'hooks_module': 'rhodecode.lib.hooks_daemon',
109 'user_agent': 'ssh-user-agent',
111 110 'hooks': ['push', 'pull'],
111 'hooks_module': 'rhodecode.lib.hooks_daemon',
112 'is_shadow_repo': False,
113 'detect_force_push': False,
114 'check_branch_perms': False,
115
112 116 'SSH': True,
113 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name)
117 'SSH_PERMISSIONS': self.user_permissions.get(self.repo_name),
114 118 }
115 119 if extras:
116 120 scm_data.update(extras)
117 121 os.putenv("RC_SCM_DATA", json.dumps(scm_data))
118 122
119 123 def get_root_store(self):
120 124 root_store = self.store
121 125 if not root_store.endswith('/'):
122 126 # always append trailing slash
123 127 root_store = root_store + '/'
124 128 return root_store
125 129
126 130 def _handle_tunnel(self, extras):
127 131 # pre-auth
128 132 action = 'pull'
129 133 exit_code = self._check_permissions(action)
130 134 if exit_code:
131 135 return exit_code, False
132 136
133 137 req = self.env['request']
134 138 server_url = req.host_url + req.script_name
135 139 extras['server_url'] = server_url
136 140
137 141 log.debug('Using %s binaries from path %s', self.backend, self._path)
138 142 exit_code = self.tunnel.run(extras)
139 143
140 144 return exit_code, action == "push"
141 145
142 def run(self):
146 def run(self, tunnel_extras=None):
147 tunnel_extras = tunnel_extras or {}
143 148 extras = {}
149 extras.update(tunnel_extras)
144 150
145 151 callback_daemon, extras = prepare_callback_daemon(
146 152 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
147 153 host=vcs_settings.HOOKS_HOST,
148 154 use_direct_calls=False)
149 155
150 156 with callback_daemon:
151 157 try:
152 158 return self._handle_tunnel(extras)
153 159 finally:
154 160 log.debug('Running cleanup with cache invalidation')
155 161 if self.repo_name:
156 162 self._invalidate_cache(self.repo_name)
@@ -1,146 +1,149 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 json
22 22 import mock
23 23 import pytest
24 24
25 25 from rhodecode.apps.ssh_support.lib.backends.git import GitServer
26 26 from rhodecode.apps.ssh_support.tests.conftest import dummy_env, dummy_user
27 27
28 28
29 29 class GitServerCreator(object):
30 30 root = '/tmp/repo/path/'
31 31 git_path = '/usr/local/bin/git'
32 32 config_data = {
33 33 'app:main': {
34 34 'ssh.executable.git': git_path,
35 35 'vcs.hooks.protocol': 'http',
36 36 }
37 37 }
38 38 repo_name = 'test_git'
39 39 repo_mode = 'receive-pack'
40 40 user = dummy_user()
41 41
42 42 def __init__(self):
43 43 def config_get(part, key):
44 44 return self.config_data.get(part, {}).get(key)
45 45 self.config_mock = mock.Mock()
46 46 self.config_mock.get = mock.Mock(side_effect=config_get)
47 47
48 48 def create(self, **kwargs):
49 49 parameters = {
50 50 'store': self.root,
51 51 'ini_path': '',
52 52 'user': self.user,
53 53 'repo_name': self.repo_name,
54 54 'repo_mode': self.repo_mode,
55 55 'user_permissions': {
56 56 self.repo_name: 'repository.admin'
57 57 },
58 58 'config': self.config_mock,
59 59 'env': dummy_env()
60 60 }
61 61 parameters.update(kwargs)
62 62 server = GitServer(**parameters)
63 63 return server
64 64
65 65
66 66 @pytest.fixture
67 67 def git_server(app):
68 68 return GitServerCreator()
69 69
70 70
71 71 class TestGitServer(object):
72 72
73 73 def test_command(self, git_server):
74 74 server = git_server.create()
75 75 expected_command = (
76 76 'cd {root}; {git_path} {repo_mode} \'{root}{repo_name}\''.format(
77 77 root=git_server.root, git_path=git_server.git_path,
78 78 repo_mode=git_server.repo_mode, repo_name=git_server.repo_name)
79 79 )
80 80 assert expected_command == server.tunnel.command()
81 81
82 82 @pytest.mark.parametrize('permissions, action, code', [
83 83 ({}, 'pull', -2),
84 84 ({'test_git': 'repository.read'}, 'pull', 0),
85 85 ({'test_git': 'repository.read'}, 'push', -2),
86 86 ({'test_git': 'repository.write'}, 'push', 0),
87 87 ({'test_git': 'repository.admin'}, 'push', 0),
88 88
89 89 ])
90 90 def test_permission_checks(self, git_server, permissions, action, code):
91 91 server = git_server.create(user_permissions=permissions)
92 92 result = server._check_permissions(action)
93 93 assert result is code
94 94
95 95 @pytest.mark.parametrize('permissions, value', [
96 96 ({}, False),
97 97 ({'test_git': 'repository.read'}, False),
98 98 ({'test_git': 'repository.write'}, True),
99 99 ({'test_git': 'repository.admin'}, True),
100 100
101 101 ])
102 102 def test_has_write_permissions(self, git_server, permissions, value):
103 103 server = git_server.create(user_permissions=permissions)
104 104 result = server.has_write_perm()
105 105 assert result is value
106 106
107 107 def test_run_returns_executes_command(self, git_server):
108 108 server = git_server.create()
109 109 from rhodecode.apps.ssh_support.lib.backends.git import GitTunnelWrapper
110 110 with mock.patch.object(GitTunnelWrapper, 'create_hooks_env') as _patch:
111 111 _patch.return_value = 0
112 112 with mock.patch.object(GitTunnelWrapper, 'command', return_value='date'):
113 113 exit_code = server.run()
114 114
115 115 assert exit_code == (0, False)
116 116
117 117 @pytest.mark.parametrize(
118 118 'repo_mode, action', [
119 119 ['receive-pack', 'push'],
120 120 ['upload-pack', 'pull']
121 121 ])
122 122 def test_update_environment(self, git_server, repo_mode, action):
123 123 server = git_server.create(repo_mode=repo_mode)
124 124 with mock.patch('os.environ', {'SSH_CLIENT': '10.10.10.10 b'}):
125 125 with mock.patch('os.putenv') as putenv_mock:
126 126 server.update_environment(action)
127 127
128 128 expected_data = {
129 129 'username': git_server.user.username,
130 130 'user_id': git_server.user.user_id,
131 131 'scm': 'git',
132 132 'repository': git_server.repo_name,
133 133 'make_lock': None,
134 134 'action': action,
135 135 'ip': '10.10.10.10',
136 136 'locked_by': [None, None],
137 137 'config': '',
138 138 'server_url': None,
139 139 'hooks': ['push', 'pull'],
140 140 'is_shadow_repo': False,
141 141 'hooks_module': 'rhodecode.lib.hooks_daemon',
142 'check_branch_perms': False,
143 'detect_force_push': False,
144 'user_agent': u'ssh-user-agent',
142 145 'SSH': True,
143 146 'SSH_PERMISSIONS': 'repository.admin',
144 147 }
145 148 args, kwargs = putenv_mock.call_args
146 149 assert json.loads(args[1]) == expected_data
@@ -1,54 +1,54 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-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 pytest
22 22
23 23
24 24 class TestSSHWrapper(object):
25 25
26 26 def test_serve_raises_an_exception_when_vcs_is_not_recognized(self, ssh_wrapper):
27 27 with pytest.raises(Exception) as exc_info:
28 28 ssh_wrapper.serve(
29 29 vcs='microsoft-tfs', repo='test-repo', mode=None, user='test',
30 permissions={})
30 permissions={}, branch_permissions={})
31 31 assert exc_info.value.message == 'Unrecognised VCS: microsoft-tfs'
32 32
33 33 def test_parse_config(self, ssh_wrapper):
34 34 config = ssh_wrapper.parse_config(ssh_wrapper.ini_path)
35 35 assert config
36 36
37 37 def test_get_connection_info(self, ssh_wrapper):
38 38 conn_info = ssh_wrapper.get_connection_info()
39 39 assert {'client_ip': '127.0.0.1',
40 40 'client_port': '22',
41 41 'server_ip': '10.0.0.1',
42 42 'server_port': '443'} == conn_info
43 43
44 44 @pytest.mark.parametrize('command, vcs', [
45 45 ('xxx', None),
46 46 ('svnserve -t', 'svn'),
47 47 ('hg -R repo serve --stdio', 'hg'),
48 48 ('git-receive-pack \'repo.git\'', 'git'),
49 49
50 50 ])
51 51 def test_get_repo_details(self, ssh_wrapper, command, vcs):
52 52 ssh_wrapper.command = command
53 53 vcs_type, repo_name, mode = ssh_wrapper.get_repo_details(mode='auto')
54 54 assert vcs_type == vcs
@@ -1,2343 +1,2342 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 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 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 443 if self.user_is_admin and not self.calculate_super_admin_as_user:
444 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 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 482 # since super-admin also can have custom rule permissions
483 483 # we *always* need to calculate those inherited from default, and also explicit
484 484 self._calculate_default_permissions_repository_branches(
485 485 user_inherit_object_permissions=False)
486 486 self._calculate_repository_branch_permissions()
487 487
488 488 return self._permission_structure()
489 489
490 490 def _calculate_global_default_permissions(self):
491 491 """
492 492 global permissions taken from the default user
493 493 """
494 494 default_global_perms = UserToPerm.query()\
495 495 .filter(UserToPerm.user_id == self.default_user_id)\
496 496 .options(joinedload(UserToPerm.permission))
497 497
498 498 for perm in default_global_perms:
499 499 self.permissions_global.add(perm.permission.permission_name)
500 500
501 501 if self.user_is_admin:
502 502 self.permissions_global.add('hg.admin')
503 503 self.permissions_global.add('hg.create.write_on_repogroup.true')
504 504
505 505 def _calculate_global_permissions(self):
506 506 """
507 507 Set global system permissions with user permissions or permissions
508 508 taken from the user groups of the current user.
509 509
510 510 The permissions include repo creating, repo group creating, forking
511 511 etc.
512 512 """
513 513
514 514 # now we read the defined permissions and overwrite what we have set
515 515 # before those can be configured from groups or users explicitly.
516 516
517 517 # In case we want to extend this list we should make sure
518 518 # this is in sync with User.DEFAULT_USER_PERMISSIONS definitions
519 519 _configurable = frozenset([
520 520 'hg.fork.none', 'hg.fork.repository',
521 521 'hg.create.none', 'hg.create.repository',
522 522 'hg.usergroup.create.false', 'hg.usergroup.create.true',
523 523 'hg.repogroup.create.false', 'hg.repogroup.create.true',
524 524 'hg.create.write_on_repogroup.false', 'hg.create.write_on_repogroup.true',
525 525 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
526 526 ])
527 527
528 528 # USER GROUPS comes first user group global permissions
529 529 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
530 530 .options(joinedload(UserGroupToPerm.permission))\
531 531 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
532 532 UserGroupMember.users_group_id))\
533 533 .filter(UserGroupMember.user_id == self.user_id)\
534 534 .order_by(UserGroupToPerm.users_group_id)\
535 535 .all()
536 536
537 537 # need to group here by groups since user can be in more than
538 538 # one group, so we get all groups
539 539 _explicit_grouped_perms = [
540 540 [x, list(y)] for x, y in
541 541 itertools.groupby(user_perms_from_users_groups,
542 542 lambda _x: _x.users_group)]
543 543
544 544 for gr, perms in _explicit_grouped_perms:
545 545 # since user can be in multiple groups iterate over them and
546 546 # select the lowest permissions first (more explicit)
547 547 # TODO(marcink): do this^^
548 548
549 549 # group doesn't inherit default permissions so we actually set them
550 550 if not gr.inherit_default_permissions:
551 551 # NEED TO IGNORE all previously set configurable permissions
552 552 # and replace them with explicitly set from this user
553 553 # group permissions
554 554 self.permissions_global = self.permissions_global.difference(
555 555 _configurable)
556 556 for perm in perms:
557 557 self.permissions_global.add(perm.permission.permission_name)
558 558
559 559 # user explicit global permissions
560 560 user_perms = Session().query(UserToPerm)\
561 561 .options(joinedload(UserToPerm.permission))\
562 562 .filter(UserToPerm.user_id == self.user_id).all()
563 563
564 564 if not self.inherit_default_permissions:
565 565 # NEED TO IGNORE all configurable permissions and
566 566 # replace them with explicitly set from this user permissions
567 567 self.permissions_global = self.permissions_global.difference(
568 568 _configurable)
569 569 for perm in user_perms:
570 570 self.permissions_global.add(perm.permission.permission_name)
571 571
572 572 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
573 573 for perm in self.default_repo_perms:
574 574 r_k = perm.UserRepoToPerm.repository.repo_name
575 575 p = perm.Permission.permission_name
576 576 o = PermOrigin.REPO_DEFAULT
577 577 self.permissions_repositories[r_k] = p, o
578 578
579 579 # if we decide this user isn't inheriting permissions from
580 580 # default user we set him to .none so only explicit
581 581 # permissions work
582 582 if not user_inherit_object_permissions:
583 583 p = 'repository.none'
584 584 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
585 585 self.permissions_repositories[r_k] = p, o
586 586
587 587 if perm.Repository.private and not (
588 588 perm.Repository.user_id == self.user_id):
589 589 # disable defaults for private repos,
590 590 p = 'repository.none'
591 591 o = PermOrigin.REPO_PRIVATE
592 592 self.permissions_repositories[r_k] = p, o
593 593
594 594 elif perm.Repository.user_id == self.user_id:
595 595 # set admin if owner
596 596 p = 'repository.admin'
597 597 o = PermOrigin.REPO_OWNER
598 598 self.permissions_repositories[r_k] = p, o
599 599
600 600 if self.user_is_admin:
601 601 p = 'repository.admin'
602 602 o = PermOrigin.SUPER_ADMIN
603 603 self.permissions_repositories[r_k] = p, o
604 604
605 605 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
606 606 for perm in self.default_branch_repo_perms:
607 607
608 608 r_k = perm.UserRepoToPerm.repository.repo_name
609 609 p = perm.Permission.permission_name
610 610 pattern = perm.UserToRepoBranchPermission.branch_pattern
611 611 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
612 612
613 613 if not self.explicit:
614 614 # TODO(marcink): fix this for multiple entries
615 615 cur_perm = self.permissions_repository_branches.get(r_k) or 'branch.none'
616 616 p = self._choose_permission(p, cur_perm)
617 617
618 618 # NOTE(marcink): register all pattern/perm instances in this
619 619 # special dict that aggregates entries
620 620 self.permissions_repository_branches[r_k] = pattern, p, o
621 621
622 622 def _calculate_default_permissions_repository_groups(self, user_inherit_object_permissions):
623 623 for perm in self.default_repo_groups_perms:
624 624 rg_k = perm.UserRepoGroupToPerm.group.group_name
625 625 p = perm.Permission.permission_name
626 626 o = PermOrigin.REPOGROUP_DEFAULT
627 627 self.permissions_repository_groups[rg_k] = p, o
628 628
629 629 # if we decide this user isn't inheriting permissions from default
630 630 # user we set him to .none so only explicit permissions work
631 631 if not user_inherit_object_permissions:
632 632 p = 'group.none'
633 633 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
634 634 self.permissions_repository_groups[rg_k] = p, o
635 635
636 636 if perm.RepoGroup.user_id == self.user_id:
637 637 # set admin if owner
638 638 p = 'group.admin'
639 639 o = PermOrigin.REPOGROUP_OWNER
640 640 self.permissions_repository_groups[rg_k] = p, o
641 641
642 642 if self.user_is_admin:
643 643 p = 'group.admin'
644 644 o = PermOrigin.SUPER_ADMIN
645 645 self.permissions_repository_groups[rg_k] = p, o
646 646
647 647 def _calculate_default_permissions_user_groups(self, user_inherit_object_permissions):
648 648 for perm in self.default_user_group_perms:
649 649 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
650 650 p = perm.Permission.permission_name
651 651 o = PermOrigin.USERGROUP_DEFAULT
652 652 self.permissions_user_groups[u_k] = p, o
653 653
654 654 # if we decide this user isn't inheriting permissions from default
655 655 # user we set him to .none so only explicit permissions work
656 656 if not user_inherit_object_permissions:
657 657 p = 'usergroup.none'
658 658 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
659 659 self.permissions_user_groups[u_k] = p, o
660 660
661 661 if perm.UserGroup.user_id == self.user_id:
662 662 # set admin if owner
663 663 p = 'usergroup.admin'
664 664 o = PermOrigin.USERGROUP_OWNER
665 665 self.permissions_user_groups[u_k] = p, o
666 666
667 667 if self.user_is_admin:
668 668 p = 'usergroup.admin'
669 669 o = PermOrigin.SUPER_ADMIN
670 670 self.permissions_user_groups[u_k] = p, o
671 671
672 672 def _calculate_default_permissions(self):
673 673 """
674 674 Set default user permissions for repositories, repository branches,
675 675 repository groups, user groups taken from the default user.
676 676
677 677 Calculate inheritance of object permissions based on what we have now
678 678 in GLOBAL permissions. We check if .false is in GLOBAL since this is
679 679 explicitly set. Inherit is the opposite of .false being there.
680 680
681 681 .. note::
682 682
683 683 the syntax is little bit odd but what we need to check here is
684 684 the opposite of .false permission being in the list so even for
685 685 inconsistent state when both .true/.false is there
686 686 .false is more important
687 687
688 688 """
689 689 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
690 690 in self.permissions_global)
691 691
692 692 # default permissions inherited from `default` user permissions
693 693 self._calculate_default_permissions_repositories(
694 694 user_inherit_object_permissions)
695 695
696 696 self._calculate_default_permissions_repository_branches(
697 697 user_inherit_object_permissions)
698 698
699 699 self._calculate_default_permissions_repository_groups(
700 700 user_inherit_object_permissions)
701 701
702 702 self._calculate_default_permissions_user_groups(
703 703 user_inherit_object_permissions)
704 704
705 705 def _calculate_repository_permissions(self):
706 706 """
707 707 Repository permissions for the current user.
708 708
709 709 Check if the user is part of user groups for this repository and
710 710 fill in the permission from it. `_choose_permission` decides of which
711 711 permission should be selected based on selected method.
712 712 """
713 713
714 714 # user group for repositories permissions
715 715 user_repo_perms_from_user_group = Permission\
716 716 .get_default_repo_perms_from_user_group(
717 717 self.user_id, self.scope_repo_id)
718 718
719 719 multiple_counter = collections.defaultdict(int)
720 720 for perm in user_repo_perms_from_user_group:
721 721 r_k = perm.UserGroupRepoToPerm.repository.repo_name
722 722 multiple_counter[r_k] += 1
723 723 p = perm.Permission.permission_name
724 724 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
725 725 .users_group.users_group_name
726 726
727 727 if multiple_counter[r_k] > 1:
728 728 cur_perm = self.permissions_repositories[r_k]
729 729 p = self._choose_permission(p, cur_perm)
730 730
731 731 self.permissions_repositories[r_k] = p, o
732 732
733 733 if perm.Repository.user_id == self.user_id:
734 734 # set admin if owner
735 735 p = 'repository.admin'
736 736 o = PermOrigin.REPO_OWNER
737 737 self.permissions_repositories[r_k] = p, o
738 738
739 739 if self.user_is_admin:
740 740 p = 'repository.admin'
741 741 o = PermOrigin.SUPER_ADMIN
742 742 self.permissions_repositories[r_k] = p, o
743 743
744 744 # user explicit permissions for repositories, overrides any specified
745 745 # by the group permission
746 746 user_repo_perms = Permission.get_default_repo_perms(
747 747 self.user_id, self.scope_repo_id)
748 748 for perm in user_repo_perms:
749 749 r_k = perm.UserRepoToPerm.repository.repo_name
750 750 p = perm.Permission.permission_name
751 751 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
752 752
753 753 if not self.explicit:
754 754 cur_perm = self.permissions_repositories.get(
755 755 r_k, 'repository.none')
756 756 p = self._choose_permission(p, cur_perm)
757 757
758 758 self.permissions_repositories[r_k] = p, o
759 759
760 760 if perm.Repository.user_id == self.user_id:
761 761 # set admin if owner
762 762 p = 'repository.admin'
763 763 o = PermOrigin.REPO_OWNER
764 764 self.permissions_repositories[r_k] = p, o
765 765
766 766 if self.user_is_admin:
767 767 p = 'repository.admin'
768 768 o = PermOrigin.SUPER_ADMIN
769 769 self.permissions_repositories[r_k] = p, o
770 770
771 771 def _calculate_repository_branch_permissions(self):
772 772 # user group for repositories permissions
773 773 user_repo_branch_perms_from_user_group = Permission\
774 774 .get_default_repo_branch_perms_from_user_group(
775 775 self.user_id, self.scope_repo_id)
776 776
777 777 multiple_counter = collections.defaultdict(int)
778 778 for perm in user_repo_branch_perms_from_user_group:
779 779 r_k = perm.UserGroupRepoToPerm.repository.repo_name
780 780 p = perm.Permission.permission_name
781 781 pattern = perm.UserGroupToRepoBranchPermission.branch_pattern
782 782 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
783 783 .users_group.users_group_name
784 784
785 785 multiple_counter[r_k] += 1
786 786 if multiple_counter[r_k] > 1:
787 787 # TODO(marcink): fix this for multi branch support, and multiple entries
788 788 cur_perm = self.permissions_repository_branches[r_k]
789 789 p = self._choose_permission(p, cur_perm)
790 790
791 791 self.permissions_repository_branches[r_k] = pattern, p, o
792 792
793 793 # user explicit branch permissions for repositories, overrides
794 794 # any specified by the group permission
795 795 user_repo_branch_perms = Permission.get_default_repo_branch_perms(
796 796 self.user_id, self.scope_repo_id)
797 797
798 798 for perm in user_repo_branch_perms:
799 799
800 800 r_k = perm.UserRepoToPerm.repository.repo_name
801 801 p = perm.Permission.permission_name
802 802 pattern = perm.UserToRepoBranchPermission.branch_pattern
803 803 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
804 804
805 805 if not self.explicit:
806 806 # TODO(marcink): fix this for multiple entries
807 807 cur_perm = self.permissions_repository_branches.get(r_k) or 'branch.none'
808 808 p = self._choose_permission(p, cur_perm)
809 809
810 810 # NOTE(marcink): register all pattern/perm instances in this
811 811 # special dict that aggregates entries
812 812 self.permissions_repository_branches[r_k] = pattern, p, o
813 813
814 814 def _calculate_repository_group_permissions(self):
815 815 """
816 816 Repository group permissions for the current user.
817 817
818 818 Check if the user is part of user groups for repository groups and
819 819 fill in the permissions from it. `_choose_permission` decides of which
820 820 permission should be selected based on selected method.
821 821 """
822 822 # user group for repo groups permissions
823 823 user_repo_group_perms_from_user_group = Permission\
824 824 .get_default_group_perms_from_user_group(
825 825 self.user_id, self.scope_repo_group_id)
826 826
827 827 multiple_counter = collections.defaultdict(int)
828 828 for perm in user_repo_group_perms_from_user_group:
829 829 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
830 830 multiple_counter[rg_k] += 1
831 831 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
832 832 .users_group.users_group_name
833 833 p = perm.Permission.permission_name
834 834
835 835 if multiple_counter[rg_k] > 1:
836 836 cur_perm = self.permissions_repository_groups[rg_k]
837 837 p = self._choose_permission(p, cur_perm)
838 838 self.permissions_repository_groups[rg_k] = p, o
839 839
840 840 if perm.RepoGroup.user_id == self.user_id:
841 841 # set admin if owner, even for member of other user group
842 842 p = 'group.admin'
843 843 o = PermOrigin.REPOGROUP_OWNER
844 844 self.permissions_repository_groups[rg_k] = p, o
845 845
846 846 if self.user_is_admin:
847 847 p = 'group.admin'
848 848 o = PermOrigin.SUPER_ADMIN
849 849 self.permissions_repository_groups[rg_k] = p, o
850 850
851 851 # user explicit permissions for repository groups
852 852 user_repo_groups_perms = Permission.get_default_group_perms(
853 853 self.user_id, self.scope_repo_group_id)
854 854 for perm in user_repo_groups_perms:
855 855 rg_k = perm.UserRepoGroupToPerm.group.group_name
856 856 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
857 857 .user.username
858 858 p = perm.Permission.permission_name
859 859
860 860 if not self.explicit:
861 861 cur_perm = self.permissions_repository_groups.get(
862 862 rg_k, 'group.none')
863 863 p = self._choose_permission(p, cur_perm)
864 864
865 865 self.permissions_repository_groups[rg_k] = p, o
866 866
867 867 if perm.RepoGroup.user_id == self.user_id:
868 868 # set admin if owner
869 869 p = 'group.admin'
870 870 o = PermOrigin.REPOGROUP_OWNER
871 871 self.permissions_repository_groups[rg_k] = p, o
872 872
873 873 if self.user_is_admin:
874 874 p = 'group.admin'
875 875 o = PermOrigin.SUPER_ADMIN
876 876 self.permissions_repository_groups[rg_k] = p, o
877 877
878 878 def _calculate_user_group_permissions(self):
879 879 """
880 880 User group permissions for the current user.
881 881 """
882 882 # user group for user group permissions
883 883 user_group_from_user_group = Permission\
884 884 .get_default_user_group_perms_from_user_group(
885 885 self.user_id, self.scope_user_group_id)
886 886
887 887 multiple_counter = collections.defaultdict(int)
888 888 for perm in user_group_from_user_group:
889 889 ug_k = perm.UserGroupUserGroupToPerm\
890 890 .target_user_group.users_group_name
891 891 multiple_counter[ug_k] += 1
892 892 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
893 893 .user_group.users_group_name
894 894 p = perm.Permission.permission_name
895 895
896 896 if multiple_counter[ug_k] > 1:
897 897 cur_perm = self.permissions_user_groups[ug_k]
898 898 p = self._choose_permission(p, cur_perm)
899 899
900 900 self.permissions_user_groups[ug_k] = p, o
901 901
902 902 if perm.UserGroup.user_id == self.user_id:
903 903 # set admin if owner, even for member of other user group
904 904 p = 'usergroup.admin'
905 905 o = PermOrigin.USERGROUP_OWNER
906 906 self.permissions_user_groups[ug_k] = p, o
907 907
908 908 if self.user_is_admin:
909 909 p = 'usergroup.admin'
910 910 o = PermOrigin.SUPER_ADMIN
911 911 self.permissions_user_groups[ug_k] = p, o
912 912
913 913 # user explicit permission for user groups
914 914 user_user_groups_perms = Permission.get_default_user_group_perms(
915 915 self.user_id, self.scope_user_group_id)
916 916 for perm in user_user_groups_perms:
917 917 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
918 918 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
919 919 .user.username
920 920 p = perm.Permission.permission_name
921 921
922 922 if not self.explicit:
923 923 cur_perm = self.permissions_user_groups.get(
924 924 ug_k, 'usergroup.none')
925 925 p = self._choose_permission(p, cur_perm)
926 926
927 927 self.permissions_user_groups[ug_k] = p, o
928 928
929 929 if perm.UserGroup.user_id == self.user_id:
930 930 # set admin if owner
931 931 p = 'usergroup.admin'
932 932 o = PermOrigin.USERGROUP_OWNER
933 933 self.permissions_user_groups[ug_k] = p, o
934 934
935 935 if self.user_is_admin:
936 936 p = 'usergroup.admin'
937 937 o = PermOrigin.SUPER_ADMIN
938 938 self.permissions_user_groups[ug_k] = p, o
939 939
940 940 def _choose_permission(self, new_perm, cur_perm):
941 941 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
942 942 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
943 943 if self.algo == 'higherwin':
944 944 if new_perm_val > cur_perm_val:
945 945 return new_perm
946 946 return cur_perm
947 947 elif self.algo == 'lowerwin':
948 948 if new_perm_val < cur_perm_val:
949 949 return new_perm
950 950 return cur_perm
951 951
952 952 def _permission_structure(self):
953 953 return {
954 954 'global': self.permissions_global,
955 955 'repositories': self.permissions_repositories,
956 956 'repository_branches': self.permissions_repository_branches,
957 957 'repositories_groups': self.permissions_repository_groups,
958 958 'user_groups': self.permissions_user_groups,
959 959 }
960 960
961 961
962 962 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
963 963 """
964 964 Check if given controller_name is in whitelist of auth token access
965 965 """
966 966 if not whitelist:
967 967 from rhodecode import CONFIG
968 968 whitelist = aslist(
969 969 CONFIG.get('api_access_controllers_whitelist'), sep=',')
970 970 # backward compat translation
971 971 compat = {
972 972 # old controller, new VIEW
973 973 'ChangesetController:*': 'RepoCommitsView:*',
974 974 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
975 975 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
976 976 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
977 977 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
978 978 'GistsController:*': 'GistView:*',
979 979 }
980 980
981 981 log.debug(
982 982 'Allowed views for AUTH TOKEN access: %s' % (whitelist,))
983 983 auth_token_access_valid = False
984 984
985 985 for entry in whitelist:
986 986 token_match = True
987 987 if entry in compat:
988 988 # translate from old Controllers to Pyramid Views
989 989 entry = compat[entry]
990 990
991 991 if '@' in entry:
992 992 # specific AuthToken
993 993 entry, allowed_token = entry.split('@', 1)
994 994 token_match = auth_token == allowed_token
995 995
996 996 if fnmatch.fnmatch(view_name, entry) and token_match:
997 997 auth_token_access_valid = True
998 998 break
999 999
1000 1000 if auth_token_access_valid:
1001 1001 log.debug('view: `%s` matches entry in whitelist: %s'
1002 1002 % (view_name, whitelist))
1003 1003 else:
1004 1004 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
1005 1005 % (view_name, whitelist))
1006 1006 if auth_token:
1007 1007 # if we use auth token key and don't have access it's a warning
1008 1008 log.warning(msg)
1009 1009 else:
1010 1010 log.debug(msg)
1011 1011
1012 1012 return auth_token_access_valid
1013 1013
1014 1014
1015 1015 class AuthUser(object):
1016 1016 """
1017 1017 A simple object that handles all attributes of user in RhodeCode
1018 1018
1019 1019 It does lookup based on API key,given user, or user present in session
1020 1020 Then it fills all required information for such user. It also checks if
1021 1021 anonymous access is enabled and if so, it returns default user as logged in
1022 1022 """
1023 1023 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
1024 1024
1025 1025 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
1026 1026
1027 1027 self.user_id = user_id
1028 1028 self._api_key = api_key
1029 1029
1030 1030 self.api_key = None
1031 1031 self.username = username
1032 1032 self.ip_addr = ip_addr
1033 1033 self.name = ''
1034 1034 self.lastname = ''
1035 1035 self.first_name = ''
1036 1036 self.last_name = ''
1037 1037 self.email = ''
1038 1038 self.is_authenticated = False
1039 1039 self.admin = False
1040 1040 self.inherit_default_permissions = False
1041 1041 self.password = ''
1042 1042
1043 1043 self.anonymous_user = None # propagated on propagate_data
1044 1044 self.propagate_data()
1045 1045 self._instance = None
1046 1046 self._permissions_scoped_cache = {} # used to bind scoped calculation
1047 1047
1048 1048 @LazyProperty
1049 1049 def permissions(self):
1050 1050 return self.get_perms(user=self, cache=None)
1051 1051
1052 1052 @LazyProperty
1053 1053 def permissions_safe(self):
1054 1054 """
1055 1055 Filtered permissions excluding not allowed repositories
1056 1056 """
1057 1057 perms = self.get_perms(user=self, cache=None)
1058 1058
1059 1059 perms['repositories'] = {
1060 1060 k: v for k, v in perms['repositories'].items()
1061 1061 if v != 'repository.none'}
1062 1062 perms['repositories_groups'] = {
1063 1063 k: v for k, v in perms['repositories_groups'].items()
1064 1064 if v != 'group.none'}
1065 1065 perms['user_groups'] = {
1066 1066 k: v for k, v in perms['user_groups'].items()
1067 1067 if v != 'usergroup.none'}
1068 1068 perms['repository_branches'] = {
1069 1069 k: v for k, v in perms['repository_branches'].iteritems()
1070 1070 if v != 'branch.none'}
1071 1071 return perms
1072 1072
1073 1073 @LazyProperty
1074 1074 def permissions_full_details(self):
1075 1075 return self.get_perms(
1076 1076 user=self, cache=None, calculate_super_admin=True)
1077 1077
1078 1078 def permissions_with_scope(self, scope):
1079 1079 """
1080 1080 Call the get_perms function with scoped data. The scope in that function
1081 1081 narrows the SQL calls to the given ID of objects resulting in fetching
1082 1082 Just particular permission we want to obtain. If scope is an empty dict
1083 1083 then it basically narrows the scope to GLOBAL permissions only.
1084 1084
1085 1085 :param scope: dict
1086 1086 """
1087 1087 if 'repo_name' in scope:
1088 1088 obj = Repository.get_by_repo_name(scope['repo_name'])
1089 1089 if obj:
1090 1090 scope['repo_id'] = obj.repo_id
1091 1091 _scope = collections.OrderedDict()
1092 1092 _scope['repo_id'] = -1
1093 1093 _scope['user_group_id'] = -1
1094 1094 _scope['repo_group_id'] = -1
1095 1095
1096 1096 for k in sorted(scope.keys()):
1097 1097 _scope[k] = scope[k]
1098 1098
1099 1099 # store in cache to mimic how the @LazyProperty works,
1100 1100 # the difference here is that we use the unique key calculated
1101 1101 # from params and values
1102 1102 return self.get_perms(user=self, cache=None, scope=_scope)
1103 1103
1104 1104 def get_instance(self):
1105 1105 return User.get(self.user_id)
1106 1106
1107 1107 def propagate_data(self):
1108 1108 """
1109 1109 Fills in user data and propagates values to this instance. Maps fetched
1110 1110 user attributes to this class instance attributes
1111 1111 """
1112 1112 log.debug('AuthUser: starting data propagation for new potential user')
1113 1113 user_model = UserModel()
1114 1114 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1115 1115 is_user_loaded = False
1116 1116
1117 1117 # lookup by userid
1118 1118 if self.user_id is not None and self.user_id != anon_user.user_id:
1119 1119 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1120 1120 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1121 1121
1122 1122 # try go get user by api key
1123 1123 elif self._api_key and self._api_key != anon_user.api_key:
1124 1124 log.debug('Trying Auth User lookup by API KEY: `%s`', self._api_key)
1125 1125 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1126 1126
1127 1127 # lookup by username
1128 1128 elif self.username:
1129 1129 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1130 1130 is_user_loaded = user_model.fill_data(self, username=self.username)
1131 1131 else:
1132 1132 log.debug('No data in %s that could been used to log in', self)
1133 1133
1134 1134 if not is_user_loaded:
1135 1135 log.debug(
1136 1136 'Failed to load user. Fallback to default user %s', anon_user)
1137 1137 # if we cannot authenticate user try anonymous
1138 1138 if anon_user.active:
1139 1139 log.debug('default user is active, using it as a session user')
1140 1140 user_model.fill_data(self, user_id=anon_user.user_id)
1141 1141 # then we set this user is logged in
1142 1142 self.is_authenticated = True
1143 1143 else:
1144 1144 log.debug('default user is NOT active')
1145 1145 # in case of disabled anonymous user we reset some of the
1146 1146 # parameters so such user is "corrupted", skipping the fill_data
1147 1147 for attr in ['user_id', 'username', 'admin', 'active']:
1148 1148 setattr(self, attr, None)
1149 1149 self.is_authenticated = False
1150 1150
1151 1151 if not self.username:
1152 1152 self.username = 'None'
1153 1153
1154 1154 log.debug('AuthUser: propagated user is now %s', self)
1155 1155
1156 1156 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1157 1157 calculate_super_admin=False, cache=None):
1158 1158 """
1159 1159 Fills user permission attribute with permissions taken from database
1160 1160 works for permissions given for repositories, and for permissions that
1161 1161 are granted to groups
1162 1162
1163 1163 :param user: instance of User object from database
1164 1164 :param explicit: In case there are permissions both for user and a group
1165 1165 that user is part of, explicit flag will defiine if user will
1166 1166 explicitly override permissions from group, if it's False it will
1167 1167 make decision based on the algo
1168 1168 :param algo: algorithm to decide what permission should be choose if
1169 1169 it's multiple defined, eg user in two different groups. It also
1170 1170 decides if explicit flag is turned off how to specify the permission
1171 1171 for case when user is in a group + have defined separate permission
1172 1172 :param calculate_super_admin: calculate permissions for super-admin in the
1173 1173 same way as for regular user without speedups
1174 1174 :param cache: Use caching for calculation, None = let the cache backend decide
1175 1175 """
1176 1176 user_id = user.user_id
1177 1177 user_is_admin = user.is_admin
1178 1178
1179 1179 # inheritance of global permissions like create repo/fork repo etc
1180 1180 user_inherit_default_permissions = user.inherit_default_permissions
1181 1181
1182 1182 cache_seconds = safe_int(
1183 1183 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1184 1184
1185 1185 if cache is None:
1186 1186 # let the backend cache decide
1187 1187 cache_on = cache_seconds > 0
1188 1188 else:
1189 1189 cache_on = cache
1190 1190
1191 1191 log.debug(
1192 1192 'Computing PERMISSION tree for user %s scope `%s` '
1193 1193 'with caching: %s[TTL: %ss]' % (user, scope, cache_on, cache_seconds or 0))
1194 1194
1195 1195 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1196 1196 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1197 1197
1198 1198 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
1199 1199 condition=cache_on)
1200 1200 def compute_perm_tree(cache_name,
1201 1201 user_id, scope, user_is_admin,user_inherit_default_permissions,
1202 1202 explicit, algo, calculate_super_admin):
1203 1203 return _cached_perms_data(
1204 1204 user_id, scope, user_is_admin, user_inherit_default_permissions,
1205 1205 explicit, algo, calculate_super_admin)
1206 1206
1207 1207 start = time.time()
1208 1208 result = compute_perm_tree(
1209 1209 'permissions', user_id, scope, user_is_admin,
1210 1210 user_inherit_default_permissions, explicit, algo,
1211 1211 calculate_super_admin)
1212 1212
1213 1213 result_repr = []
1214 1214 for k in result:
1215 1215 result_repr.append((k, len(result[k])))
1216 1216 total = time.time() - start
1217 1217 log.debug('PERMISSION tree for user %s computed in %.3fs: %s' % (
1218 1218 user, total, result_repr))
1219 1219
1220 1220 return result
1221 1221
1222 1222 @property
1223 1223 def is_default(self):
1224 1224 return self.username == User.DEFAULT_USER
1225 1225
1226 1226 @property
1227 1227 def is_admin(self):
1228 1228 return self.admin
1229 1229
1230 1230 @property
1231 1231 def is_user_object(self):
1232 1232 return self.user_id is not None
1233 1233
1234 1234 @property
1235 1235 def repositories_admin(self):
1236 1236 """
1237 1237 Returns list of repositories you're an admin of
1238 1238 """
1239 1239 return [
1240 1240 x[0] for x in self.permissions['repositories'].items()
1241 1241 if x[1] == 'repository.admin']
1242 1242
1243 1243 @property
1244 1244 def repository_groups_admin(self):
1245 1245 """
1246 1246 Returns list of repository groups you're an admin of
1247 1247 """
1248 1248 return [
1249 1249 x[0] for x in self.permissions['repositories_groups'].items()
1250 1250 if x[1] == 'group.admin']
1251 1251
1252 1252 @property
1253 1253 def user_groups_admin(self):
1254 1254 """
1255 1255 Returns list of user groups you're an admin of
1256 1256 """
1257 1257 return [
1258 1258 x[0] for x in self.permissions['user_groups'].items()
1259 1259 if x[1] == 'usergroup.admin']
1260 1260
1261 1261 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1262 1262 """
1263 1263 Returns list of repository ids that user have access to based on given
1264 1264 perms. The cache flag should be only used in cases that are used for
1265 1265 display purposes, NOT IN ANY CASE for permission checks.
1266 1266 """
1267 1267 from rhodecode.model.scm import RepoList
1268 1268 if not perms:
1269 1269 perms = [
1270 1270 'repository.read', 'repository.write', 'repository.admin']
1271 1271
1272 1272 def _cached_repo_acl(user_id, perm_def, _name_filter):
1273 1273 qry = Repository.query()
1274 1274 if _name_filter:
1275 1275 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1276 1276 qry = qry.filter(
1277 1277 Repository.repo_name.ilike(ilike_expression))
1278 1278
1279 1279 return [x.repo_id for x in
1280 1280 RepoList(qry, perm_set=perm_def)]
1281 1281
1282 1282 return _cached_repo_acl(self.user_id, perms, name_filter)
1283 1283
1284 1284 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1285 1285 """
1286 1286 Returns list of repository group ids that user have access to based on given
1287 1287 perms. The cache flag should be only used in cases that are used for
1288 1288 display purposes, NOT IN ANY CASE for permission checks.
1289 1289 """
1290 1290 from rhodecode.model.scm import RepoGroupList
1291 1291 if not perms:
1292 1292 perms = [
1293 1293 'group.read', 'group.write', 'group.admin']
1294 1294
1295 1295 def _cached_repo_group_acl(user_id, perm_def, _name_filter):
1296 1296 qry = RepoGroup.query()
1297 1297 if _name_filter:
1298 1298 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1299 1299 qry = qry.filter(
1300 1300 RepoGroup.group_name.ilike(ilike_expression))
1301 1301
1302 1302 return [x.group_id for x in
1303 1303 RepoGroupList(qry, perm_set=perm_def)]
1304 1304
1305 1305 return _cached_repo_group_acl(self.user_id, perms, name_filter)
1306 1306
1307 1307 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1308 1308 """
1309 1309 Returns list of user group ids that user have access to based on given
1310 1310 perms. The cache flag should be only used in cases that are used for
1311 1311 display purposes, NOT IN ANY CASE for permission checks.
1312 1312 """
1313 1313 from rhodecode.model.scm import UserGroupList
1314 1314 if not perms:
1315 1315 perms = [
1316 1316 'usergroup.read', 'usergroup.write', 'usergroup.admin']
1317 1317
1318 1318 def _cached_user_group_acl(user_id, perm_def, name_filter):
1319 1319 qry = UserGroup.query()
1320 1320 if name_filter:
1321 1321 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1322 1322 qry = qry.filter(
1323 1323 UserGroup.users_group_name.ilike(ilike_expression))
1324 1324
1325 1325 return [x.users_group_id for x in
1326 1326 UserGroupList(qry, perm_set=perm_def)]
1327 1327
1328 1328 return _cached_user_group_acl(self.user_id, perms, name_filter)
1329 1329
1330 1330 @property
1331 1331 def ip_allowed(self):
1332 1332 """
1333 1333 Checks if ip_addr used in constructor is allowed from defined list of
1334 1334 allowed ip_addresses for user
1335 1335
1336 1336 :returns: boolean, True if ip is in allowed ip range
1337 1337 """
1338 1338 # check IP
1339 1339 inherit = self.inherit_default_permissions
1340 1340 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1341 1341 inherit_from_default=inherit)
1342 1342 @property
1343 1343 def personal_repo_group(self):
1344 1344 return RepoGroup.get_user_personal_repo_group(self.user_id)
1345 1345
1346 1346 @LazyProperty
1347 1347 def feed_token(self):
1348 1348 return self.get_instance().feed_token
1349 1349
1350 1350 @classmethod
1351 1351 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1352 1352 allowed_ips = AuthUser.get_allowed_ips(
1353 1353 user_id, cache=True, inherit_from_default=inherit_from_default)
1354 1354 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1355 1355 log.debug('IP:%s for user %s is in range of %s' % (
1356 1356 ip_addr, user_id, allowed_ips))
1357 1357 return True
1358 1358 else:
1359 1359 log.info('Access for IP:%s forbidden for user %s, '
1360 1360 'not in %s' % (ip_addr, user_id, allowed_ips))
1361 1361 return False
1362 1362
1363 1363 def get_branch_permissions(self, repo_name, perms=None):
1364 1364 perms = perms or self.permissions_with_scope({'repo_name': repo_name})
1365 branch_perms = perms.get('repository_branches')
1366 return branch_perms
1365 branch_perms = perms.get('repository_branches', {})
1366 if not branch_perms:
1367 return {}
1368 repo_branch_perms = branch_perms.get(repo_name)
1369 return repo_branch_perms or {}
1367 1370
1368 1371 def get_rule_and_branch_permission(self, repo_name, branch_name):
1369 1372 """
1370 1373 Check if this AuthUser has defined any permissions for branches. If any of
1371 1374 the rules match in order, we return the matching permissions
1372 1375 """
1373 1376
1374 1377 rule = default_perm = ''
1375 1378
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)
1379 repo_branch_perms = self.get_branch_permissions(repo_name=repo_name)
1381 1380 if not repo_branch_perms:
1382 1381 return rule, default_perm
1383 1382
1384 1383 # now calculate the permissions
1385 1384 for pattern, branch_perm in repo_branch_perms.items():
1386 1385 if fnmatch.fnmatch(branch_name, pattern):
1387 1386 rule = '`{}`=>{}'.format(pattern, branch_perm)
1388 1387 return rule, branch_perm
1389 1388
1390 1389 return rule, default_perm
1391 1390
1392 1391 def __repr__(self):
1393 1392 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1394 1393 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
1395 1394
1396 1395 def set_authenticated(self, authenticated=True):
1397 1396 if self.user_id != self.anonymous_user.user_id:
1398 1397 self.is_authenticated = authenticated
1399 1398
1400 1399 def get_cookie_store(self):
1401 1400 return {
1402 1401 'username': self.username,
1403 1402 'password': md5(self.password or ''),
1404 1403 'user_id': self.user_id,
1405 1404 'is_authenticated': self.is_authenticated
1406 1405 }
1407 1406
1408 1407 @classmethod
1409 1408 def from_cookie_store(cls, cookie_store):
1410 1409 """
1411 1410 Creates AuthUser from a cookie store
1412 1411
1413 1412 :param cls:
1414 1413 :param cookie_store:
1415 1414 """
1416 1415 user_id = cookie_store.get('user_id')
1417 1416 username = cookie_store.get('username')
1418 1417 api_key = cookie_store.get('api_key')
1419 1418 return AuthUser(user_id, api_key, username)
1420 1419
1421 1420 @classmethod
1422 1421 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1423 1422 _set = set()
1424 1423
1425 1424 if inherit_from_default:
1426 1425 def_user_id = User.get_default_user(cache=True).user_id
1427 1426 default_ips = UserIpMap.query().filter(UserIpMap.user_id == def_user_id)
1428 1427 if cache:
1429 1428 default_ips = default_ips.options(
1430 1429 FromCache("sql_cache_short", "get_user_ips_default"))
1431 1430
1432 1431 # populate from default user
1433 1432 for ip in default_ips:
1434 1433 try:
1435 1434 _set.add(ip.ip_addr)
1436 1435 except ObjectDeletedError:
1437 1436 # since we use heavy caching sometimes it happens that
1438 1437 # we get deleted objects here, we just skip them
1439 1438 pass
1440 1439
1441 1440 # NOTE:(marcink) we don't want to load any rules for empty
1442 1441 # user_id which is the case of access of non logged users when anonymous
1443 1442 # access is disabled
1444 1443 user_ips = []
1445 1444 if user_id:
1446 1445 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1447 1446 if cache:
1448 1447 user_ips = user_ips.options(
1449 1448 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1450 1449
1451 1450 for ip in user_ips:
1452 1451 try:
1453 1452 _set.add(ip.ip_addr)
1454 1453 except ObjectDeletedError:
1455 1454 # since we use heavy caching sometimes it happens that we get
1456 1455 # deleted objects here, we just skip them
1457 1456 pass
1458 1457 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1459 1458
1460 1459
1461 1460 def set_available_permissions(settings):
1462 1461 """
1463 1462 This function will propagate pyramid settings with all available defined
1464 1463 permission given in db. We don't want to check each time from db for new
1465 1464 permissions since adding a new permission also requires application restart
1466 1465 ie. to decorate new views with the newly created permission
1467 1466
1468 1467 :param settings: current pyramid registry.settings
1469 1468
1470 1469 """
1471 1470 log.debug('auth: getting information about all available permissions')
1472 1471 try:
1473 1472 sa = meta.Session
1474 1473 all_perms = sa.query(Permission).all()
1475 1474 settings.setdefault('available_permissions',
1476 1475 [x.permission_name for x in all_perms])
1477 1476 log.debug('auth: set available permissions')
1478 1477 except Exception:
1479 1478 log.exception('Failed to fetch permissions from the database.')
1480 1479 raise
1481 1480
1482 1481
1483 1482 def get_csrf_token(session, force_new=False, save_if_missing=True):
1484 1483 """
1485 1484 Return the current authentication token, creating one if one doesn't
1486 1485 already exist and the save_if_missing flag is present.
1487 1486
1488 1487 :param session: pass in the pyramid session, else we use the global ones
1489 1488 :param force_new: force to re-generate the token and store it in session
1490 1489 :param save_if_missing: save the newly generated token if it's missing in
1491 1490 session
1492 1491 """
1493 1492 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1494 1493 # from pyramid.csrf import get_csrf_token
1495 1494
1496 1495 if (csrf_token_key not in session and save_if_missing) or force_new:
1497 1496 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1498 1497 session[csrf_token_key] = token
1499 1498 if hasattr(session, 'save'):
1500 1499 session.save()
1501 1500 return session.get(csrf_token_key)
1502 1501
1503 1502
1504 1503 def get_request(perm_class_instance):
1505 1504 from pyramid.threadlocal import get_current_request
1506 1505 pyramid_request = get_current_request()
1507 1506 return pyramid_request
1508 1507
1509 1508
1510 1509 # CHECK DECORATORS
1511 1510 class CSRFRequired(object):
1512 1511 """
1513 1512 Decorator for authenticating a form
1514 1513
1515 1514 This decorator uses an authorization token stored in the client's
1516 1515 session for prevention of certain Cross-site request forgery (CSRF)
1517 1516 attacks (See
1518 1517 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1519 1518 information).
1520 1519
1521 1520 For use with the ``webhelpers.secure_form`` helper functions.
1522 1521
1523 1522 """
1524 1523 def __init__(self, token=csrf_token_key, header='X-CSRF-Token',
1525 1524 except_methods=None):
1526 1525 self.token = token
1527 1526 self.header = header
1528 1527 self.except_methods = except_methods or []
1529 1528
1530 1529 def __call__(self, func):
1531 1530 return get_cython_compat_decorator(self.__wrapper, func)
1532 1531
1533 1532 def _get_csrf(self, _request):
1534 1533 return _request.POST.get(self.token, _request.headers.get(self.header))
1535 1534
1536 1535 def check_csrf(self, _request, cur_token):
1537 1536 supplied_token = self._get_csrf(_request)
1538 1537 return supplied_token and supplied_token == cur_token
1539 1538
1540 1539 def _get_request(self):
1541 1540 return get_request(self)
1542 1541
1543 1542 def __wrapper(self, func, *fargs, **fkwargs):
1544 1543 request = self._get_request()
1545 1544
1546 1545 if request.method in self.except_methods:
1547 1546 return func(*fargs, **fkwargs)
1548 1547
1549 1548 cur_token = get_csrf_token(request.session, save_if_missing=False)
1550 1549 if self.check_csrf(request, cur_token):
1551 1550 if request.POST.get(self.token):
1552 1551 del request.POST[self.token]
1553 1552 return func(*fargs, **fkwargs)
1554 1553 else:
1555 1554 reason = 'token-missing'
1556 1555 supplied_token = self._get_csrf(request)
1557 1556 if supplied_token and cur_token != supplied_token:
1558 1557 reason = 'token-mismatch [%s:%s]' % (
1559 1558 cur_token or ''[:6], supplied_token or ''[:6])
1560 1559
1561 1560 csrf_message = \
1562 1561 ("Cross-site request forgery detected, request denied. See "
1563 1562 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1564 1563 "more information.")
1565 1564 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1566 1565 'REMOTE_ADDR:%s, HEADERS:%s' % (
1567 1566 request, reason, request.remote_addr, request.headers))
1568 1567
1569 1568 raise HTTPForbidden(explanation=csrf_message)
1570 1569
1571 1570
1572 1571 class LoginRequired(object):
1573 1572 """
1574 1573 Must be logged in to execute this function else
1575 1574 redirect to login page
1576 1575
1577 1576 :param api_access: if enabled this checks only for valid auth token
1578 1577 and grants access based on valid token
1579 1578 """
1580 1579 def __init__(self, auth_token_access=None):
1581 1580 self.auth_token_access = auth_token_access
1582 1581
1583 1582 def __call__(self, func):
1584 1583 return get_cython_compat_decorator(self.__wrapper, func)
1585 1584
1586 1585 def _get_request(self):
1587 1586 return get_request(self)
1588 1587
1589 1588 def __wrapper(self, func, *fargs, **fkwargs):
1590 1589 from rhodecode.lib import helpers as h
1591 1590 cls = fargs[0]
1592 1591 user = cls._rhodecode_user
1593 1592 request = self._get_request()
1594 1593 _ = request.translate
1595 1594
1596 1595 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1597 1596 log.debug('Starting login restriction checks for user: %s' % (user,))
1598 1597 # check if our IP is allowed
1599 1598 ip_access_valid = True
1600 1599 if not user.ip_allowed:
1601 1600 h.flash(h.literal(_('IP %s not allowed' % (user.ip_addr,))),
1602 1601 category='warning')
1603 1602 ip_access_valid = False
1604 1603
1605 1604 # check if we used an APIKEY and it's a valid one
1606 1605 # defined white-list of controllers which API access will be enabled
1607 1606 _auth_token = request.GET.get(
1608 1607 'auth_token', '') or request.GET.get('api_key', '')
1609 1608 auth_token_access_valid = allowed_auth_token_access(
1610 1609 loc, auth_token=_auth_token)
1611 1610
1612 1611 # explicit controller is enabled or API is in our whitelist
1613 1612 if self.auth_token_access or auth_token_access_valid:
1614 1613 log.debug('Checking AUTH TOKEN access for %s' % (cls,))
1615 1614 db_user = user.get_instance()
1616 1615
1617 1616 if db_user:
1618 1617 if self.auth_token_access:
1619 1618 roles = self.auth_token_access
1620 1619 else:
1621 1620 roles = [UserApiKeys.ROLE_HTTP]
1622 1621 token_match = db_user.authenticate_by_token(
1623 1622 _auth_token, roles=roles)
1624 1623 else:
1625 1624 log.debug('Unable to fetch db instance for auth user: %s', user)
1626 1625 token_match = False
1627 1626
1628 1627 if _auth_token and token_match:
1629 1628 auth_token_access_valid = True
1630 1629 log.debug('AUTH TOKEN ****%s is VALID' % (_auth_token[-4:],))
1631 1630 else:
1632 1631 auth_token_access_valid = False
1633 1632 if not _auth_token:
1634 1633 log.debug("AUTH TOKEN *NOT* present in request")
1635 1634 else:
1636 1635 log.warning(
1637 1636 "AUTH TOKEN ****%s *NOT* valid" % _auth_token[-4:])
1638 1637
1639 1638 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
1640 1639 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1641 1640 else 'AUTH_TOKEN_AUTH'
1642 1641
1643 1642 if ip_access_valid and (
1644 1643 user.is_authenticated or auth_token_access_valid):
1645 1644 log.info(
1646 1645 'user %s authenticating with:%s IS authenticated on func %s'
1647 1646 % (user, reason, loc))
1648 1647
1649 1648 return func(*fargs, **fkwargs)
1650 1649 else:
1651 1650 log.warning(
1652 1651 'user %s authenticating with:%s NOT authenticated on '
1653 1652 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s'
1654 1653 % (user, reason, loc, ip_access_valid,
1655 1654 auth_token_access_valid))
1656 1655 # we preserve the get PARAM
1657 1656 came_from = get_came_from(request)
1658 1657
1659 1658 log.debug('redirecting to login page with %s' % (came_from,))
1660 1659 raise HTTPFound(
1661 1660 h.route_path('login', _query={'came_from': came_from}))
1662 1661
1663 1662
1664 1663 class NotAnonymous(object):
1665 1664 """
1666 1665 Must be logged in to execute this function else
1667 1666 redirect to login page
1668 1667 """
1669 1668
1670 1669 def __call__(self, func):
1671 1670 return get_cython_compat_decorator(self.__wrapper, func)
1672 1671
1673 1672 def _get_request(self):
1674 1673 return get_request(self)
1675 1674
1676 1675 def __wrapper(self, func, *fargs, **fkwargs):
1677 1676 import rhodecode.lib.helpers as h
1678 1677 cls = fargs[0]
1679 1678 self.user = cls._rhodecode_user
1680 1679 request = self._get_request()
1681 1680 _ = request.translate
1682 1681 log.debug('Checking if user is not anonymous @%s' % cls)
1683 1682
1684 1683 anonymous = self.user.username == User.DEFAULT_USER
1685 1684
1686 1685 if anonymous:
1687 1686 came_from = get_came_from(request)
1688 1687 h.flash(_('You need to be a registered user to '
1689 1688 'perform this action'),
1690 1689 category='warning')
1691 1690 raise HTTPFound(
1692 1691 h.route_path('login', _query={'came_from': came_from}))
1693 1692 else:
1694 1693 return func(*fargs, **fkwargs)
1695 1694
1696 1695
1697 1696 class PermsDecorator(object):
1698 1697 """
1699 1698 Base class for controller decorators, we extract the current user from
1700 1699 the class itself, which has it stored in base controllers
1701 1700 """
1702 1701
1703 1702 def __init__(self, *required_perms):
1704 1703 self.required_perms = set(required_perms)
1705 1704
1706 1705 def __call__(self, func):
1707 1706 return get_cython_compat_decorator(self.__wrapper, func)
1708 1707
1709 1708 def _get_request(self):
1710 1709 return get_request(self)
1711 1710
1712 1711 def __wrapper(self, func, *fargs, **fkwargs):
1713 1712 import rhodecode.lib.helpers as h
1714 1713 cls = fargs[0]
1715 1714 _user = cls._rhodecode_user
1716 1715 request = self._get_request()
1717 1716 _ = request.translate
1718 1717
1719 1718 log.debug('checking %s permissions %s for %s %s',
1720 1719 self.__class__.__name__, self.required_perms, cls, _user)
1721 1720
1722 1721 if self.check_permissions(_user):
1723 1722 log.debug('Permission granted for %s %s', cls, _user)
1724 1723 return func(*fargs, **fkwargs)
1725 1724
1726 1725 else:
1727 1726 log.debug('Permission denied for %s %s', cls, _user)
1728 1727 anonymous = _user.username == User.DEFAULT_USER
1729 1728
1730 1729 if anonymous:
1731 1730 came_from = get_came_from(self._get_request())
1732 1731 h.flash(_('You need to be signed in to view this page'),
1733 1732 category='warning')
1734 1733 raise HTTPFound(
1735 1734 h.route_path('login', _query={'came_from': came_from}))
1736 1735
1737 1736 else:
1738 1737 # redirect with 404 to prevent resource discovery
1739 1738 raise HTTPNotFound()
1740 1739
1741 1740 def check_permissions(self, user):
1742 1741 """Dummy function for overriding"""
1743 1742 raise NotImplementedError(
1744 1743 'You have to write this function in child class')
1745 1744
1746 1745
1747 1746 class HasPermissionAllDecorator(PermsDecorator):
1748 1747 """
1749 1748 Checks for access permission for all given predicates. All of them
1750 1749 have to be meet in order to fulfill the request
1751 1750 """
1752 1751
1753 1752 def check_permissions(self, user):
1754 1753 perms = user.permissions_with_scope({})
1755 1754 if self.required_perms.issubset(perms['global']):
1756 1755 return True
1757 1756 return False
1758 1757
1759 1758
1760 1759 class HasPermissionAnyDecorator(PermsDecorator):
1761 1760 """
1762 1761 Checks for access permission for any of given predicates. In order to
1763 1762 fulfill the request any of predicates must be meet
1764 1763 """
1765 1764
1766 1765 def check_permissions(self, user):
1767 1766 perms = user.permissions_with_scope({})
1768 1767 if self.required_perms.intersection(perms['global']):
1769 1768 return True
1770 1769 return False
1771 1770
1772 1771
1773 1772 class HasRepoPermissionAllDecorator(PermsDecorator):
1774 1773 """
1775 1774 Checks for access permission for all given predicates for specific
1776 1775 repository. All of them have to be meet in order to fulfill the request
1777 1776 """
1778 1777 def _get_repo_name(self):
1779 1778 _request = self._get_request()
1780 1779 return get_repo_slug(_request)
1781 1780
1782 1781 def check_permissions(self, user):
1783 1782 perms = user.permissions
1784 1783 repo_name = self._get_repo_name()
1785 1784
1786 1785 try:
1787 1786 user_perms = {perms['repositories'][repo_name]}
1788 1787 except KeyError:
1789 1788 log.debug('cannot locate repo with name: `%s` in permissions defs',
1790 1789 repo_name)
1791 1790 return False
1792 1791
1793 1792 log.debug('checking `%s` permissions for repo `%s`',
1794 1793 user_perms, repo_name)
1795 1794 if self.required_perms.issubset(user_perms):
1796 1795 return True
1797 1796 return False
1798 1797
1799 1798
1800 1799 class HasRepoPermissionAnyDecorator(PermsDecorator):
1801 1800 """
1802 1801 Checks for access permission for any of given predicates for specific
1803 1802 repository. In order to fulfill the request any of predicates must be meet
1804 1803 """
1805 1804 def _get_repo_name(self):
1806 1805 _request = self._get_request()
1807 1806 return get_repo_slug(_request)
1808 1807
1809 1808 def check_permissions(self, user):
1810 1809 perms = user.permissions
1811 1810 repo_name = self._get_repo_name()
1812 1811
1813 1812 try:
1814 1813 user_perms = {perms['repositories'][repo_name]}
1815 1814 except KeyError:
1816 1815 log.debug(
1817 1816 'cannot locate repo with name: `%s` in permissions defs',
1818 1817 repo_name)
1819 1818 return False
1820 1819
1821 1820 log.debug('checking `%s` permissions for repo `%s`',
1822 1821 user_perms, repo_name)
1823 1822 if self.required_perms.intersection(user_perms):
1824 1823 return True
1825 1824 return False
1826 1825
1827 1826
1828 1827 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1829 1828 """
1830 1829 Checks for access permission for all given predicates for specific
1831 1830 repository group. All of them have to be meet in order to
1832 1831 fulfill the request
1833 1832 """
1834 1833 def _get_repo_group_name(self):
1835 1834 _request = self._get_request()
1836 1835 return get_repo_group_slug(_request)
1837 1836
1838 1837 def check_permissions(self, user):
1839 1838 perms = user.permissions
1840 1839 group_name = self._get_repo_group_name()
1841 1840 try:
1842 1841 user_perms = {perms['repositories_groups'][group_name]}
1843 1842 except KeyError:
1844 1843 log.debug(
1845 1844 'cannot locate repo group with name: `%s` in permissions defs',
1846 1845 group_name)
1847 1846 return False
1848 1847
1849 1848 log.debug('checking `%s` permissions for repo group `%s`',
1850 1849 user_perms, group_name)
1851 1850 if self.required_perms.issubset(user_perms):
1852 1851 return True
1853 1852 return False
1854 1853
1855 1854
1856 1855 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1857 1856 """
1858 1857 Checks for access permission for any of given predicates for specific
1859 1858 repository group. In order to fulfill the request any
1860 1859 of predicates must be met
1861 1860 """
1862 1861 def _get_repo_group_name(self):
1863 1862 _request = self._get_request()
1864 1863 return get_repo_group_slug(_request)
1865 1864
1866 1865 def check_permissions(self, user):
1867 1866 perms = user.permissions
1868 1867 group_name = self._get_repo_group_name()
1869 1868
1870 1869 try:
1871 1870 user_perms = {perms['repositories_groups'][group_name]}
1872 1871 except KeyError:
1873 1872 log.debug(
1874 1873 'cannot locate repo group with name: `%s` in permissions defs',
1875 1874 group_name)
1876 1875 return False
1877 1876
1878 1877 log.debug('checking `%s` permissions for repo group `%s`',
1879 1878 user_perms, group_name)
1880 1879 if self.required_perms.intersection(user_perms):
1881 1880 return True
1882 1881 return False
1883 1882
1884 1883
1885 1884 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1886 1885 """
1887 1886 Checks for access permission for all given predicates for specific
1888 1887 user group. All of them have to be meet in order to fulfill the request
1889 1888 """
1890 1889 def _get_user_group_name(self):
1891 1890 _request = self._get_request()
1892 1891 return get_user_group_slug(_request)
1893 1892
1894 1893 def check_permissions(self, user):
1895 1894 perms = user.permissions
1896 1895 group_name = self._get_user_group_name()
1897 1896 try:
1898 1897 user_perms = {perms['user_groups'][group_name]}
1899 1898 except KeyError:
1900 1899 return False
1901 1900
1902 1901 if self.required_perms.issubset(user_perms):
1903 1902 return True
1904 1903 return False
1905 1904
1906 1905
1907 1906 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1908 1907 """
1909 1908 Checks for access permission for any of given predicates for specific
1910 1909 user group. In order to fulfill the request any of predicates must be meet
1911 1910 """
1912 1911 def _get_user_group_name(self):
1913 1912 _request = self._get_request()
1914 1913 return get_user_group_slug(_request)
1915 1914
1916 1915 def check_permissions(self, user):
1917 1916 perms = user.permissions
1918 1917 group_name = self._get_user_group_name()
1919 1918 try:
1920 1919 user_perms = {perms['user_groups'][group_name]}
1921 1920 except KeyError:
1922 1921 return False
1923 1922
1924 1923 if self.required_perms.intersection(user_perms):
1925 1924 return True
1926 1925 return False
1927 1926
1928 1927
1929 1928 # CHECK FUNCTIONS
1930 1929 class PermsFunction(object):
1931 1930 """Base function for other check functions"""
1932 1931
1933 1932 def __init__(self, *perms):
1934 1933 self.required_perms = set(perms)
1935 1934 self.repo_name = None
1936 1935 self.repo_group_name = None
1937 1936 self.user_group_name = None
1938 1937
1939 1938 def __bool__(self):
1940 1939 frame = inspect.currentframe()
1941 1940 stack_trace = traceback.format_stack(frame)
1942 1941 log.error('Checking bool value on a class instance of perm '
1943 1942 'function is not allowed: %s' % ''.join(stack_trace))
1944 1943 # rather than throwing errors, here we always return False so if by
1945 1944 # accident someone checks truth for just an instance it will always end
1946 1945 # up in returning False
1947 1946 return False
1948 1947 __nonzero__ = __bool__
1949 1948
1950 1949 def __call__(self, check_location='', user=None):
1951 1950 if not user:
1952 1951 log.debug('Using user attribute from global request')
1953 1952 request = self._get_request()
1954 1953 user = request.user
1955 1954
1956 1955 # init auth user if not already given
1957 1956 if not isinstance(user, AuthUser):
1958 1957 log.debug('Wrapping user %s into AuthUser', user)
1959 1958 user = AuthUser(user.user_id)
1960 1959
1961 1960 cls_name = self.__class__.__name__
1962 1961 check_scope = self._get_check_scope(cls_name)
1963 1962 check_location = check_location or 'unspecified location'
1964 1963
1965 1964 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
1966 1965 self.required_perms, user, check_scope, check_location)
1967 1966 if not user:
1968 1967 log.warning('Empty user given for permission check')
1969 1968 return False
1970 1969
1971 1970 if self.check_permissions(user):
1972 1971 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
1973 1972 check_scope, user, check_location)
1974 1973 return True
1975 1974
1976 1975 else:
1977 1976 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
1978 1977 check_scope, user, check_location)
1979 1978 return False
1980 1979
1981 1980 def _get_request(self):
1982 1981 return get_request(self)
1983 1982
1984 1983 def _get_check_scope(self, cls_name):
1985 1984 return {
1986 1985 'HasPermissionAll': 'GLOBAL',
1987 1986 'HasPermissionAny': 'GLOBAL',
1988 1987 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
1989 1988 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
1990 1989 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
1991 1990 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
1992 1991 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
1993 1992 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
1994 1993 }.get(cls_name, '?:%s' % cls_name)
1995 1994
1996 1995 def check_permissions(self, user):
1997 1996 """Dummy function for overriding"""
1998 1997 raise Exception('You have to write this function in child class')
1999 1998
2000 1999
2001 2000 class HasPermissionAll(PermsFunction):
2002 2001 def check_permissions(self, user):
2003 2002 perms = user.permissions_with_scope({})
2004 2003 if self.required_perms.issubset(perms.get('global')):
2005 2004 return True
2006 2005 return False
2007 2006
2008 2007
2009 2008 class HasPermissionAny(PermsFunction):
2010 2009 def check_permissions(self, user):
2011 2010 perms = user.permissions_with_scope({})
2012 2011 if self.required_perms.intersection(perms.get('global')):
2013 2012 return True
2014 2013 return False
2015 2014
2016 2015
2017 2016 class HasRepoPermissionAll(PermsFunction):
2018 2017 def __call__(self, repo_name=None, check_location='', user=None):
2019 2018 self.repo_name = repo_name
2020 2019 return super(HasRepoPermissionAll, self).__call__(check_location, user)
2021 2020
2022 2021 def _get_repo_name(self):
2023 2022 if not self.repo_name:
2024 2023 _request = self._get_request()
2025 2024 self.repo_name = get_repo_slug(_request)
2026 2025 return self.repo_name
2027 2026
2028 2027 def check_permissions(self, user):
2029 2028 self.repo_name = self._get_repo_name()
2030 2029 perms = user.permissions
2031 2030 try:
2032 2031 user_perms = {perms['repositories'][self.repo_name]}
2033 2032 except KeyError:
2034 2033 return False
2035 2034 if self.required_perms.issubset(user_perms):
2036 2035 return True
2037 2036 return False
2038 2037
2039 2038
2040 2039 class HasRepoPermissionAny(PermsFunction):
2041 2040 def __call__(self, repo_name=None, check_location='', user=None):
2042 2041 self.repo_name = repo_name
2043 2042 return super(HasRepoPermissionAny, self).__call__(check_location, user)
2044 2043
2045 2044 def _get_repo_name(self):
2046 2045 if not self.repo_name:
2047 2046 _request = self._get_request()
2048 2047 self.repo_name = get_repo_slug(_request)
2049 2048 return self.repo_name
2050 2049
2051 2050 def check_permissions(self, user):
2052 2051 self.repo_name = self._get_repo_name()
2053 2052 perms = user.permissions
2054 2053 try:
2055 2054 user_perms = {perms['repositories'][self.repo_name]}
2056 2055 except KeyError:
2057 2056 return False
2058 2057 if self.required_perms.intersection(user_perms):
2059 2058 return True
2060 2059 return False
2061 2060
2062 2061
2063 2062 class HasRepoGroupPermissionAny(PermsFunction):
2064 2063 def __call__(self, group_name=None, check_location='', user=None):
2065 2064 self.repo_group_name = group_name
2066 2065 return super(HasRepoGroupPermissionAny, self).__call__(
2067 2066 check_location, user)
2068 2067
2069 2068 def check_permissions(self, user):
2070 2069 perms = user.permissions
2071 2070 try:
2072 2071 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2073 2072 except KeyError:
2074 2073 return False
2075 2074 if self.required_perms.intersection(user_perms):
2076 2075 return True
2077 2076 return False
2078 2077
2079 2078
2080 2079 class HasRepoGroupPermissionAll(PermsFunction):
2081 2080 def __call__(self, group_name=None, check_location='', user=None):
2082 2081 self.repo_group_name = group_name
2083 2082 return super(HasRepoGroupPermissionAll, self).__call__(
2084 2083 check_location, user)
2085 2084
2086 2085 def check_permissions(self, user):
2087 2086 perms = user.permissions
2088 2087 try:
2089 2088 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2090 2089 except KeyError:
2091 2090 return False
2092 2091 if self.required_perms.issubset(user_perms):
2093 2092 return True
2094 2093 return False
2095 2094
2096 2095
2097 2096 class HasUserGroupPermissionAny(PermsFunction):
2098 2097 def __call__(self, user_group_name=None, check_location='', user=None):
2099 2098 self.user_group_name = user_group_name
2100 2099 return super(HasUserGroupPermissionAny, self).__call__(
2101 2100 check_location, user)
2102 2101
2103 2102 def check_permissions(self, user):
2104 2103 perms = user.permissions
2105 2104 try:
2106 2105 user_perms = {perms['user_groups'][self.user_group_name]}
2107 2106 except KeyError:
2108 2107 return False
2109 2108 if self.required_perms.intersection(user_perms):
2110 2109 return True
2111 2110 return False
2112 2111
2113 2112
2114 2113 class HasUserGroupPermissionAll(PermsFunction):
2115 2114 def __call__(self, user_group_name=None, check_location='', user=None):
2116 2115 self.user_group_name = user_group_name
2117 2116 return super(HasUserGroupPermissionAll, self).__call__(
2118 2117 check_location, user)
2119 2118
2120 2119 def check_permissions(self, user):
2121 2120 perms = user.permissions
2122 2121 try:
2123 2122 user_perms = {perms['user_groups'][self.user_group_name]}
2124 2123 except KeyError:
2125 2124 return False
2126 2125 if self.required_perms.issubset(user_perms):
2127 2126 return True
2128 2127 return False
2129 2128
2130 2129
2131 2130 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
2132 2131 class HasPermissionAnyMiddleware(object):
2133 2132 def __init__(self, *perms):
2134 2133 self.required_perms = set(perms)
2135 2134
2136 2135 def __call__(self, auth_user, repo_name):
2137 2136 # repo_name MUST be unicode, since we handle keys in permission
2138 2137 # dict by unicode
2139 2138 repo_name = safe_unicode(repo_name)
2140 2139 log.debug(
2141 2140 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2142 2141 self.required_perms, auth_user, repo_name)
2143 2142
2144 2143 if self.check_permissions(auth_user, repo_name):
2145 2144 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2146 2145 repo_name, auth_user, 'PermissionMiddleware')
2147 2146 return True
2148 2147
2149 2148 else:
2150 2149 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2151 2150 repo_name, auth_user, 'PermissionMiddleware')
2152 2151 return False
2153 2152
2154 2153 def check_permissions(self, user, repo_name):
2155 2154 perms = user.permissions_with_scope({'repo_name': repo_name})
2156 2155
2157 2156 try:
2158 2157 user_perms = {perms['repositories'][repo_name]}
2159 2158 except Exception:
2160 2159 log.exception('Error while accessing user permissions')
2161 2160 return False
2162 2161
2163 2162 if self.required_perms.intersection(user_perms):
2164 2163 return True
2165 2164 return False
2166 2165
2167 2166
2168 2167 # SPECIAL VERSION TO HANDLE API AUTH
2169 2168 class _BaseApiPerm(object):
2170 2169 def __init__(self, *perms):
2171 2170 self.required_perms = set(perms)
2172 2171
2173 2172 def __call__(self, check_location=None, user=None, repo_name=None,
2174 2173 group_name=None, user_group_name=None):
2175 2174 cls_name = self.__class__.__name__
2176 2175 check_scope = 'global:%s' % (self.required_perms,)
2177 2176 if repo_name:
2178 2177 check_scope += ', repo_name:%s' % (repo_name,)
2179 2178
2180 2179 if group_name:
2181 2180 check_scope += ', repo_group_name:%s' % (group_name,)
2182 2181
2183 2182 if user_group_name:
2184 2183 check_scope += ', user_group_name:%s' % (user_group_name,)
2185 2184
2186 2185 log.debug(
2187 2186 'checking cls:%s %s %s @ %s'
2188 2187 % (cls_name, self.required_perms, check_scope, check_location))
2189 2188 if not user:
2190 2189 log.debug('Empty User passed into arguments')
2191 2190 return False
2192 2191
2193 2192 # process user
2194 2193 if not isinstance(user, AuthUser):
2195 2194 user = AuthUser(user.user_id)
2196 2195 if not check_location:
2197 2196 check_location = 'unspecified'
2198 2197 if self.check_permissions(user.permissions, repo_name, group_name,
2199 2198 user_group_name):
2200 2199 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2201 2200 check_scope, user, check_location)
2202 2201 return True
2203 2202
2204 2203 else:
2205 2204 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2206 2205 check_scope, user, check_location)
2207 2206 return False
2208 2207
2209 2208 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2210 2209 user_group_name=None):
2211 2210 """
2212 2211 implement in child class should return True if permissions are ok,
2213 2212 False otherwise
2214 2213
2215 2214 :param perm_defs: dict with permission definitions
2216 2215 :param repo_name: repo name
2217 2216 """
2218 2217 raise NotImplementedError()
2219 2218
2220 2219
2221 2220 class HasPermissionAllApi(_BaseApiPerm):
2222 2221 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2223 2222 user_group_name=None):
2224 2223 if self.required_perms.issubset(perm_defs.get('global')):
2225 2224 return True
2226 2225 return False
2227 2226
2228 2227
2229 2228 class HasPermissionAnyApi(_BaseApiPerm):
2230 2229 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2231 2230 user_group_name=None):
2232 2231 if self.required_perms.intersection(perm_defs.get('global')):
2233 2232 return True
2234 2233 return False
2235 2234
2236 2235
2237 2236 class HasRepoPermissionAllApi(_BaseApiPerm):
2238 2237 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2239 2238 user_group_name=None):
2240 2239 try:
2241 2240 _user_perms = {perm_defs['repositories'][repo_name]}
2242 2241 except KeyError:
2243 2242 log.warning(traceback.format_exc())
2244 2243 return False
2245 2244 if self.required_perms.issubset(_user_perms):
2246 2245 return True
2247 2246 return False
2248 2247
2249 2248
2250 2249 class HasRepoPermissionAnyApi(_BaseApiPerm):
2251 2250 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2252 2251 user_group_name=None):
2253 2252 try:
2254 2253 _user_perms = {perm_defs['repositories'][repo_name]}
2255 2254 except KeyError:
2256 2255 log.warning(traceback.format_exc())
2257 2256 return False
2258 2257 if self.required_perms.intersection(_user_perms):
2259 2258 return True
2260 2259 return False
2261 2260
2262 2261
2263 2262 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2264 2263 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2265 2264 user_group_name=None):
2266 2265 try:
2267 2266 _user_perms = {perm_defs['repositories_groups'][group_name]}
2268 2267 except KeyError:
2269 2268 log.warning(traceback.format_exc())
2270 2269 return False
2271 2270 if self.required_perms.intersection(_user_perms):
2272 2271 return True
2273 2272 return False
2274 2273
2275 2274
2276 2275 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2277 2276 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2278 2277 user_group_name=None):
2279 2278 try:
2280 2279 _user_perms = {perm_defs['repositories_groups'][group_name]}
2281 2280 except KeyError:
2282 2281 log.warning(traceback.format_exc())
2283 2282 return False
2284 2283 if self.required_perms.issubset(_user_perms):
2285 2284 return True
2286 2285 return False
2287 2286
2288 2287
2289 2288 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2290 2289 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2291 2290 user_group_name=None):
2292 2291 try:
2293 2292 _user_perms = {perm_defs['user_groups'][user_group_name]}
2294 2293 except KeyError:
2295 2294 log.warning(traceback.format_exc())
2296 2295 return False
2297 2296 if self.required_perms.intersection(_user_perms):
2298 2297 return True
2299 2298 return False
2300 2299
2301 2300
2302 2301 def check_ip_access(source_ip, allowed_ips=None):
2303 2302 """
2304 2303 Checks if source_ip is a subnet of any of allowed_ips.
2305 2304
2306 2305 :param source_ip:
2307 2306 :param allowed_ips: list of allowed ips together with mask
2308 2307 """
2309 2308 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
2310 2309 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2311 2310 if isinstance(allowed_ips, (tuple, list, set)):
2312 2311 for ip in allowed_ips:
2313 2312 ip = safe_unicode(ip)
2314 2313 try:
2315 2314 network_address = ipaddress.ip_network(ip, strict=False)
2316 2315 if source_ip_address in network_address:
2317 2316 log.debug('IP %s is network %s' %
2318 2317 (source_ip_address, network_address))
2319 2318 return True
2320 2319 # for any case we cannot determine the IP, don't crash just
2321 2320 # skip it and log as error, we want to say forbidden still when
2322 2321 # sending bad IP
2323 2322 except Exception:
2324 2323 log.error(traceback.format_exc())
2325 2324 continue
2326 2325 return False
2327 2326
2328 2327
2329 2328 def get_cython_compat_decorator(wrapper, func):
2330 2329 """
2331 2330 Creates a cython compatible decorator. The previously used
2332 2331 decorator.decorator() function seems to be incompatible with cython.
2333 2332
2334 2333 :param wrapper: __wrapper method of the decorator class
2335 2334 :param func: decorated function
2336 2335 """
2337 2336 @wraps(func)
2338 2337 def local_wrapper(*args, **kwds):
2339 2338 return wrapper(func, *args, **kwds)
2340 2339 local_wrapper.__wrapped__ = func
2341 2340 return local_wrapper
2342 2341
2343 2342
@@ -1,548 +1,550 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 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 extras = {
182 # NOTE(marcink): This should be also in sync with
183 # rhodecode/apps/ssh_support/lib/backends/base.py:update_enviroment scm_data
184 scm_data = {
183 185 'ip': get_ip_addr(environ),
184 186 'username': username,
185 187 'user_id': user_id,
186 188 'action': action,
187 189 'repository': repo_name,
188 190 'scm': scm,
189 191 'config': rhodecode.CONFIG['__file__'],
190 192 'make_lock': make_lock,
191 193 'locked_by': locked_by,
192 194 'server_url': utils2.get_server_url(environ),
193 195 'user_agent': get_user_agent(environ),
194 196 'hooks': get_enabled_hook_classes(ui_settings),
195 197 'is_shadow_repo': is_shadow_repo,
196 198 'detect_force_push': detect_force_push,
197 199 'check_branch_perms': check_branch_perms,
198 200 }
199 return extras
201 return scm_data
200 202
201 203
202 204 class BasicAuth(AuthBasicAuthenticator):
203 205
204 206 def __init__(self, realm, authfunc, registry, auth_http_code=None,
205 207 initial_call_detection=False, acl_repo_name=None):
206 208 self.realm = realm
207 209 self.initial_call = initial_call_detection
208 210 self.authfunc = authfunc
209 211 self.registry = registry
210 212 self.acl_repo_name = acl_repo_name
211 213 self._rc_auth_http_code = auth_http_code
212 214
213 215 def _get_response_from_code(self, http_code):
214 216 try:
215 217 return get_exception(safe_int(http_code))
216 218 except Exception:
217 219 log.exception('Failed to fetch response for code %s' % http_code)
218 220 return HTTPForbidden
219 221
220 222 def get_rc_realm(self):
221 223 return safe_str(self.registry.rhodecode_settings.get('rhodecode_realm'))
222 224
223 225 def build_authentication(self):
224 226 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
225 227 if self._rc_auth_http_code and not self.initial_call:
226 228 # return alternative HTTP code if alternative http return code
227 229 # is specified in RhodeCode config, but ONLY if it's not the
228 230 # FIRST call
229 231 custom_response_klass = self._get_response_from_code(
230 232 self._rc_auth_http_code)
231 233 return custom_response_klass(headers=head)
232 234 return HTTPUnauthorized(headers=head)
233 235
234 236 def authenticate(self, environ):
235 237 authorization = AUTHORIZATION(environ)
236 238 if not authorization:
237 239 return self.build_authentication()
238 240 (authmeth, auth) = authorization.split(' ', 1)
239 241 if 'basic' != authmeth.lower():
240 242 return self.build_authentication()
241 243 auth = auth.strip().decode('base64')
242 244 _parts = auth.split(':', 1)
243 245 if len(_parts) == 2:
244 246 username, password = _parts
245 247 auth_data = self.authfunc(
246 248 username, password, environ, VCS_TYPE,
247 249 registry=self.registry, acl_repo_name=self.acl_repo_name)
248 250 if auth_data:
249 251 return {'username': username, 'auth_data': auth_data}
250 252 if username and password:
251 253 # we mark that we actually executed authentication once, at
252 254 # that point we can use the alternative auth code
253 255 self.initial_call = False
254 256
255 257 return self.build_authentication()
256 258
257 259 __call__ = authenticate
258 260
259 261
260 262 def calculate_version_hash(config):
261 263 return sha1(
262 264 config.get('beaker.session.secret', '') +
263 265 rhodecode.__version__)[:8]
264 266
265 267
266 268 def get_current_lang(request):
267 269 # NOTE(marcink): remove after pyramid move
268 270 try:
269 271 return translation.get_lang()[0]
270 272 except:
271 273 pass
272 274
273 275 return getattr(request, '_LOCALE_', request.locale_name)
274 276
275 277
276 278 def attach_context_attributes(context, request, user_id):
277 279 """
278 280 Attach variables into template context called `c`.
279 281 """
280 282 config = request.registry.settings
281 283
282 284
283 285 rc_config = SettingsModel().get_all_settings(cache=True)
284 286
285 287 context.rhodecode_version = rhodecode.__version__
286 288 context.rhodecode_edition = config.get('rhodecode.edition')
287 289 # unique secret + version does not leak the version but keep consistency
288 290 context.rhodecode_version_hash = calculate_version_hash(config)
289 291
290 292 # Default language set for the incoming request
291 293 context.language = get_current_lang(request)
292 294
293 295 # Visual options
294 296 context.visual = AttributeDict({})
295 297
296 298 # DB stored Visual Items
297 299 context.visual.show_public_icon = str2bool(
298 300 rc_config.get('rhodecode_show_public_icon'))
299 301 context.visual.show_private_icon = str2bool(
300 302 rc_config.get('rhodecode_show_private_icon'))
301 303 context.visual.stylify_metatags = str2bool(
302 304 rc_config.get('rhodecode_stylify_metatags'))
303 305 context.visual.dashboard_items = safe_int(
304 306 rc_config.get('rhodecode_dashboard_items', 100))
305 307 context.visual.admin_grid_items = safe_int(
306 308 rc_config.get('rhodecode_admin_grid_items', 100))
307 309 context.visual.repository_fields = str2bool(
308 310 rc_config.get('rhodecode_repository_fields'))
309 311 context.visual.show_version = str2bool(
310 312 rc_config.get('rhodecode_show_version'))
311 313 context.visual.use_gravatar = str2bool(
312 314 rc_config.get('rhodecode_use_gravatar'))
313 315 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
314 316 context.visual.default_renderer = rc_config.get(
315 317 'rhodecode_markup_renderer', 'rst')
316 318 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
317 319 context.visual.rhodecode_support_url = \
318 320 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
319 321
320 322 context.visual.affected_files_cut_off = 60
321 323
322 324 context.pre_code = rc_config.get('rhodecode_pre_code')
323 325 context.post_code = rc_config.get('rhodecode_post_code')
324 326 context.rhodecode_name = rc_config.get('rhodecode_title')
325 327 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
326 328 # if we have specified default_encoding in the request, it has more
327 329 # priority
328 330 if request.GET.get('default_encoding'):
329 331 context.default_encodings.insert(0, request.GET.get('default_encoding'))
330 332 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
331 333 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
332 334
333 335 # INI stored
334 336 context.labs_active = str2bool(
335 337 config.get('labs_settings_active', 'false'))
336 338 context.ssh_enabled = str2bool(
337 339 config.get('ssh.generate_authorized_keyfile', 'false'))
338 340
339 341 context.visual.allow_repo_location_change = str2bool(
340 342 config.get('allow_repo_location_change', True))
341 343 context.visual.allow_custom_hooks_settings = str2bool(
342 344 config.get('allow_custom_hooks_settings', True))
343 345 context.debug_style = str2bool(config.get('debug_style', False))
344 346
345 347 context.rhodecode_instanceid = config.get('instance_id')
346 348
347 349 context.visual.cut_off_limit_diff = safe_int(
348 350 config.get('cut_off_limit_diff'))
349 351 context.visual.cut_off_limit_file = safe_int(
350 352 config.get('cut_off_limit_file'))
351 353
352 354 # AppEnlight
353 355 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
354 356 context.appenlight_api_public_key = config.get(
355 357 'appenlight.api_public_key', '')
356 358 context.appenlight_server_url = config.get('appenlight.server_url', '')
357 359
358 360 # JS template context
359 361 context.template_context = {
360 362 'repo_name': None,
361 363 'repo_type': None,
362 364 'repo_landing_commit': None,
363 365 'rhodecode_user': {
364 366 'username': None,
365 367 'email': None,
366 368 'notification_status': False
367 369 },
368 370 'visual': {
369 371 'default_renderer': None
370 372 },
371 373 'commit_data': {
372 374 'commit_id': None
373 375 },
374 376 'pull_request_data': {'pull_request_id': None},
375 377 'timeago': {
376 378 'refresh_time': 120 * 1000,
377 379 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
378 380 },
379 381 'pyramid_dispatch': {
380 382
381 383 },
382 384 'extra': {'plugins': {}}
383 385 }
384 386 # END CONFIG VARS
385 387
386 388 diffmode = 'sideside'
387 389 if request.GET.get('diffmode'):
388 390 if request.GET['diffmode'] == 'unified':
389 391 diffmode = 'unified'
390 392 elif request.session.get('diffmode'):
391 393 diffmode = request.session['diffmode']
392 394
393 395 context.diffmode = diffmode
394 396
395 397 if request.session.get('diffmode') != diffmode:
396 398 request.session['diffmode'] = diffmode
397 399
398 400 context.csrf_token = auth.get_csrf_token(session=request.session)
399 401 context.backends = rhodecode.BACKENDS.keys()
400 402 context.backends.sort()
401 403 context.unread_notifications = NotificationModel().get_unread_cnt_for_user(user_id)
402 404
403 405 # web case
404 406 if hasattr(request, 'user'):
405 407 context.auth_user = request.user
406 408 context.rhodecode_user = request.user
407 409
408 410 # api case
409 411 if hasattr(request, 'rpc_user'):
410 412 context.auth_user = request.rpc_user
411 413 context.rhodecode_user = request.rpc_user
412 414
413 415 # attach the whole call context to the request
414 416 request.call_context = context
415 417
416 418
417 419 def get_auth_user(request):
418 420 environ = request.environ
419 421 session = request.session
420 422
421 423 ip_addr = get_ip_addr(environ)
422 424 # make sure that we update permissions each time we call controller
423 425 _auth_token = (request.GET.get('auth_token', '') or
424 426 request.GET.get('api_key', ''))
425 427
426 428 if _auth_token:
427 429 # when using API_KEY we assume user exists, and
428 430 # doesn't need auth based on cookies.
429 431 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
430 432 authenticated = False
431 433 else:
432 434 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
433 435 try:
434 436 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
435 437 ip_addr=ip_addr)
436 438 except UserCreationError as e:
437 439 h.flash(e, 'error')
438 440 # container auth or other auth functions that create users
439 441 # on the fly can throw this exception signaling that there's
440 442 # issue with user creation, explanation should be provided
441 443 # in Exception itself. We then create a simple blank
442 444 # AuthUser
443 445 auth_user = AuthUser(ip_addr=ip_addr)
444 446
445 447 # in case someone changes a password for user it triggers session
446 448 # flush and forces a re-login
447 449 if password_changed(auth_user, session):
448 450 session.invalidate()
449 451 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
450 452 auth_user = AuthUser(ip_addr=ip_addr)
451 453
452 454 authenticated = cookie_store.get('is_authenticated')
453 455
454 456 if not auth_user.is_authenticated and auth_user.is_user_object:
455 457 # user is not authenticated and not empty
456 458 auth_user.set_authenticated(authenticated)
457 459
458 460 return auth_user
459 461
460 462
461 463 def h_filter(s):
462 464 """
463 465 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
464 466 we wrap this with additional functionality that converts None to empty
465 467 strings
466 468 """
467 469 if s is None:
468 470 return markupsafe.Markup()
469 471 return markupsafe.escape(s)
470 472
471 473
472 474 def add_events_routes(config):
473 475 """
474 476 Adds routing that can be used in events. Because some events are triggered
475 477 outside of pyramid context, we need to bootstrap request with some
476 478 routing registered
477 479 """
478 480
479 481 from rhodecode.apps._base import ADMIN_PREFIX
480 482
481 483 config.add_route(name='home', pattern='/')
482 484
483 485 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
484 486 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
485 487 config.add_route(name='repo_summary', pattern='/{repo_name}')
486 488 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
487 489 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
488 490
489 491 config.add_route(name='pullrequest_show',
490 492 pattern='/{repo_name}/pull-request/{pull_request_id}')
491 493 config.add_route(name='pull_requests_global',
492 494 pattern='/pull-request/{pull_request_id}')
493 495 config.add_route(name='repo_commit',
494 496 pattern='/{repo_name}/changeset/{commit_id}')
495 497
496 498 config.add_route(name='repo_files',
497 499 pattern='/{repo_name}/files/{commit_id}/{f_path}')
498 500
499 501
500 502 def bootstrap_config(request):
501 503 import pyramid.testing
502 504 registry = pyramid.testing.Registry('RcTestRegistry')
503 505
504 506 config = pyramid.testing.setUp(registry=registry, request=request)
505 507
506 508 # allow pyramid lookup in testing
507 509 config.include('pyramid_mako')
508 510 config.include('pyramid_beaker')
509 511 config.include('rhodecode.lib.rc_cache')
510 512
511 513 add_events_routes(config)
512 514
513 515 return config
514 516
515 517
516 518 def bootstrap_request(**kwargs):
517 519 import pyramid.testing
518 520
519 521 class TestRequest(pyramid.testing.DummyRequest):
520 522 application_url = kwargs.pop('application_url', 'http://example.com')
521 523 host = kwargs.pop('host', 'example.com:80')
522 524 domain = kwargs.pop('domain', 'example.com')
523 525
524 526 def translate(self, msg):
525 527 return msg
526 528
527 529 def plularize(self, singular, plural, n):
528 530 return singular
529 531
530 532 def get_partial_renderer(self, tmpl_name):
531 533
532 534 from rhodecode.lib.partial_renderer import get_partial_renderer
533 535 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
534 536
535 537 _call_context = {}
536 538 @property
537 539 def call_context(self):
538 540 return self._call_context
539 541
540 542 class TestDummySession(pyramid.testing.DummySession):
541 543 def save(*arg, **kw):
542 544 pass
543 545
544 546 request = TestRequest(**kwargs)
545 547 request.session = TestDummySession()
546 548
547 549 return request
548 550
General Comments 0
You need to be logged in to leave comments. Login now