##// END OF EJS Templates
simplevcs: store rc_extras reference inside the apps itself so it can be used during...
marcink -
r2389:0272fdf5 default
parent child Browse files
Show More
@@ -1,169 +1,171 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import urllib
22 import urllib
23 from urlparse import urljoin
23 from urlparse import urljoin
24
24
25
25
26 import requests
26 import requests
27 from webob.exc import HTTPNotAcceptable
27 from webob.exc import HTTPNotAcceptable
28
28
29 from rhodecode.lib.middleware import simplevcs
29 from rhodecode.lib.middleware import simplevcs
30 from rhodecode.lib.utils import is_valid_repo
30 from rhodecode.lib.utils import is_valid_repo
31 from rhodecode.lib.utils2 import str2bool
31 from rhodecode.lib.utils2 import str2bool
32
32
33 log = logging.getLogger(__name__)
33 log = logging.getLogger(__name__)
34
34
35
35
36 class SimpleSvnApp(object):
36 class SimpleSvnApp(object):
37 IGNORED_HEADERS = [
37 IGNORED_HEADERS = [
38 'connection', 'keep-alive', 'content-encoding',
38 'connection', 'keep-alive', 'content-encoding',
39 'transfer-encoding', 'content-length']
39 'transfer-encoding', 'content-length']
40 rc_extras = {}
41
40
42
41 def __init__(self, config):
43 def __init__(self, config):
42 self.config = config
44 self.config = config
43
45
44 def __call__(self, environ, start_response):
46 def __call__(self, environ, start_response):
45 request_headers = self._get_request_headers(environ)
47 request_headers = self._get_request_headers(environ)
46
48
47 data = environ['wsgi.input']
49 data = environ['wsgi.input']
48 # johbo: Avoid that we end up with sending the request in chunked
50 # johbo: Avoid that we end up with sending the request in chunked
49 # transfer encoding (mainly on Gunicorn). If we know the content
51 # transfer encoding (mainly on Gunicorn). If we know the content
50 # length, then we should transfer the payload in one request.
52 # length, then we should transfer the payload in one request.
51 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
53 if environ['REQUEST_METHOD'] == 'MKCOL' or 'CONTENT_LENGTH' in environ:
52 data = data.read()
54 data = data.read()
53
55
54 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
56 log.debug('Calling: %s method via `%s`', environ['REQUEST_METHOD'],
55 self._get_url(environ['PATH_INFO']))
57 self._get_url(environ['PATH_INFO']))
56 response = requests.request(
58 response = requests.request(
57 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
59 environ['REQUEST_METHOD'], self._get_url(environ['PATH_INFO']),
58 data=data, headers=request_headers)
60 data=data, headers=request_headers)
59
61
60 if response.status_code not in [200, 401]:
62 if response.status_code not in [200, 401]:
61 if response.status_code >= 500:
63 if response.status_code >= 500:
62 log.error('Got SVN response:%s with text:`%s`',
64 log.error('Got SVN response:%s with text:`%s`',
63 response, response.text)
65 response, response.text)
64 else:
66 else:
65 log.debug('Got SVN response:%s with text:`%s`',
67 log.debug('Got SVN response:%s with text:`%s`',
66 response, response.text)
68 response, response.text)
67
69
68 response_headers = self._get_response_headers(response.headers)
70 response_headers = self._get_response_headers(response.headers)
69 start_response(
71 start_response(
70 '{} {}'.format(response.status_code, response.reason),
72 '{} {}'.format(response.status_code, response.reason),
71 response_headers)
73 response_headers)
72 return response.iter_content(chunk_size=1024)
74 return response.iter_content(chunk_size=1024)
73
75
74 def _get_url(self, path):
76 def _get_url(self, path):
75 url_path = urljoin(
77 url_path = urljoin(
76 self.config.get('subversion_http_server_url', ''), path)
78 self.config.get('subversion_http_server_url', ''), path)
77 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
79 url_path = urllib.quote(url_path, safe="/:=~+!$,;'")
78 return url_path
80 return url_path
79
81
80 def _get_request_headers(self, environ):
82 def _get_request_headers(self, environ):
81 headers = {}
83 headers = {}
82
84
83 for key in environ:
85 for key in environ:
84 if not key.startswith('HTTP_'):
86 if not key.startswith('HTTP_'):
85 continue
87 continue
86 new_key = key.split('_')
88 new_key = key.split('_')
87 new_key = [k.capitalize() for k in new_key[1:]]
89 new_key = [k.capitalize() for k in new_key[1:]]
88 new_key = '-'.join(new_key)
90 new_key = '-'.join(new_key)
89 headers[new_key] = environ[key]
91 headers[new_key] = environ[key]
90
92
91 if 'CONTENT_TYPE' in environ:
93 if 'CONTENT_TYPE' in environ:
92 headers['Content-Type'] = environ['CONTENT_TYPE']
94 headers['Content-Type'] = environ['CONTENT_TYPE']
93
95
94 if 'CONTENT_LENGTH' in environ:
96 if 'CONTENT_LENGTH' in environ:
95 headers['Content-Length'] = environ['CONTENT_LENGTH']
97 headers['Content-Length'] = environ['CONTENT_LENGTH']
96
98
97 return headers
99 return headers
98
100
99 def _get_response_headers(self, headers):
101 def _get_response_headers(self, headers):
100 headers = [
102 headers = [
101 (h, headers[h])
103 (h, headers[h])
102 for h in headers
104 for h in headers
103 if h.lower() not in self.IGNORED_HEADERS
105 if h.lower() not in self.IGNORED_HEADERS
104 ]
106 ]
105
107
106 return headers
108 return headers
107
109
108
110
109 class DisabledSimpleSvnApp(object):
111 class DisabledSimpleSvnApp(object):
110 def __init__(self, config):
112 def __init__(self, config):
111 self.config = config
113 self.config = config
112
114
113 def __call__(self, environ, start_response):
115 def __call__(self, environ, start_response):
114 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
116 reason = 'Cannot handle SVN call because: SVN HTTP Proxy is not enabled'
115 log.warning(reason)
117 log.warning(reason)
116 return HTTPNotAcceptable(reason)(environ, start_response)
118 return HTTPNotAcceptable(reason)(environ, start_response)
117
119
118
120
119 class SimpleSvn(simplevcs.SimpleVCS):
121 class SimpleSvn(simplevcs.SimpleVCS):
120
122
121 SCM = 'svn'
123 SCM = 'svn'
122 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
124 READ_ONLY_COMMANDS = ('OPTIONS', 'PROPFIND', 'GET', 'REPORT')
123 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
125 DEFAULT_HTTP_SERVER = 'http://localhost:8090'
124
126
125 def _get_repository_name(self, environ):
127 def _get_repository_name(self, environ):
126 """
128 """
127 Gets repository name out of PATH_INFO header
129 Gets repository name out of PATH_INFO header
128
130
129 :param environ: environ where PATH_INFO is stored
131 :param environ: environ where PATH_INFO is stored
130 """
132 """
131 path = environ['PATH_INFO'].split('!')
133 path = environ['PATH_INFO'].split('!')
132 repo_name = path[0].strip('/')
134 repo_name = path[0].strip('/')
133
135
134 # SVN includes the whole path in it's requests, including
136 # SVN includes the whole path in it's requests, including
135 # subdirectories inside the repo. Therefore we have to search for
137 # subdirectories inside the repo. Therefore we have to search for
136 # the repo root directory.
138 # the repo root directory.
137 if not is_valid_repo(repo_name, self.base_path, self.SCM):
139 if not is_valid_repo(repo_name, self.base_path, self.SCM):
138 current_path = ''
140 current_path = ''
139 for component in repo_name.split('/'):
141 for component in repo_name.split('/'):
140 current_path += component
142 current_path += component
141 if is_valid_repo(current_path, self.base_path, self.SCM):
143 if is_valid_repo(current_path, self.base_path, self.SCM):
142 return current_path
144 return current_path
143 current_path += '/'
145 current_path += '/'
144
146
145 return repo_name
147 return repo_name
146
148
147 def _get_action(self, environ):
149 def _get_action(self, environ):
148 return (
150 return (
149 'pull'
151 'pull'
150 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
152 if environ['REQUEST_METHOD'] in self.READ_ONLY_COMMANDS
151 else 'push')
153 else 'push')
152
154
153 def _create_wsgi_app(self, repo_path, repo_name, config):
155 def _create_wsgi_app(self, repo_path, repo_name, config):
154 if self._is_svn_enabled():
156 if self._is_svn_enabled():
155 return SimpleSvnApp(config)
157 return SimpleSvnApp(config)
156 # we don't have http proxy enabled return dummy request handler
158 # we don't have http proxy enabled return dummy request handler
157 return DisabledSimpleSvnApp(config)
159 return DisabledSimpleSvnApp(config)
158
160
159 def _is_svn_enabled(self):
161 def _is_svn_enabled(self):
160 conf = self.repo_vcs_config
162 conf = self.repo_vcs_config
161 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
163 return str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
162
164
163 def _create_config(self, extras, repo_name):
165 def _create_config(self, extras, repo_name):
164 conf = self.repo_vcs_config
166 conf = self.repo_vcs_config
165 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
167 server_url = conf.get('vcs_svn_proxy', 'http_server_url')
166 server_url = server_url or self.DEFAULT_HTTP_SERVER
168 server_url = server_url or self.DEFAULT_HTTP_SERVER
167
169
168 extras['subversion_http_server_url'] = server_url
170 extras['subversion_http_server_url'] = server_url
169 return extras
171 return extras
@@ -1,610 +1,612 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2014-2017 RhodeCode GmbH
3 # Copyright (C) 2014-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
22 SimpleVCS middleware for handling protocol request (push/clone etc.)
23 It's implemented with basic auth function
23 It's implemented with basic auth function
24 """
24 """
25
25
26 import os
26 import os
27 import re
27 import re
28 import logging
28 import logging
29 import importlib
29 import importlib
30 from functools import wraps
30 from functools import wraps
31
31
32 import time
32 import time
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
33 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
34 # TODO(marcink): check if we should use webob.exc here ?
34 # TODO(marcink): check if we should use webob.exc here ?
35 from pyramid.httpexceptions import (
35 from pyramid.httpexceptions import (
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
36 HTTPNotFound, HTTPForbidden, HTTPNotAcceptable, HTTPInternalServerError)
37
37
38 import rhodecode
38 import rhodecode
39 from rhodecode.authentication.base import (
39 from rhodecode.authentication.base import (
40 authenticate, get_perms_cache_manager, VCS_TYPE)
40 authenticate, get_perms_cache_manager, VCS_TYPE)
41 from rhodecode.lib import caches
41 from rhodecode.lib import caches
42 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
42 from rhodecode.lib.auth import AuthUser, HasPermissionAnyMiddleware
43 from rhodecode.lib.base import (
43 from rhodecode.lib.base import (
44 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
44 BasicAuth, get_ip_addr, get_user_agent, vcs_operation_context)
45 from rhodecode.lib.exceptions import (
45 from rhodecode.lib.exceptions import (
46 HTTPLockedRC, HTTPRequirementError, UserCreationError,
46 HTTPLockedRC, HTTPRequirementError, UserCreationError,
47 NotAllowedToCreateUserError)
47 NotAllowedToCreateUserError)
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
48 from rhodecode.lib.hooks_daemon import prepare_callback_daemon
49 from rhodecode.lib.middleware import appenlight
49 from rhodecode.lib.middleware import appenlight
50 from rhodecode.lib.middleware.utils import scm_app_http
50 from rhodecode.lib.middleware.utils import scm_app_http
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
51 from rhodecode.lib.utils import is_valid_repo, SLUG_RE
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
52 from rhodecode.lib.utils2 import safe_str, fix_PATH, str2bool, safe_unicode
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
53 from rhodecode.lib.vcs.conf import settings as vcs_settings
54 from rhodecode.lib.vcs.backends import base
54 from rhodecode.lib.vcs.backends import base
55 from rhodecode.model import meta
55 from rhodecode.model import meta
56 from rhodecode.model.db import User, Repository, PullRequest
56 from rhodecode.model.db import User, Repository, PullRequest
57 from rhodecode.model.scm import ScmModel
57 from rhodecode.model.scm import ScmModel
58 from rhodecode.model.pull_request import PullRequestModel
58 from rhodecode.model.pull_request import PullRequestModel
59 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
59 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
60
60
61 log = logging.getLogger(__name__)
61 log = logging.getLogger(__name__)
62
62
63
63
64 def initialize_generator(factory):
64 def initialize_generator(factory):
65 """
65 """
66 Initializes the returned generator by draining its first element.
66 Initializes the returned generator by draining its first element.
67
67
68 This can be used to give a generator an initializer, which is the code
68 This can be used to give a generator an initializer, which is the code
69 up to the first yield statement. This decorator enforces that the first
69 up to the first yield statement. This decorator enforces that the first
70 produced element has the value ``"__init__"`` to make its special
70 produced element has the value ``"__init__"`` to make its special
71 purpose very explicit in the using code.
71 purpose very explicit in the using code.
72 """
72 """
73
73
74 @wraps(factory)
74 @wraps(factory)
75 def wrapper(*args, **kwargs):
75 def wrapper(*args, **kwargs):
76 gen = factory(*args, **kwargs)
76 gen = factory(*args, **kwargs)
77 try:
77 try:
78 init = gen.next()
78 init = gen.next()
79 except StopIteration:
79 except StopIteration:
80 raise ValueError('Generator must yield at least one element.')
80 raise ValueError('Generator must yield at least one element.')
81 if init != "__init__":
81 if init != "__init__":
82 raise ValueError('First yielded element must be "__init__".')
82 raise ValueError('First yielded element must be "__init__".')
83 return gen
83 return gen
84 return wrapper
84 return wrapper
85
85
86
86
87 class SimpleVCS(object):
87 class SimpleVCS(object):
88 """Common functionality for SCM HTTP handlers."""
88 """Common functionality for SCM HTTP handlers."""
89
89
90 SCM = 'unknown'
90 SCM = 'unknown'
91
91
92 acl_repo_name = None
92 acl_repo_name = None
93 url_repo_name = None
93 url_repo_name = None
94 vcs_repo_name = None
94 vcs_repo_name = None
95 rc_extras = {}
95
96
96 # We have to handle requests to shadow repositories different than requests
97 # We have to handle requests to shadow repositories different than requests
97 # to normal repositories. Therefore we have to distinguish them. To do this
98 # to normal repositories. Therefore we have to distinguish them. To do this
98 # we use this regex which will match only on URLs pointing to shadow
99 # we use this regex which will match only on URLs pointing to shadow
99 # repositories.
100 # repositories.
100 shadow_repo_re = re.compile(
101 shadow_repo_re = re.compile(
101 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
102 '(?P<groups>(?:{slug_pat}/)*)' # repo groups
102 '(?P<target>{slug_pat})/' # target repo
103 '(?P<target>{slug_pat})/' # target repo
103 'pull-request/(?P<pr_id>\d+)/' # pull request
104 'pull-request/(?P<pr_id>\d+)/' # pull request
104 'repository$' # shadow repo
105 'repository$' # shadow repo
105 .format(slug_pat=SLUG_RE.pattern))
106 .format(slug_pat=SLUG_RE.pattern))
106
107
107 def __init__(self, config, registry):
108 def __init__(self, config, registry):
108 self.registry = registry
109 self.registry = registry
109 self.config = config
110 self.config = config
110 # re-populated by specialized middleware
111 # re-populated by specialized middleware
111 self.repo_vcs_config = base.Config()
112 self.repo_vcs_config = base.Config()
112 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
113 self.rhodecode_settings = SettingsModel().get_all_settings(cache=True)
113
114
114 registry.rhodecode_settings = self.rhodecode_settings
115 registry.rhodecode_settings = self.rhodecode_settings
115 # authenticate this VCS request using authfunc
116 # authenticate this VCS request using authfunc
116 auth_ret_code_detection = \
117 auth_ret_code_detection = \
117 str2bool(self.config.get('auth_ret_code_detection', False))
118 str2bool(self.config.get('auth_ret_code_detection', False))
118 self.authenticate = BasicAuth(
119 self.authenticate = BasicAuth(
119 '', authenticate, registry, config.get('auth_ret_code'),
120 '', authenticate, registry, config.get('auth_ret_code'),
120 auth_ret_code_detection)
121 auth_ret_code_detection)
121 self.ip_addr = '0.0.0.0'
122 self.ip_addr = '0.0.0.0'
122
123
123 @property
124 @property
124 def base_path(self):
125 def base_path(self):
125 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
126 settings_path = self.repo_vcs_config.get(*VcsSettingsModel.PATH_SETTING)
126 if not settings_path:
127 if not settings_path:
127 # try, maybe we passed in explicitly as config option
128 # try, maybe we passed in explicitly as config option
128 settings_path = self.config.get('base_path')
129 settings_path = self.config.get('base_path')
129 return settings_path
130 return settings_path
130
131
131 def set_repo_names(self, environ):
132 def set_repo_names(self, environ):
132 """
133 """
133 This will populate the attributes acl_repo_name, url_repo_name,
134 This will populate the attributes acl_repo_name, url_repo_name,
134 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
135 vcs_repo_name and is_shadow_repo. In case of requests to normal (non
135 shadow) repositories all names are equal. In case of requests to a
136 shadow) repositories all names are equal. In case of requests to a
136 shadow repository the acl-name points to the target repo of the pull
137 shadow repository the acl-name points to the target repo of the pull
137 request and the vcs-name points to the shadow repo file system path.
138 request and the vcs-name points to the shadow repo file system path.
138 The url-name is always the URL used by the vcs client program.
139 The url-name is always the URL used by the vcs client program.
139
140
140 Example in case of a shadow repo:
141 Example in case of a shadow repo:
141 acl_repo_name = RepoGroup/MyRepo
142 acl_repo_name = RepoGroup/MyRepo
142 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
143 url_repo_name = RepoGroup/MyRepo/pull-request/3/repository
143 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
144 vcs_repo_name = /repo/base/path/RepoGroup/.__shadow_MyRepo_pr-3'
144 """
145 """
145 # First we set the repo name from URL for all attributes. This is the
146 # First we set the repo name from URL for all attributes. This is the
146 # default if handling normal (non shadow) repo requests.
147 # default if handling normal (non shadow) repo requests.
147 self.url_repo_name = self._get_repository_name(environ)
148 self.url_repo_name = self._get_repository_name(environ)
148 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
149 self.acl_repo_name = self.vcs_repo_name = self.url_repo_name
149 self.is_shadow_repo = False
150 self.is_shadow_repo = False
150
151
151 # Check if this is a request to a shadow repository.
152 # Check if this is a request to a shadow repository.
152 match = self.shadow_repo_re.match(self.url_repo_name)
153 match = self.shadow_repo_re.match(self.url_repo_name)
153 if match:
154 if match:
154 match_dict = match.groupdict()
155 match_dict = match.groupdict()
155
156
156 # Build acl repo name from regex match.
157 # Build acl repo name from regex match.
157 acl_repo_name = safe_unicode('{groups}{target}'.format(
158 acl_repo_name = safe_unicode('{groups}{target}'.format(
158 groups=match_dict['groups'] or '',
159 groups=match_dict['groups'] or '',
159 target=match_dict['target']))
160 target=match_dict['target']))
160
161
161 # Retrieve pull request instance by ID from regex match.
162 # Retrieve pull request instance by ID from regex match.
162 pull_request = PullRequest.get(match_dict['pr_id'])
163 pull_request = PullRequest.get(match_dict['pr_id'])
163
164
164 # Only proceed if we got a pull request and if acl repo name from
165 # Only proceed if we got a pull request and if acl repo name from
165 # URL equals the target repo name of the pull request.
166 # URL equals the target repo name of the pull request.
166 if pull_request and (acl_repo_name ==
167 if pull_request and (acl_repo_name ==
167 pull_request.target_repo.repo_name):
168 pull_request.target_repo.repo_name):
168 # Get file system path to shadow repository.
169 # Get file system path to shadow repository.
169 workspace_id = PullRequestModel()._workspace_id(pull_request)
170 workspace_id = PullRequestModel()._workspace_id(pull_request)
170 target_vcs = pull_request.target_repo.scm_instance()
171 target_vcs = pull_request.target_repo.scm_instance()
171 vcs_repo_name = target_vcs._get_shadow_repository_path(
172 vcs_repo_name = target_vcs._get_shadow_repository_path(
172 workspace_id)
173 workspace_id)
173
174
174 # Store names for later usage.
175 # Store names for later usage.
175 self.vcs_repo_name = vcs_repo_name
176 self.vcs_repo_name = vcs_repo_name
176 self.acl_repo_name = acl_repo_name
177 self.acl_repo_name = acl_repo_name
177 self.is_shadow_repo = True
178 self.is_shadow_repo = True
178
179
179 log.debug('Setting all VCS repository names: %s', {
180 log.debug('Setting all VCS repository names: %s', {
180 'acl_repo_name': self.acl_repo_name,
181 'acl_repo_name': self.acl_repo_name,
181 'url_repo_name': self.url_repo_name,
182 'url_repo_name': self.url_repo_name,
182 'vcs_repo_name': self.vcs_repo_name,
183 'vcs_repo_name': self.vcs_repo_name,
183 })
184 })
184
185
185 @property
186 @property
186 def scm_app(self):
187 def scm_app(self):
187 custom_implementation = self.config['vcs.scm_app_implementation']
188 custom_implementation = self.config['vcs.scm_app_implementation']
188 if custom_implementation == 'http':
189 if custom_implementation == 'http':
189 log.info('Using HTTP implementation of scm app.')
190 log.info('Using HTTP implementation of scm app.')
190 scm_app_impl = scm_app_http
191 scm_app_impl = scm_app_http
191 else:
192 else:
192 log.info('Using custom implementation of scm_app: "{}"'.format(
193 log.info('Using custom implementation of scm_app: "{}"'.format(
193 custom_implementation))
194 custom_implementation))
194 scm_app_impl = importlib.import_module(custom_implementation)
195 scm_app_impl = importlib.import_module(custom_implementation)
195 return scm_app_impl
196 return scm_app_impl
196
197
197 def _get_by_id(self, repo_name):
198 def _get_by_id(self, repo_name):
198 """
199 """
199 Gets a special pattern _<ID> from clone url and tries to replace it
200 Gets a special pattern _<ID> from clone url and tries to replace it
200 with a repository_name for support of _<ID> non changeable urls
201 with a repository_name for support of _<ID> non changeable urls
201 """
202 """
202
203
203 data = repo_name.split('/')
204 data = repo_name.split('/')
204 if len(data) >= 2:
205 if len(data) >= 2:
205 from rhodecode.model.repo import RepoModel
206 from rhodecode.model.repo import RepoModel
206 by_id_match = RepoModel().get_repo_by_id(repo_name)
207 by_id_match = RepoModel().get_repo_by_id(repo_name)
207 if by_id_match:
208 if by_id_match:
208 data[1] = by_id_match.repo_name
209 data[1] = by_id_match.repo_name
209
210
210 return safe_str('/'.join(data))
211 return safe_str('/'.join(data))
211
212
212 def _invalidate_cache(self, repo_name):
213 def _invalidate_cache(self, repo_name):
213 """
214 """
214 Set's cache for this repository for invalidation on next access
215 Set's cache for this repository for invalidation on next access
215
216
216 :param repo_name: full repo name, also a cache key
217 :param repo_name: full repo name, also a cache key
217 """
218 """
218 ScmModel().mark_for_invalidation(repo_name)
219 ScmModel().mark_for_invalidation(repo_name)
219
220
220 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
221 def is_valid_and_existing_repo(self, repo_name, base_path, scm_type):
221 db_repo = Repository.get_by_repo_name(repo_name)
222 db_repo = Repository.get_by_repo_name(repo_name)
222 if not db_repo:
223 if not db_repo:
223 log.debug('Repository `%s` not found inside the database.',
224 log.debug('Repository `%s` not found inside the database.',
224 repo_name)
225 repo_name)
225 return False
226 return False
226
227
227 if db_repo.repo_type != scm_type:
228 if db_repo.repo_type != scm_type:
228 log.warning(
229 log.warning(
229 'Repository `%s` have incorrect scm_type, expected %s got %s',
230 'Repository `%s` have incorrect scm_type, expected %s got %s',
230 repo_name, db_repo.repo_type, scm_type)
231 repo_name, db_repo.repo_type, scm_type)
231 return False
232 return False
232
233
233 return is_valid_repo(repo_name, base_path,
234 return is_valid_repo(repo_name, base_path,
234 explicit_scm=scm_type, expect_scm=scm_type)
235 explicit_scm=scm_type, expect_scm=scm_type)
235
236
236 def valid_and_active_user(self, user):
237 def valid_and_active_user(self, user):
237 """
238 """
238 Checks if that user is not empty, and if it's actually object it checks
239 Checks if that user is not empty, and if it's actually object it checks
239 if he's active.
240 if he's active.
240
241
241 :param user: user object or None
242 :param user: user object or None
242 :return: boolean
243 :return: boolean
243 """
244 """
244 if user is None:
245 if user is None:
245 return False
246 return False
246
247
247 elif user.active:
248 elif user.active:
248 return True
249 return True
249
250
250 return False
251 return False
251
252
252 @property
253 @property
253 def is_shadow_repo_dir(self):
254 def is_shadow_repo_dir(self):
254 return os.path.isdir(self.vcs_repo_name)
255 return os.path.isdir(self.vcs_repo_name)
255
256
256 def _check_permission(self, action, user, repo_name, ip_addr=None,
257 def _check_permission(self, action, user, repo_name, ip_addr=None,
257 plugin_id='', plugin_cache_active=False, cache_ttl=0):
258 plugin_id='', plugin_cache_active=False, cache_ttl=0):
258 """
259 """
259 Checks permissions using action (push/pull) user and repository
260 Checks permissions using action (push/pull) user and repository
260 name. If plugin_cache and ttl is set it will use the plugin which
261 name. If plugin_cache and ttl is set it will use the plugin which
261 authenticated the user to store the cached permissions result for N
262 authenticated the user to store the cached permissions result for N
262 amount of seconds as in cache_ttl
263 amount of seconds as in cache_ttl
263
264
264 :param action: push or pull action
265 :param action: push or pull action
265 :param user: user instance
266 :param user: user instance
266 :param repo_name: repository name
267 :param repo_name: repository name
267 """
268 """
268
269
269 # get instance of cache manager configured for a namespace
270 # get instance of cache manager configured for a namespace
270 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
271 cache_manager = get_perms_cache_manager(custom_ttl=cache_ttl)
271 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
272 log.debug('AUTH_CACHE_TTL for permissions `%s` active: %s (TTL: %s)',
272 plugin_id, plugin_cache_active, cache_ttl)
273 plugin_id, plugin_cache_active, cache_ttl)
273
274
274 # for environ based password can be empty, but then the validation is
275 # for environ based password can be empty, but then the validation is
275 # on the server that fills in the env data needed for authentication
276 # on the server that fills in the env data needed for authentication
276 _perm_calc_hash = caches.compute_key_from_params(
277 _perm_calc_hash = caches.compute_key_from_params(
277 plugin_id, action, user.user_id, repo_name, ip_addr)
278 plugin_id, action, user.user_id, repo_name, ip_addr)
278
279
279 # _authenticate is a wrapper for .auth() method of plugin.
280 # _authenticate is a wrapper for .auth() method of plugin.
280 # it checks if .auth() sends proper data.
281 # it checks if .auth() sends proper data.
281 # For RhodeCodeExternalAuthPlugin it also maps users to
282 # For RhodeCodeExternalAuthPlugin it also maps users to
282 # Database and maps the attributes returned from .auth()
283 # Database and maps the attributes returned from .auth()
283 # to RhodeCode database. If this function returns data
284 # to RhodeCode database. If this function returns data
284 # then auth is correct.
285 # then auth is correct.
285 start = time.time()
286 start = time.time()
286 log.debug('Running plugin `%s` permissions check', plugin_id)
287 log.debug('Running plugin `%s` permissions check', plugin_id)
287
288
288 def perm_func():
289 def perm_func():
289 """
290 """
290 This function is used internally in Cache of Beaker to calculate
291 This function is used internally in Cache of Beaker to calculate
291 Results
292 Results
292 """
293 """
293 log.debug('auth: calculating permission access now...')
294 log.debug('auth: calculating permission access now...')
294 # check IP
295 # check IP
295 inherit = user.inherit_default_permissions
296 inherit = user.inherit_default_permissions
296 ip_allowed = AuthUser.check_ip_allowed(
297 ip_allowed = AuthUser.check_ip_allowed(
297 user.user_id, ip_addr, inherit_from_default=inherit)
298 user.user_id, ip_addr, inherit_from_default=inherit)
298 if ip_allowed:
299 if ip_allowed:
299 log.info('Access for IP:%s allowed', ip_addr)
300 log.info('Access for IP:%s allowed', ip_addr)
300 else:
301 else:
301 return False
302 return False
302
303
303 if action == 'push':
304 if action == 'push':
304 perms = ('repository.write', 'repository.admin')
305 perms = ('repository.write', 'repository.admin')
305 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
306 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
306 return False
307 return False
307
308
308 else:
309 else:
309 # any other action need at least read permission
310 # any other action need at least read permission
310 perms = (
311 perms = (
311 'repository.read', 'repository.write', 'repository.admin')
312 'repository.read', 'repository.write', 'repository.admin')
312 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
313 if not HasPermissionAnyMiddleware(*perms)(user, repo_name):
313 return False
314 return False
314
315
315 return True
316 return True
316
317
317 if plugin_cache_active:
318 if plugin_cache_active:
318 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
319 log.debug('Trying to fetch cached perms by %s', _perm_calc_hash[:6])
319 perm_result = cache_manager.get(
320 perm_result = cache_manager.get(
320 _perm_calc_hash, createfunc=perm_func)
321 _perm_calc_hash, createfunc=perm_func)
321 else:
322 else:
322 perm_result = perm_func()
323 perm_result = perm_func()
323
324
324 auth_time = time.time() - start
325 auth_time = time.time() - start
325 log.debug('Permissions for plugin `%s` completed in %.3fs, '
326 log.debug('Permissions for plugin `%s` completed in %.3fs, '
326 'expiration time of fetched cache %.1fs.',
327 'expiration time of fetched cache %.1fs.',
327 plugin_id, auth_time, cache_ttl)
328 plugin_id, auth_time, cache_ttl)
328
329
329 return perm_result
330 return perm_result
330
331
331 def _check_ssl(self, environ, start_response):
332 def _check_ssl(self, environ, start_response):
332 """
333 """
333 Checks the SSL check flag and returns False if SSL is not present
334 Checks the SSL check flag and returns False if SSL is not present
334 and required True otherwise
335 and required True otherwise
335 """
336 """
336 org_proto = environ['wsgi._org_proto']
337 org_proto = environ['wsgi._org_proto']
337 # check if we have SSL required ! if not it's a bad request !
338 # check if we have SSL required ! if not it's a bad request !
338 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
339 require_ssl = str2bool(self.repo_vcs_config.get('web', 'push_ssl'))
339 if require_ssl and org_proto == 'http':
340 if require_ssl and org_proto == 'http':
340 log.debug('proto is %s and SSL is required BAD REQUEST !',
341 log.debug('proto is %s and SSL is required BAD REQUEST !',
341 org_proto)
342 org_proto)
342 return False
343 return False
343 return True
344 return True
344
345
345 def __call__(self, environ, start_response):
346 def __call__(self, environ, start_response):
346 try:
347 try:
347 return self._handle_request(environ, start_response)
348 return self._handle_request(environ, start_response)
348 except Exception:
349 except Exception:
349 log.exception("Exception while handling request")
350 log.exception("Exception while handling request")
350 appenlight.track_exception(environ)
351 appenlight.track_exception(environ)
351 return HTTPInternalServerError()(environ, start_response)
352 return HTTPInternalServerError()(environ, start_response)
352 finally:
353 finally:
353 meta.Session.remove()
354 meta.Session.remove()
354
355
355 def _handle_request(self, environ, start_response):
356 def _handle_request(self, environ, start_response):
356
357
357 if not self._check_ssl(environ, start_response):
358 if not self._check_ssl(environ, start_response):
358 reason = ('SSL required, while RhodeCode was unable '
359 reason = ('SSL required, while RhodeCode was unable '
359 'to detect this as SSL request')
360 'to detect this as SSL request')
360 log.debug('User not allowed to proceed, %s', reason)
361 log.debug('User not allowed to proceed, %s', reason)
361 return HTTPNotAcceptable(reason)(environ, start_response)
362 return HTTPNotAcceptable(reason)(environ, start_response)
362
363
363 if not self.url_repo_name:
364 if not self.url_repo_name:
364 log.warning('Repository name is empty: %s', self.url_repo_name)
365 log.warning('Repository name is empty: %s', self.url_repo_name)
365 # failed to get repo name, we fail now
366 # failed to get repo name, we fail now
366 return HTTPNotFound()(environ, start_response)
367 return HTTPNotFound()(environ, start_response)
367 log.debug('Extracted repo name is %s', self.url_repo_name)
368 log.debug('Extracted repo name is %s', self.url_repo_name)
368
369
369 ip_addr = get_ip_addr(environ)
370 ip_addr = get_ip_addr(environ)
370 user_agent = get_user_agent(environ)
371 user_agent = get_user_agent(environ)
371 username = None
372 username = None
372
373
373 # skip passing error to error controller
374 # skip passing error to error controller
374 environ['pylons.status_code_redirect'] = True
375 environ['pylons.status_code_redirect'] = True
375
376
376 # ======================================================================
377 # ======================================================================
377 # GET ACTION PULL or PUSH
378 # GET ACTION PULL or PUSH
378 # ======================================================================
379 # ======================================================================
379 action = self._get_action(environ)
380 action = self._get_action(environ)
380
381
381 # ======================================================================
382 # ======================================================================
382 # Check if this is a request to a shadow repository of a pull request.
383 # Check if this is a request to a shadow repository of a pull request.
383 # In this case only pull action is allowed.
384 # In this case only pull action is allowed.
384 # ======================================================================
385 # ======================================================================
385 if self.is_shadow_repo and action != 'pull':
386 if self.is_shadow_repo and action != 'pull':
386 reason = 'Only pull action is allowed for shadow repositories.'
387 reason = 'Only pull action is allowed for shadow repositories.'
387 log.debug('User not allowed to proceed, %s', reason)
388 log.debug('User not allowed to proceed, %s', reason)
388 return HTTPNotAcceptable(reason)(environ, start_response)
389 return HTTPNotAcceptable(reason)(environ, start_response)
389
390
390 # Check if the shadow repo actually exists, in case someone refers
391 # Check if the shadow repo actually exists, in case someone refers
391 # to it, and it has been deleted because of successful merge.
392 # to it, and it has been deleted because of successful merge.
392 if self.is_shadow_repo and not self.is_shadow_repo_dir:
393 if self.is_shadow_repo and not self.is_shadow_repo_dir:
393 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
394 log.debug('Shadow repo detected, and shadow repo dir `%s` is missing',
394 self.is_shadow_repo_dir)
395 self.is_shadow_repo_dir)
395 return HTTPNotFound()(environ, start_response)
396 return HTTPNotFound()(environ, start_response)
396
397
397 # ======================================================================
398 # ======================================================================
398 # CHECK ANONYMOUS PERMISSION
399 # CHECK ANONYMOUS PERMISSION
399 # ======================================================================
400 # ======================================================================
400 if action in ['pull', 'push']:
401 if action in ['pull', 'push']:
401 anonymous_user = User.get_default_user()
402 anonymous_user = User.get_default_user()
402 username = anonymous_user.username
403 username = anonymous_user.username
403 if anonymous_user.active:
404 if anonymous_user.active:
404 # ONLY check permissions if the user is activated
405 # ONLY check permissions if the user is activated
405 anonymous_perm = self._check_permission(
406 anonymous_perm = self._check_permission(
406 action, anonymous_user, self.acl_repo_name, ip_addr)
407 action, anonymous_user, self.acl_repo_name, ip_addr)
407 else:
408 else:
408 anonymous_perm = False
409 anonymous_perm = False
409
410
410 if not anonymous_user.active or not anonymous_perm:
411 if not anonymous_user.active or not anonymous_perm:
411 if not anonymous_user.active:
412 if not anonymous_user.active:
412 log.debug('Anonymous access is disabled, running '
413 log.debug('Anonymous access is disabled, running '
413 'authentication')
414 'authentication')
414
415
415 if not anonymous_perm:
416 if not anonymous_perm:
416 log.debug('Not enough credentials to access this '
417 log.debug('Not enough credentials to access this '
417 'repository as anonymous user')
418 'repository as anonymous user')
418
419
419 username = None
420 username = None
420 # ==============================================================
421 # ==============================================================
421 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
422 # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
422 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
423 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
423 # ==============================================================
424 # ==============================================================
424
425
425 # try to auth based on environ, container auth methods
426 # try to auth based on environ, container auth methods
426 log.debug('Running PRE-AUTH for container based authentication')
427 log.debug('Running PRE-AUTH for container based authentication')
427 pre_auth = authenticate(
428 pre_auth = authenticate(
428 '', '', environ, VCS_TYPE, registry=self.registry,
429 '', '', environ, VCS_TYPE, registry=self.registry,
429 acl_repo_name=self.acl_repo_name)
430 acl_repo_name=self.acl_repo_name)
430 if pre_auth and pre_auth.get('username'):
431 if pre_auth and pre_auth.get('username'):
431 username = pre_auth['username']
432 username = pre_auth['username']
432 log.debug('PRE-AUTH got %s as username', username)
433 log.debug('PRE-AUTH got %s as username', username)
433 if pre_auth:
434 if pre_auth:
434 log.debug('PRE-AUTH successful from %s',
435 log.debug('PRE-AUTH successful from %s',
435 pre_auth.get('auth_data', {}).get('_plugin'))
436 pre_auth.get('auth_data', {}).get('_plugin'))
436
437
437 # If not authenticated by the container, running basic auth
438 # If not authenticated by the container, running basic auth
438 # before inject the calling repo_name for special scope checks
439 # before inject the calling repo_name for special scope checks
439 self.authenticate.acl_repo_name = self.acl_repo_name
440 self.authenticate.acl_repo_name = self.acl_repo_name
440
441
441 plugin_cache_active, cache_ttl = False, 0
442 plugin_cache_active, cache_ttl = False, 0
442 plugin = None
443 plugin = None
443 if not username:
444 if not username:
444 self.authenticate.realm = self.authenticate.get_rc_realm()
445 self.authenticate.realm = self.authenticate.get_rc_realm()
445
446
446 try:
447 try:
447 auth_result = self.authenticate(environ)
448 auth_result = self.authenticate(environ)
448 except (UserCreationError, NotAllowedToCreateUserError) as e:
449 except (UserCreationError, NotAllowedToCreateUserError) as e:
449 log.error(e)
450 log.error(e)
450 reason = safe_str(e)
451 reason = safe_str(e)
451 return HTTPNotAcceptable(reason)(environ, start_response)
452 return HTTPNotAcceptable(reason)(environ, start_response)
452
453
453 if isinstance(auth_result, dict):
454 if isinstance(auth_result, dict):
454 AUTH_TYPE.update(environ, 'basic')
455 AUTH_TYPE.update(environ, 'basic')
455 REMOTE_USER.update(environ, auth_result['username'])
456 REMOTE_USER.update(environ, auth_result['username'])
456 username = auth_result['username']
457 username = auth_result['username']
457 plugin = auth_result.get('auth_data', {}).get('_plugin')
458 plugin = auth_result.get('auth_data', {}).get('_plugin')
458 log.info(
459 log.info(
459 'MAIN-AUTH successful for user `%s` from %s plugin',
460 'MAIN-AUTH successful for user `%s` from %s plugin',
460 username, plugin)
461 username, plugin)
461
462
462 plugin_cache_active, cache_ttl = auth_result.get(
463 plugin_cache_active, cache_ttl = auth_result.get(
463 'auth_data', {}).get('_ttl_cache') or (False, 0)
464 'auth_data', {}).get('_ttl_cache') or (False, 0)
464 else:
465 else:
465 return auth_result.wsgi_application(
466 return auth_result.wsgi_application(
466 environ, start_response)
467 environ, start_response)
467
468
468
469
469 # ==============================================================
470 # ==============================================================
470 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
471 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
471 # ==============================================================
472 # ==============================================================
472 user = User.get_by_username(username)
473 user = User.get_by_username(username)
473 if not self.valid_and_active_user(user):
474 if not self.valid_and_active_user(user):
474 return HTTPForbidden()(environ, start_response)
475 return HTTPForbidden()(environ, start_response)
475 username = user.username
476 username = user.username
476 user.update_lastactivity()
477 user.update_lastactivity()
477 meta.Session().commit()
478 meta.Session().commit()
478
479
479 # check user attributes for password change flag
480 # check user attributes for password change flag
480 user_obj = user
481 user_obj = user
481 if user_obj and user_obj.username != User.DEFAULT_USER and \
482 if user_obj and user_obj.username != User.DEFAULT_USER and \
482 user_obj.user_data.get('force_password_change'):
483 user_obj.user_data.get('force_password_change'):
483 reason = 'password change required'
484 reason = 'password change required'
484 log.debug('User not allowed to authenticate, %s', reason)
485 log.debug('User not allowed to authenticate, %s', reason)
485 return HTTPNotAcceptable(reason)(environ, start_response)
486 return HTTPNotAcceptable(reason)(environ, start_response)
486
487
487 # check permissions for this repository
488 # check permissions for this repository
488 perm = self._check_permission(
489 perm = self._check_permission(
489 action, user, self.acl_repo_name, ip_addr,
490 action, user, self.acl_repo_name, ip_addr,
490 plugin, plugin_cache_active, cache_ttl)
491 plugin, plugin_cache_active, cache_ttl)
491 if not perm:
492 if not perm:
492 return HTTPForbidden()(environ, start_response)
493 return HTTPForbidden()(environ, start_response)
493
494
494 # extras are injected into UI object and later available
495 # extras are injected into UI object and later available
495 # in hooks executed by RhodeCode
496 # in hooks executed by RhodeCode
496 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
497 check_locking = _should_check_locking(environ.get('QUERY_STRING'))
497 extras = vcs_operation_context(
498 extras = vcs_operation_context(
498 environ, repo_name=self.acl_repo_name, username=username,
499 environ, repo_name=self.acl_repo_name, username=username,
499 action=action, scm=self.SCM, check_locking=check_locking,
500 action=action, scm=self.SCM, check_locking=check_locking,
500 is_shadow_repo=self.is_shadow_repo
501 is_shadow_repo=self.is_shadow_repo
501 )
502 )
502
503
503 # ======================================================================
504 # ======================================================================
504 # REQUEST HANDLING
505 # REQUEST HANDLING
505 # ======================================================================
506 # ======================================================================
506 repo_path = os.path.join(
507 repo_path = os.path.join(
507 safe_str(self.base_path), safe_str(self.vcs_repo_name))
508 safe_str(self.base_path), safe_str(self.vcs_repo_name))
508 log.debug('Repository path is %s', repo_path)
509 log.debug('Repository path is %s', repo_path)
509
510
510 fix_PATH()
511 fix_PATH()
511
512
512 log.info(
513 log.info(
513 '%s action on %s repo "%s" by "%s" from %s %s',
514 '%s action on %s repo "%s" by "%s" from %s %s',
514 action, self.SCM, safe_str(self.url_repo_name),
515 action, self.SCM, safe_str(self.url_repo_name),
515 safe_str(username), ip_addr, user_agent)
516 safe_str(username), ip_addr, user_agent)
516
517
517 return self._generate_vcs_response(
518 return self._generate_vcs_response(
518 environ, start_response, repo_path, extras, action)
519 environ, start_response, repo_path, extras, action)
519
520
520 @initialize_generator
521 @initialize_generator
521 def _generate_vcs_response(
522 def _generate_vcs_response(
522 self, environ, start_response, repo_path, extras, action):
523 self, environ, start_response, repo_path, extras, action):
523 """
524 """
524 Returns a generator for the response content.
525 Returns a generator for the response content.
525
526
526 This method is implemented as a generator, so that it can trigger
527 This method is implemented as a generator, so that it can trigger
527 the cache validation after all content sent back to the client. It
528 the cache validation after all content sent back to the client. It
528 also handles the locking exceptions which will be triggered when
529 also handles the locking exceptions which will be triggered when
529 the first chunk is produced by the underlying WSGI application.
530 the first chunk is produced by the underlying WSGI application.
530 """
531 """
531 callback_daemon, extras = self._prepare_callback_daemon(extras)
532 callback_daemon, extras = self._prepare_callback_daemon(extras)
532 config = self._create_config(extras, self.acl_repo_name)
533 config = self._create_config(extras, self.acl_repo_name)
533 log.debug('HOOKS extras is %s', extras)
534 log.debug('HOOKS extras is %s', extras)
534 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
535 app = self._create_wsgi_app(repo_path, self.url_repo_name, config)
536 app.rc_extras = extras
535
537
536 try:
538 try:
537 with callback_daemon:
539 with callback_daemon:
538 try:
540 try:
539 response = app(environ, start_response)
541 response = app(environ, start_response)
540 finally:
542 finally:
541 # This statement works together with the decorator
543 # This statement works together with the decorator
542 # "initialize_generator" above. The decorator ensures that
544 # "initialize_generator" above. The decorator ensures that
543 # we hit the first yield statement before the generator is
545 # we hit the first yield statement before the generator is
544 # returned back to the WSGI server. This is needed to
546 # returned back to the WSGI server. This is needed to
545 # ensure that the call to "app" above triggers the
547 # ensure that the call to "app" above triggers the
546 # needed callback to "start_response" before the
548 # needed callback to "start_response" before the
547 # generator is actually used.
549 # generator is actually used.
548 yield "__init__"
550 yield "__init__"
549
551
550 for chunk in response:
552 for chunk in response:
551 yield chunk
553 yield chunk
552 except Exception as exc:
554 except Exception as exc:
553 # TODO: martinb: Exceptions are only raised in case of the Pyro4
555 # TODO: martinb: Exceptions are only raised in case of the Pyro4
554 # backend. Refactor this except block after dropping Pyro4 support.
556 # backend. Refactor this except block after dropping Pyro4 support.
555 # TODO: johbo: Improve "translating" back the exception.
557 # TODO: johbo: Improve "translating" back the exception.
556 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
558 if getattr(exc, '_vcs_kind', None) == 'repo_locked':
557 exc = HTTPLockedRC(*exc.args)
559 exc = HTTPLockedRC(*exc.args)
558 _code = rhodecode.CONFIG.get('lock_ret_code')
560 _code = rhodecode.CONFIG.get('lock_ret_code')
559 log.debug('Repository LOCKED ret code %s!', (_code,))
561 log.debug('Repository LOCKED ret code %s!', (_code,))
560 elif getattr(exc, '_vcs_kind', None) == 'requirement':
562 elif getattr(exc, '_vcs_kind', None) == 'requirement':
561 log.debug(
563 log.debug(
562 'Repository requires features unknown to this Mercurial')
564 'Repository requires features unknown to this Mercurial')
563 exc = HTTPRequirementError(*exc.args)
565 exc = HTTPRequirementError(*exc.args)
564 else:
566 else:
565 raise
567 raise
566
568
567 for chunk in exc(environ, start_response):
569 for chunk in exc(environ, start_response):
568 yield chunk
570 yield chunk
569 finally:
571 finally:
570 # invalidate cache on push
572 # invalidate cache on push
571 try:
573 try:
572 if action == 'push':
574 if action == 'push':
573 self._invalidate_cache(self.url_repo_name)
575 self._invalidate_cache(self.url_repo_name)
574 finally:
576 finally:
575 meta.Session.remove()
577 meta.Session.remove()
576
578
577 def _get_repository_name(self, environ):
579 def _get_repository_name(self, environ):
578 """Get repository name out of the environmnent
580 """Get repository name out of the environmnent
579
581
580 :param environ: WSGI environment
582 :param environ: WSGI environment
581 """
583 """
582 raise NotImplementedError()
584 raise NotImplementedError()
583
585
584 def _get_action(self, environ):
586 def _get_action(self, environ):
585 """Map request commands into a pull or push command.
587 """Map request commands into a pull or push command.
586
588
587 :param environ: WSGI environment
589 :param environ: WSGI environment
588 """
590 """
589 raise NotImplementedError()
591 raise NotImplementedError()
590
592
591 def _create_wsgi_app(self, repo_path, repo_name, config):
593 def _create_wsgi_app(self, repo_path, repo_name, config):
592 """Return the WSGI app that will finally handle the request."""
594 """Return the WSGI app that will finally handle the request."""
593 raise NotImplementedError()
595 raise NotImplementedError()
594
596
595 def _create_config(self, extras, repo_name):
597 def _create_config(self, extras, repo_name):
596 """Create a safe config representation."""
598 """Create a safe config representation."""
597 raise NotImplementedError()
599 raise NotImplementedError()
598
600
599 def _prepare_callback_daemon(self, extras):
601 def _prepare_callback_daemon(self, extras):
600 return prepare_callback_daemon(
602 return prepare_callback_daemon(
601 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
603 extras, protocol=vcs_settings.HOOKS_PROTOCOL,
602 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
604 use_direct_calls=vcs_settings.HOOKS_DIRECT_CALLS)
603
605
604
606
605 def _should_check_locking(query_string):
607 def _should_check_locking(query_string):
606 # this is kind of hacky, but due to how mercurial handles client-server
608 # this is kind of hacky, but due to how mercurial handles client-server
607 # server see all operation on commit; bookmarks, phases and
609 # server see all operation on commit; bookmarks, phases and
608 # obsolescence marker in different transaction, we don't want to check
610 # obsolescence marker in different transaction, we don't want to check
609 # locking on those
611 # locking on those
610 return query_string not in ['cmd=listkeys']
612 return query_string not in ['cmd=listkeys']
General Comments 0
You need to be logged in to leave comments. Login now