Show More
@@ -91,7 +91,6 b' class SshWrapper(object):' | |||||
91 |
|
91 | |||
92 | def get_repo_details(self, mode): |
|
92 | def get_repo_details(self, mode): | |
93 | vcs_type = mode if mode in ['svn', 'hg', 'git'] else None |
|
93 | vcs_type = mode if mode in ['svn', 'hg', 'git'] else None | |
94 | mode = mode |
|
|||
95 | repo_name = None |
|
94 | repo_name = None | |
96 |
|
95 | |||
97 | hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$' |
|
96 | hg_pattern = r'^hg\s+\-R\s+(\S+)\s+serve\s+\-\-stdio$' | |
@@ -101,8 +100,7 b' class SshWrapper(object):' | |||||
101 | repo_name = hg_match.group(1).strip('/') |
|
100 | repo_name = hg_match.group(1).strip('/') | |
102 | return vcs_type, repo_name, mode |
|
101 | return vcs_type, repo_name, mode | |
103 |
|
102 | |||
104 | git_pattern = ( |
|
103 | git_pattern = r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$' | |
105 | r'^git-(receive-pack|upload-pack)\s\'[/]?(\S+?)(|\.git)\'$') |
|
|||
106 | git_match = re.match(git_pattern, self.command) |
|
104 | git_match = re.match(git_pattern, self.command) | |
107 | if git_match is not None: |
|
105 | if git_match is not None: | |
108 | vcs_type = 'git' |
|
106 | vcs_type = 'git' | |
@@ -115,7 +113,8 b' class SshWrapper(object):' | |||||
115 |
|
113 | |||
116 | if svn_match is not None: |
|
114 | if svn_match is not None: | |
117 | vcs_type = 'svn' |
|
115 | vcs_type = 'svn' | |
118 | # Repo name should be extracted from the input stream |
|
116 | # Repo name should be extracted from the input stream, we're unable to | |
|
117 | # extract it at this point in execution | |||
119 | return vcs_type, repo_name, mode |
|
118 | return vcs_type, repo_name, mode | |
120 |
|
119 | |||
121 | return vcs_type, repo_name, mode |
|
120 | return vcs_type, repo_name, mode | |
@@ -188,8 +187,7 b' class SshWrapper(object):' | |||||
188 | log.debug('SSH Connection info %s', self.get_connection_info()) |
|
187 | log.debug('SSH Connection info %s', self.get_connection_info()) | |
189 |
|
188 | |||
190 | if shell and self.command is None: |
|
189 | if shell and self.command is None: | |
191 | log.info( |
|
190 | log.info('Dropping to shell, no command given and shell is allowed') | |
192 | 'Dropping to shell, no command given and shell is allowed') |
|
|||
193 | os.execl('/bin/bash', '-l') |
|
191 | os.execl('/bin/bash', '-l') | |
194 | exit_code = 1 |
|
192 | exit_code = 1 | |
195 |
|
193 | |||
@@ -216,8 +214,7 b' class SshWrapper(object):' | |||||
216 | exit_code = -1 |
|
214 | exit_code = -1 | |
217 |
|
215 | |||
218 | else: |
|
216 | else: | |
219 | log.error( |
|
217 | log.error('Unhandled Command: "%s" Aborting.', self.command) | |
220 | 'Unhandled Command: "%s" Aborting.', self.command) |
|
|||
221 | exit_code = -1 |
|
218 | exit_code = -1 | |
222 |
|
219 | |||
223 | return exit_code |
|
220 | return exit_code |
@@ -66,9 +66,8 b' class VcsServer(object):' | |||||
66 |
|
66 | |||
67 | def _check_permissions(self, action): |
|
67 | def _check_permissions(self, action): | |
68 | permission = self.user_permissions.get(self.repo_name) |
|
68 | permission = self.user_permissions.get(self.repo_name) | |
69 | log.debug( |
|
69 | log.debug('permission for %s on %s are: %s', | |
70 | 'permission for %s on %s are: %s', |
|
70 | self.user, self.repo_name, permission) | |
71 | self.user, self.repo_name, permission) |
|
|||
72 |
|
71 | |||
73 | if not permission: |
|
72 | if not permission: | |
74 | log.error('user `%s` permissions to repo:%s are empty. Forbidding access.', |
|
73 | log.error('user `%s` permissions to repo:%s are empty. Forbidding access.', |
@@ -68,8 +68,7 b' class GitServer(VcsServer):' | |||||
68 | self.store = store |
|
68 | self.store = store | |
69 | self.ini_path = ini_path |
|
69 | self.ini_path = ini_path | |
70 | self.repo_name = repo_name |
|
70 | self.repo_name = repo_name | |
71 | self._path = self.git_path = config.get( |
|
71 | self._path = self.git_path = config.get('app:main', 'ssh.executable.git') | |
72 | 'app:main', 'ssh.executable.git') |
|
|||
73 |
|
72 | |||
74 | self.repo_mode = repo_mode |
|
73 | self.repo_mode = repo_mode | |
75 | self.tunnel = GitTunnelWrapper(server=self) |
|
74 | self.tunnel = GitTunnelWrapper(server=self) |
@@ -95,9 +95,8 b' class SubversionTunnelWrapper(object):' | |||||
95 | signal.alarm(self.timeout) |
|
95 | signal.alarm(self.timeout) | |
96 | first_response = self._read_first_client_response() |
|
96 | first_response = self._read_first_client_response() | |
97 | signal.alarm(0) |
|
97 | signal.alarm(0) | |
98 | return ( |
|
98 | return (self._parse_first_client_response(first_response) | |
99 | self._parse_first_client_response(first_response) |
|
99 | if first_response else None) | |
100 | if first_response else None) |
|
|||
101 |
|
100 | |||
102 | def patch_first_client_response(self, response, **kwargs): |
|
101 | def patch_first_client_response(self, response, **kwargs): | |
103 | self.create_hooks_env() |
|
102 | self.create_hooks_env() | |
@@ -112,9 +111,8 b' class SubversionTunnelWrapper(object):' | |||||
112 | self.process.stdin.write(buffer_) |
|
111 | self.process.stdin.write(buffer_) | |
113 |
|
112 | |||
114 | def fail(self, message): |
|
113 | def fail(self, message): | |
115 | print( |
|
114 | print("( failure ( ( 210005 {message} 0: 0 ) ) )".format( | |
116 | "( failure ( ( 210005 {message} 0: 0 ) ) )".format( |
|
115 | message=self._svn_string(message))) | |
117 | message=self._svn_string(message))) |
|
|||
118 | self.remove_configs() |
|
116 | self.remove_configs() | |
119 | self.process.kill() |
|
117 | self.process.kill() | |
120 | return 1 |
|
118 | return 1 | |
@@ -139,6 +137,7 b' class SubversionTunnelWrapper(object):' | |||||
139 | brackets_stack.pop() |
|
137 | brackets_stack.pop() | |
140 | elif next_byte == " " and not brackets_stack: |
|
138 | elif next_byte == " " and not brackets_stack: | |
141 | break |
|
139 | break | |
|
140 | ||||
142 | return buffer_ |
|
141 | return buffer_ | |
143 |
|
142 | |||
144 | def _parse_first_client_response(self, buffer_): |
|
143 | def _parse_first_client_response(self, buffer_): | |
@@ -149,8 +148,7 b' class SubversionTunnelWrapper(object):' | |||||
149 | ( version:number ( cap:word ... ) url:string ? ra-client:string |
|
148 | ( version:number ( cap:word ... ) url:string ? ra-client:string | |
150 | ( ? client:string ) ) |
|
149 | ( ? client:string ) ) | |
151 |
|
150 | |||
152 | Please check https://svn.apache.org/repos/asf/subversion/trunk/ |
|
151 | Please check https://svn.apache.org/repos/asf/subversion/trunk/subversion/libsvn_ra_svn/protocol | |
153 | subversion/libsvn_ra_svn/protocol |
|
|||
154 | """ |
|
152 | """ | |
155 | version_re = r'(?P<version>\d+)' |
|
153 | version_re = r'(?P<version>\d+)' | |
156 | capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)' |
|
154 | capabilities_re = r'\(\s(?P<capabilities>[\w\d\-\ ]+)\s\)' | |
@@ -163,8 +161,35 b' class SubversionTunnelWrapper(object):' | |||||
163 | version=version_re, capabilities=capabilities_re, |
|
161 | version=version_re, capabilities=capabilities_re, | |
164 | url=url_re, ra_client=ra_client_re, client=client_re)) |
|
162 | url=url_re, ra_client=ra_client_re, client=client_re)) | |
165 | matcher = regex.match(buffer_) |
|
163 | matcher = regex.match(buffer_) | |
|
164 | ||||
166 | return matcher.groupdict() if matcher else None |
|
165 | return matcher.groupdict() if matcher else None | |
167 |
|
166 | |||
|
167 | def _match_repo_name(self, url): | |||
|
168 | """ | |||
|
169 | Given an server url, try to match it against ALL known repository names. | |||
|
170 | This handles a tricky SVN case for SSH and subdir commits. | |||
|
171 | E.g if our repo name is my-svn-repo, a svn commit on file in a subdir would | |||
|
172 | result in the url with this subdir added. | |||
|
173 | """ | |||
|
174 | # case 1 direct match, we don't do any "heavy" lookups | |||
|
175 | if url in self.server.user_permissions: | |||
|
176 | return url | |||
|
177 | ||||
|
178 | log.debug('Extracting repository name from subdir path %s', url) | |||
|
179 | # case 2 we check all permissions, and match closes possible case... | |||
|
180 | # NOTE(dan): In this case we only know that url has a subdir parts, it's safe | |||
|
181 | # to assume that it will have the repo name as prefix, we ensure the prefix | |||
|
182 | # for similar repositories isn't matched by adding a / | |||
|
183 | # e.g subgroup/repo-name/ and subgroup/repo-name-1/ would work correct. | |||
|
184 | for repo_name in self.server.user_permissions: | |||
|
185 | repo_name_prefix = repo_name + '/' | |||
|
186 | if url.startswith(repo_name_prefix): | |||
|
187 | log.debug('Found prefix %s match, returning proper repository name', | |||
|
188 | repo_name_prefix) | |||
|
189 | return repo_name | |||
|
190 | ||||
|
191 | return | |||
|
192 | ||||
168 | def run(self, extras): |
|
193 | def run(self, extras): | |
169 | action = 'pull' |
|
194 | action = 'pull' | |
170 | self.create_svn_config() |
|
195 | self.create_svn_config() | |
@@ -175,7 +200,8 b' class SubversionTunnelWrapper(object):' | |||||
175 | return self.fail("Repository name cannot be extracted") |
|
200 | return self.fail("Repository name cannot be extracted") | |
176 |
|
201 | |||
177 | url_parts = urlparse.urlparse(first_response['url']) |
|
202 | url_parts = urlparse.urlparse(first_response['url']) | |
178 | self.server.repo_name = url_parts.path.strip('/') |
|
203 | ||
|
204 | self.server.repo_name = self._match_repo_name(url_parts.path.strip('/')) | |||
179 |
|
205 | |||
180 | exit_code = self.server._check_permissions(action) |
|
206 | exit_code = self.server._check_permissions(action) | |
181 | if exit_code: |
|
207 | if exit_code: | |
@@ -200,10 +226,10 b' class SubversionServer(VcsServer):' | |||||
200 | .__init__(user, user_permissions, config, env) |
|
226 | .__init__(user, user_permissions, config, env) | |
201 | self.store = store |
|
227 | self.store = store | |
202 | self.ini_path = ini_path |
|
228 | self.ini_path = ini_path | |
203 | # this is set in .run() from input stream |
|
229 | # NOTE(dan): repo_name at this point is empty, | |
|
230 | # this is set later in .run() based from parsed input stream | |||
204 | self.repo_name = repo_name |
|
231 | self.repo_name = repo_name | |
205 | self._path = self.svn_path = config.get( |
|
232 | self._path = self.svn_path = config.get('app:main', 'ssh.executable.svn') | |
206 | 'app:main', 'ssh.executable.svn') |
|
|||
207 |
|
233 | |||
208 | self.tunnel = SubversionTunnelWrapper(server=self) |
|
234 | self.tunnel = SubversionTunnelWrapper(server=self) | |
209 |
|
235 |
@@ -89,6 +89,86 b' class TestSubversionServer(object):' | |||||
89 | result = server._check_permissions(action) |
|
89 | result = server._check_permissions(action) | |
90 | assert result is code |
|
90 | assert result is code | |
91 |
|
91 | |||
|
92 | @pytest.mark.parametrize('permissions, access_paths, expected_match', [ | |||
|
93 | # not matched repository name | |||
|
94 | ({ | |||
|
95 | 'test-svn': '' | |||
|
96 | }, ['test-svn-1', 'test-svn-1/subpath'], | |||
|
97 | None), | |||
|
98 | ||||
|
99 | # exact match | |||
|
100 | ({ | |||
|
101 | 'test-svn': '' | |||
|
102 | }, | |||
|
103 | ['test-svn'], | |||
|
104 | 'test-svn'), | |||
|
105 | ||||
|
106 | # subdir commits | |||
|
107 | ({ | |||
|
108 | 'test-svn': '' | |||
|
109 | }, | |||
|
110 | ['test-svn/foo', | |||
|
111 | 'test-svn/foo/test-svn', | |||
|
112 | 'test-svn/trunk/development.txt', | |||
|
113 | ], | |||
|
114 | 'test-svn'), | |||
|
115 | ||||
|
116 | # subgroups + similar patterns | |||
|
117 | ({ | |||
|
118 | 'test-svn': '', | |||
|
119 | 'test-svn-1': '', | |||
|
120 | 'test-svn-subgroup/test-svn': '', | |||
|
121 | ||||
|
122 | }, | |||
|
123 | ['test-svn-1', | |||
|
124 | 'test-svn-1/foo/test-svn', | |||
|
125 | 'test-svn-1/test-svn', | |||
|
126 | ], | |||
|
127 | 'test-svn-1'), | |||
|
128 | ||||
|
129 | # subgroups + similar patterns | |||
|
130 | ({ | |||
|
131 | 'test-svn-1': '', | |||
|
132 | 'test-svn-10': '', | |||
|
133 | 'test-svn-100': '', | |||
|
134 | }, | |||
|
135 | ['test-svn-10', | |||
|
136 | 'test-svn-10/foo/test-svn', | |||
|
137 | 'test-svn-10/test-svn', | |||
|
138 | ], | |||
|
139 | 'test-svn-10'), | |||
|
140 | ||||
|
141 | # subgroups + similar patterns | |||
|
142 | ({ | |||
|
143 | 'name': '', | |||
|
144 | 'nameContains': '', | |||
|
145 | 'nameContainsThis': '', | |||
|
146 | }, | |||
|
147 | ['nameContains', | |||
|
148 | 'nameContains/This', | |||
|
149 | 'nameContains/This/test-svn', | |||
|
150 | ], | |||
|
151 | 'nameContains'), | |||
|
152 | ||||
|
153 | # subgroups + similar patterns | |||
|
154 | ({ | |||
|
155 | 'test-svn': '', | |||
|
156 | 'test-svn-1': '', | |||
|
157 | 'test-svn-subgroup/test-svn': '', | |||
|
158 | ||||
|
159 | }, | |||
|
160 | ['test-svn-subgroup/test-svn', | |||
|
161 | 'test-svn-subgroup/test-svn/foo/test-svn', | |||
|
162 | 'test-svn-subgroup/test-svn/trunk/example.txt', | |||
|
163 | ], | |||
|
164 | 'test-svn-subgroup/test-svn'), | |||
|
165 | ]) | |||
|
166 | def test_repo_extraction_on_subdir(self, svn_server, permissions, access_paths, expected_match): | |||
|
167 | server = svn_server.create(user_permissions=permissions) | |||
|
168 | for path in access_paths: | |||
|
169 | repo_name = server.tunnel._match_repo_name(path) | |||
|
170 | assert repo_name == expected_match | |||
|
171 | ||||
92 | def test_run_returns_executes_command(self, svn_server): |
|
172 | def test_run_returns_executes_command(self, svn_server): | |
93 | server = svn_server.create() |
|
173 | server = svn_server.create() | |
94 | from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper |
|
174 | from rhodecode.apps.ssh_support.lib.backends.svn import SubversionTunnelWrapper |
General Comments 0
You need to be logged in to leave comments.
Login now