##// END OF EJS Templates
ssh: admin management of ssh keys...
Christian Oyarzun -
r7678:3e84ac8e default
parent child Browse files
Show More
@@ -0,0 +1,63 b''
1 <table class="table">
2 %if c.user_ssh_keys:
3 <tr>
4 <th>${_('Fingerprint')}</th>
5 <th>${_('Description')}</th>
6 <th>${_('Action')}</th>
7 </tr>
8 %for ssh_key in c.user_ssh_keys:
9 <tr>
10 <td>
11 ${ssh_key.fingerprint}
12 </td>
13 <td>
14 ${ssh_key.description}
15 </td>
16 <td>
17 ${h.form(url('edit_user_ssh_keys_delete', id=c.user.user_id))}
18 ${h.hidden('del_public_key', ssh_key.public_key)}
19 <button class="btn btn-danger btn-xs" type="submit"
20 onclick="return confirm('${_('Confirm to remove this SSH key: %s') % ssh_key.fingerprint}');">
21 <i class="icon-trashcan"></i>
22 ${_('Remove')}
23 </button>
24 ${h.end_form()}
25 </td>
26 </tr>
27 %endfor
28 %else:
29 <tr>
30 <td>
31 <div class="ip">${_('No SSH keys have been added')}</div>
32 </td>
33 </tr>
34 %endif
35 </table>
36
37 <div>
38 ${h.form(url('edit_user_ssh_keys', id=c.user.user_id))}
39 <div class="form">
40 <div class="form-group">
41 <label class="control-label">${_('New SSH key')}</label>
42 </div>
43 <div class="form-group">
44 <label class="control-label" for="public_key">${_('Public key')}:</label>
45 <div>
46 ${h.textarea('public_key', '', class_='form-control', placeholder=_('Public key (contents of e.g. ~/.ssh/id_rsa.pub)'), cols=80, rows=5)}
47 </div>
48 </div>
49 <div class="form-group">
50 <label class="control-label" for="description">${_('Description')}:</label>
51 <div>
52 ${h.text('description', class_='form-control', placeholder=_('Description'))}
53 </div>
54 </div>
55 <div class="form-group">
56 <div class="buttons">
57 ${h.submit('save', _('Add'), class_="btn btn-default")}
58 ${h.reset('reset', _('Reset'), class_="btn btn-default")}
59 </div>
60 </div>
61 </div>
62 ${h.end_form()}
63 </div>
@@ -1,146 +1,147 b''
1 1 List of contributors to Kallithea project:
2 2
3 3 Andrej Shadura <andrew@shadura.me> 2012 2014-2017 2019
4 4 Thomas De Schampheleire <thomas.de_schampheleire@nokia.com> 2014-2019
5 5 Étienne Gilli <etienne.gilli@gmail.com> 2015-2017 2019
6 6 Mads Kiilerich <mads@kiilerich.com> 2016-2019
7 7 Allan Nordhøy <epost@anotheragency.no> 2017-2019
8 8 ssantos <ssantos@web.de> 2018-2019
9 9 Danni Randeris <danniranderis@gmail.com> 2019
10 10 Edmund Wong <ewong@crazy-cat.org> 2019
11 11 Manuel Jacob <me@manueljacob.de> 2019
12 12 Wolfgang Scherer <wolfgang.scherer@gmx.de> 2019
13 13 Dominik Ruf <dominikruf@gmail.com> 2012 2014-2018
14 14 Michal Čihař <michal@cihar.com> 2014-2015 2018
15 15 Branko Majic <branko@majic.rs> 2015 2018
16 16 Chris Rule <crule@aegistg.com> 2018
17 17 Jesús Sánchez <jsanchezfdz95@gmail.com> 2018
18 18 Patrick Vane <patrick_vane@lowentry.com> 2018
19 19 Pheng Heong Tan <phtan90@gmail.com> 2018
20 20 Максим Якимчук <xpinovo@gmail.com> 2018
21 21 Марс Ямбар <mjambarmeta@gmail.com> 2018
22 22 Mads Kiilerich <madski@unity3d.com> 2012-2017
23 23 Unity Technologies 2012-2017
24 24 Søren Løvborg <sorenl@unity3d.com> 2015-2017
25 25 Sam Jaques <sam.jaques@me.com> 2015 2017
26 26 Asterios Dimitriou <steve@pci.gr> 2016-2017
27 27 Alessandro Molina <alessandro.molina@axant.it> 2017
28 28 Anton Schur <tonich.sh@gmail.com> 2017
29 29 Ching-Chen Mao <mao@lins.fju.edu.tw> 2017
30 30 Eivind Tagseth <eivindt@gmail.com> 2017
31 31 FUJIWARA Katsunori <foozy@lares.dti.ne.jp> 2017
32 32 Holger Schramm <info@schramm.by> 2017
33 33 Karl Goetz <karl@kgoetz.id.au> 2017
34 34 Lars Kruse <devel@sumpfralle.de> 2017
35 35 Marko Semet <markosemet@googlemail.com> 2017
36 36 Viktar Vauchkevich <victorenator@gmail.com> 2017
37 37 Takumi IINO <trot.thunder@gmail.com> 2012-2016
38 38 Jan Heylen <heyleke@gmail.com> 2015-2016
39 39 Robert Martinez <ntttq@inboxen.org> 2015-2016
40 40 Robert Rauch <mail@robertrauch.de> 2015-2016
41 41 Angel Ezquerra <angel.ezquerra@gmail.com> 2016
42 42 Anton Shestakov <av6@dwimlabs.net> 2016
43 43 Brandon Jones <bjones14@gmail.com> 2016
44 44 Kateryna Musina <kateryna@unity3d.com> 2016
45 45 Konstantin Veretennicov <kveretennicov@gmail.com> 2016
46 46 Oscar Curero <oscar@naiandei.net> 2016
47 47 Robert James Dennington <tinytimrob@googlemail.com> 2016
48 48 timeless@gmail.com 2016
49 49 YFdyh000 <yfdyh000@gmail.com> 2016
50 50 Aras Pranckevičius <aras@unity3d.com> 2012-2013 2015
51 51 Sean Farley <sean.michael.farley@gmail.com> 2013-2015
52 52 Christian Oyarzun <oyarzun@gmail.com> 2014-2015
53 53 Joseph Rivera <rivera.d.joseph@gmail.com> 2014-2015
54 54 Anatoly Bubenkov <bubenkoff@gmail.com> 2015
55 55 Andrew Bartlett <abartlet@catalyst.net.nz> 2015
56 56 Balázs Úr <urbalazs@gmail.com> 2015
57 57 Ben Finney <ben@benfinney.id.au> 2015
58 58 Daniel Hobley <danielh@unity3d.com> 2015
59 59 David Avigni <david.avigni@ankapi.com> 2015
60 60 Denis Blanchette <dblanchette@coveo.com> 2015
61 61 duanhongyi <duanhongyi@doopai.com> 2015
62 62 EriCSN Chang <ericsning@gmail.com> 2015
63 63 Grzegorz Krason <grzegorz.krason@gmail.com> 2015
64 64 Jiří Suchan <yed@vanyli.net> 2015
65 65 Kazunari Kobayashi <kobanari@nifty.com> 2015
66 66 Kevin Bullock <kbullock@ringworld.org> 2015
67 67 kobanari <kobanari@nifty.com> 2015
68 68 Marc Abramowitz <marc@marc-abramowitz.com> 2015
69 69 Marc Villetard <marc.villetard@gmail.com> 2015
70 70 Matthias Zilk <matthias.zilk@gmail.com> 2015
71 71 Michael Pohl <michael@mipapo.de> 2015
72 72 Michael V. DePalatis <mike@depalatis.net> 2015
73 73 Morten Skaaning <mortens@unity3d.com> 2015
74 74 Nick High <nick@silverchip.org> 2015
75 75 Niemand Jedermann <predatorix@web.de> 2015
76 76 Peter Vitt <petervitt@web.de> 2015
77 77 Ronny Pfannschmidt <opensource@ronnypfannschmidt.de> 2015
78 78 Tuux <tuxa@galaxie.eu.org> 2015
79 79 Viktar Palstsiuk <vipals@gmail.com> 2015
80 80 Ante Ilic <ante@unity3d.com> 2014
81 81 Bradley M. Kuhn <bkuhn@sfconservancy.org> 2014
82 82 Calinou <calinou@opmbx.org> 2014
83 83 Daniel Anderson <daniel@dattrix.com> 2014
84 84 Henrik Stuart <hg@hstuart.dk> 2014
85 85 Ingo von Borstel <kallithea@planetmaker.de> 2014
86 86 Jelmer Vernooij <jelmer@samba.org> 2014
87 87 Jim Hague <jim.hague@acm.org> 2014
88 88 Matt Fellows <kallithea@matt-fellows.me.uk> 2014
89 89 Max Roman <max@choloclos.se> 2014
90 90 Na'Tosha Bard <natosha@unity3d.com> 2014
91 91 Rasmus Selsmark <rasmuss@unity3d.com> 2014
92 92 Tim Freund <tim@freunds.net> 2014
93 93 Travis Burtrum <android@moparisthebest.com> 2014
94 94 Zoltan Gyarmati <mr.zoltan.gyarmati@gmail.com> 2014
95 95 Marcin Kuźmiński <marcin@python-works.com> 2010-2013
96 96 xpol <xpolife@gmail.com> 2012-2013
97 97 Aparkar <aparkar@icloud.com> 2013
98 98 Dennis Brakhane <brakhane@googlemail.com> 2013
99 99 Grzegorz Rożniecki <xaerxess@gmail.com> 2013
100 Ilya Beda <ir4y.ix@gmail.com> 2013
100 101 Jonathan Sternberg <jonathansternberg@gmail.com> 2013
101 102 Leonardo Carneiro <leonardo@unity3d.com> 2013
102 103 Magnus Ericmats <magnus.ericmats@gmail.com> 2013
103 104 Martin Vium <martinv@unity3d.com> 2013
104 105 Simon Lopez <simon.lopez@slopez.org> 2013
105 106 Ton Plomp <tcplomp@gmail.com> 2013
106 107 Augusto Herrmann <augusto.herrmann@planejamento.gov.br> 2011-2012
107 108 Dan Sheridan <djs@adelard.com> 2012
108 109 Dies Koper <diesk@fast.au.fujitsu.com> 2012
109 110 Erwin Kroon <e.kroon@smartmetersolutions.nl> 2012
110 111 H Waldo G <gwaldo@gmail.com> 2012
111 112 hppj <hppj@postmage.biz> 2012
112 113 Indra Talip <indra.talip@gmail.com> 2012
113 114 mikespook 2012
114 115 nansenat16 <nansenat16@null.tw> 2012
115 116 Philip Jameson <philip.j@hostdime.com> 2012
116 117 Raoul Thill <raoul.thill@gmail.com> 2012
117 118 Stefan Engel <mail@engel-stefan.de> 2012
118 119 Tony Bussieres <t.bussieres@gmail.com> 2012
119 120 Vincent Caron <vcaron@bearstech.com> 2012
120 121 Vincent Duvert <vincent@duvert.net> 2012
121 122 Vladislav Poluhin <nuklea@gmail.com> 2012
122 123 Zachary Auclair <zach101@gmail.com> 2012
123 124 Ankit Solanki <ankit.solanki@gmail.com> 2011
124 125 Dmitri Kuznetsov 2011
125 126 Jared Bunting <jared.bunting@peachjean.com> 2011
126 127 Jason Harris <jason@jasonfharris.com> 2011
127 128 Les Peabody <lpeabody@gmail.com> 2011
128 129 Liad Shani <liadff@gmail.com> 2011
129 130 Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it> 2011
130 131 Matt Zuba <matt.zuba@goodwillaz.org> 2011
131 132 Nicolas VINOT <aeris@imirhil.fr> 2011
132 133 Shawn K. O'Shea <shawn@eth0.net> 2011
133 134 Thayne Harbaugh <thayne@fusionio.com> 2011
134 135 Łukasz Balcerzak <lukaszbalcerzak@gmail.com> 2010
135 136 Andrew Kesterson <andrew@aklabs.net>
136 137 cejones
137 138 David A. Sjøen <david.sjoen@westcon.no>
138 139 James Rhodes <jrhodes@redpointsoftware.com.au>
139 140 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
140 141 larikale
141 142 RhodeCode GmbH
142 143 Sebastian Kreutzberger <sebastian@rhodecode.com>
143 144 Steve Romanow <slestak989@gmail.com>
144 145 SteveCohen
145 146 Thomas <thomas@rhodecode.com>
146 147 Thomas Waldmann <tw-public@gmx.de>
@@ -1,778 +1,785 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 Routes configuration
16 16
17 17 The more specific and detailed routes should be defined first so they
18 18 may take precedent over the more generic routes. For more information
19 19 refer to the routes manual at http://routes.groovie.org/docs/
20 20 """
21 21
22 22 from tg import request
23 23 from routes import Mapper
24 24
25 25 # prefix for non repository related links needs to be prefixed with `/`
26 26 ADMIN_PREFIX = '/_admin'
27 27
28 28
29 29 def make_map(config):
30 30 """Create, configure and return the routes Mapper"""
31 31 rmap = Mapper(directory=config['paths']['controllers'],
32 32 always_scan=config['debug'])
33 33 rmap.minimization = False
34 34 rmap.explicit = False
35 35
36 36 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
37 37
38 38 def check_repo(environ, match_dict):
39 39 """
40 40 Check for valid repository for proper 404 handling.
41 41 Also, a bit of side effect modifying match_dict ...
42 42 """
43 43 if match_dict.get('f_path'):
44 44 # fix for multiple initial slashes that causes errors
45 45 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
46 46
47 47 return is_valid_repo(match_dict['repo_name'], config['base_path'])
48 48
49 49 def check_group(environ, match_dict):
50 50 """
51 51 check for valid repository group for proper 404 handling
52 52
53 53 :param environ:
54 54 :param match_dict:
55 55 """
56 56 repo_group_name = match_dict.get('group_name')
57 57 return is_valid_repo_group(repo_group_name, config['base_path'])
58 58
59 59 def check_group_skip_path(environ, match_dict):
60 60 """
61 61 check for valid repository group for proper 404 handling, but skips
62 62 verification of existing path
63 63
64 64 :param environ:
65 65 :param match_dict:
66 66 """
67 67 repo_group_name = match_dict.get('group_name')
68 68 return is_valid_repo_group(repo_group_name, config['base_path'],
69 69 skip_path_check=True)
70 70
71 71 def check_user_group(environ, match_dict):
72 72 """
73 73 check for valid user group for proper 404 handling
74 74
75 75 :param environ:
76 76 :param match_dict:
77 77 """
78 78 return True
79 79
80 80 def check_int(environ, match_dict):
81 81 return match_dict.get('id').isdigit()
82 82
83 83 #==========================================================================
84 84 # CUSTOM ROUTES HERE
85 85 #==========================================================================
86 86
87 87 # MAIN PAGE
88 88 rmap.connect('home', '/', controller='home', action='index')
89 89 rmap.connect('about', '/about', controller='home', action='about')
90 90 rmap.redirect('/favicon.ico', '/images/favicon.ico')
91 91 rmap.connect('repo_switcher_data', '/_repos', controller='home',
92 92 action='repo_switcher_data')
93 93 rmap.connect('users_and_groups_data', '/_users_and_groups', controller='home',
94 94 action='users_and_groups_data')
95 95
96 96 rmap.connect('rst_help',
97 97 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
98 98 _static=True)
99 99 rmap.connect('kallithea_project_url', "https://kallithea-scm.org/", _static=True)
100 100 rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True)
101 101
102 102 # ADMIN REPOSITORY ROUTES
103 103 with rmap.submapper(path_prefix=ADMIN_PREFIX,
104 104 controller='admin/repos') as m:
105 105 m.connect("repos", "/repos",
106 106 action="create", conditions=dict(method=["POST"]))
107 107 m.connect("repos", "/repos",
108 108 action="index", conditions=dict(method=["GET"]))
109 109 m.connect("new_repo", "/create_repository",
110 110 action="create_repository", conditions=dict(method=["GET"]))
111 111 m.connect("update_repo", "/repos/{repo_name:.*?}",
112 112 action="update", conditions=dict(method=["POST"],
113 113 function=check_repo))
114 114 m.connect("delete_repo", "/repos/{repo_name:.*?}/delete",
115 115 action="delete", conditions=dict(method=["POST"]))
116 116
117 117 # ADMIN REPOSITORY GROUPS ROUTES
118 118 with rmap.submapper(path_prefix=ADMIN_PREFIX,
119 119 controller='admin/repo_groups') as m:
120 120 m.connect("repos_groups", "/repo_groups",
121 121 action="create", conditions=dict(method=["POST"]))
122 122 m.connect("repos_groups", "/repo_groups",
123 123 action="index", conditions=dict(method=["GET"]))
124 124 m.connect("new_repos_group", "/repo_groups/new",
125 125 action="new", conditions=dict(method=["GET"]))
126 126 m.connect("update_repos_group", "/repo_groups/{group_name:.*?}",
127 127 action="update", conditions=dict(method=["POST"],
128 128 function=check_group))
129 129
130 130 m.connect("repos_group", "/repo_groups/{group_name:.*?}",
131 131 action="show", conditions=dict(method=["GET"],
132 132 function=check_group))
133 133
134 134 # EXTRAS REPO GROUP ROUTES
135 135 m.connect("edit_repo_group", "/repo_groups/{group_name:.*?}/edit",
136 136 action="edit",
137 137 conditions=dict(method=["GET"], function=check_group))
138 138
139 139 m.connect("edit_repo_group_advanced", "/repo_groups/{group_name:.*?}/edit/advanced",
140 140 action="edit_repo_group_advanced",
141 141 conditions=dict(method=["GET"], function=check_group))
142 142
143 143 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
144 144 action="edit_repo_group_perms",
145 145 conditions=dict(method=["GET"], function=check_group))
146 146 m.connect("edit_repo_group_perms_update", "/repo_groups/{group_name:.*?}/edit/permissions",
147 147 action="update_perms",
148 148 conditions=dict(method=["POST"], function=check_group))
149 149 m.connect("edit_repo_group_perms_delete", "/repo_groups/{group_name:.*?}/edit/permissions/delete",
150 150 action="delete_perms",
151 151 conditions=dict(method=["POST"], function=check_group))
152 152
153 153 m.connect("delete_repo_group", "/repo_groups/{group_name:.*?}/delete",
154 154 action="delete", conditions=dict(method=["POST"],
155 155 function=check_group_skip_path))
156 156
157 157 # ADMIN USER ROUTES
158 158 with rmap.submapper(path_prefix=ADMIN_PREFIX,
159 159 controller='admin/users') as m:
160 160 m.connect("new_user", "/users/new",
161 161 action="create", conditions=dict(method=["POST"]))
162 162 m.connect("users", "/users",
163 163 action="index", conditions=dict(method=["GET"]))
164 164 m.connect("formatted_users", "/users.{format}",
165 165 action="index", conditions=dict(method=["GET"]))
166 166 m.connect("new_user", "/users/new",
167 167 action="new", conditions=dict(method=["GET"]))
168 168 m.connect("update_user", "/users/{id}",
169 169 action="update", conditions=dict(method=["POST"]))
170 170 m.connect("delete_user", "/users/{id}/delete",
171 171 action="delete", conditions=dict(method=["POST"]))
172 172 m.connect("edit_user", "/users/{id}/edit",
173 173 action="edit", conditions=dict(method=["GET"]))
174 174
175 175 # EXTRAS USER ROUTES
176 176 m.connect("edit_user_advanced", "/users/{id}/edit/advanced",
177 177 action="edit_advanced", conditions=dict(method=["GET"]))
178 178
179 179 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
180 180 action="edit_api_keys", conditions=dict(method=["GET"]))
181 181 m.connect("edit_user_api_keys_update", "/users/{id}/edit/api_keys",
182 182 action="add_api_key", conditions=dict(method=["POST"]))
183 183 m.connect("edit_user_api_keys_delete", "/users/{id}/edit/api_keys/delete",
184 184 action="delete_api_key", conditions=dict(method=["POST"]))
185 185
186 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
187 action="edit_ssh_keys", conditions=dict(method=["GET"]))
188 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
189 action="ssh_keys_add", conditions=dict(method=["POST"]))
190 m.connect("edit_user_ssh_keys_delete", "/users/{id}/edit/ssh_keys/delete",
191 action="ssh_keys_delete", conditions=dict(method=["POST"]))
192
186 193 m.connect("edit_user_perms", "/users/{id}/edit/permissions",
187 194 action="edit_perms", conditions=dict(method=["GET"]))
188 195 m.connect("edit_user_perms_update", "/users/{id}/edit/permissions",
189 196 action="update_perms", conditions=dict(method=["POST"]))
190 197
191 198 m.connect("edit_user_emails", "/users/{id}/edit/emails",
192 199 action="edit_emails", conditions=dict(method=["GET"]))
193 200 m.connect("edit_user_emails_update", "/users/{id}/edit/emails",
194 201 action="add_email", conditions=dict(method=["POST"]))
195 202 m.connect("edit_user_emails_delete", "/users/{id}/edit/emails/delete",
196 203 action="delete_email", conditions=dict(method=["POST"]))
197 204
198 205 m.connect("edit_user_ips", "/users/{id}/edit/ips",
199 206 action="edit_ips", conditions=dict(method=["GET"]))
200 207 m.connect("edit_user_ips_update", "/users/{id}/edit/ips",
201 208 action="add_ip", conditions=dict(method=["POST"]))
202 209 m.connect("edit_user_ips_delete", "/users/{id}/edit/ips/delete",
203 210 action="delete_ip", conditions=dict(method=["POST"]))
204 211
205 212 # ADMIN USER GROUPS REST ROUTES
206 213 with rmap.submapper(path_prefix=ADMIN_PREFIX,
207 214 controller='admin/user_groups') as m:
208 215 m.connect("users_groups", "/user_groups",
209 216 action="create", conditions=dict(method=["POST"]))
210 217 m.connect("users_groups", "/user_groups",
211 218 action="index", conditions=dict(method=["GET"]))
212 219 m.connect("new_users_group", "/user_groups/new",
213 220 action="new", conditions=dict(method=["GET"]))
214 221 m.connect("update_users_group", "/user_groups/{id}",
215 222 action="update", conditions=dict(method=["POST"]))
216 223 m.connect("delete_users_group", "/user_groups/{id}/delete",
217 224 action="delete", conditions=dict(method=["POST"]))
218 225 m.connect("edit_users_group", "/user_groups/{id}/edit",
219 226 action="edit", conditions=dict(method=["GET"]),
220 227 function=check_user_group)
221 228
222 229 # EXTRAS USER GROUP ROUTES
223 230 m.connect("edit_user_group_default_perms", "/user_groups/{id}/edit/default_perms",
224 231 action="edit_default_perms", conditions=dict(method=["GET"]))
225 232 m.connect("edit_user_group_default_perms_update", "/user_groups/{id}/edit/default_perms",
226 233 action="update_default_perms", conditions=dict(method=["POST"]))
227 234
228 235 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
229 236 action="edit_perms", conditions=dict(method=["GET"]))
230 237 m.connect("edit_user_group_perms_update", "/user_groups/{id}/edit/perms",
231 238 action="update_perms", conditions=dict(method=["POST"]))
232 239 m.connect("edit_user_group_perms_delete", "/user_groups/{id}/edit/perms/delete",
233 240 action="delete_perms", conditions=dict(method=["POST"]))
234 241
235 242 m.connect("edit_user_group_advanced", "/user_groups/{id}/edit/advanced",
236 243 action="edit_advanced", conditions=dict(method=["GET"]))
237 244
238 245 m.connect("edit_user_group_members", "/user_groups/{id}/edit/members",
239 246 action="edit_members", conditions=dict(method=["GET"]))
240 247
241 248 # ADMIN PERMISSIONS ROUTES
242 249 with rmap.submapper(path_prefix=ADMIN_PREFIX,
243 250 controller='admin/permissions') as m:
244 251 m.connect("admin_permissions", "/permissions",
245 252 action="permission_globals", conditions=dict(method=["POST"]))
246 253 m.connect("admin_permissions", "/permissions",
247 254 action="permission_globals", conditions=dict(method=["GET"]))
248 255
249 256 m.connect("admin_permissions_ips", "/permissions/ips",
250 257 action="permission_ips", conditions=dict(method=["GET"]))
251 258
252 259 m.connect("admin_permissions_perms", "/permissions/perms",
253 260 action="permission_perms", conditions=dict(method=["GET"]))
254 261
255 262 # ADMIN DEFAULTS ROUTES
256 263 with rmap.submapper(path_prefix=ADMIN_PREFIX,
257 264 controller='admin/defaults') as m:
258 265 m.connect('defaults', 'defaults',
259 266 action="index")
260 267 m.connect('defaults_update', 'defaults/{id}/update',
261 268 action="update", conditions=dict(method=["POST"]))
262 269
263 270 # ADMIN AUTH SETTINGS
264 271 rmap.connect('auth_settings', '%s/auth' % ADMIN_PREFIX,
265 272 controller='admin/auth_settings', action='auth_settings',
266 273 conditions=dict(method=["POST"]))
267 274 rmap.connect('auth_home', '%s/auth' % ADMIN_PREFIX,
268 275 controller='admin/auth_settings')
269 276
270 277 # ADMIN SETTINGS ROUTES
271 278 with rmap.submapper(path_prefix=ADMIN_PREFIX,
272 279 controller='admin/settings') as m:
273 280 m.connect("admin_settings", "/settings",
274 281 action="settings_vcs", conditions=dict(method=["POST"]))
275 282 m.connect("admin_settings", "/settings",
276 283 action="settings_vcs", conditions=dict(method=["GET"]))
277 284
278 285 m.connect("admin_settings_mapping", "/settings/mapping",
279 286 action="settings_mapping", conditions=dict(method=["POST"]))
280 287 m.connect("admin_settings_mapping", "/settings/mapping",
281 288 action="settings_mapping", conditions=dict(method=["GET"]))
282 289
283 290 m.connect("admin_settings_global", "/settings/global",
284 291 action="settings_global", conditions=dict(method=["POST"]))
285 292 m.connect("admin_settings_global", "/settings/global",
286 293 action="settings_global", conditions=dict(method=["GET"]))
287 294
288 295 m.connect("admin_settings_visual", "/settings/visual",
289 296 action="settings_visual", conditions=dict(method=["POST"]))
290 297 m.connect("admin_settings_visual", "/settings/visual",
291 298 action="settings_visual", conditions=dict(method=["GET"]))
292 299
293 300 m.connect("admin_settings_email", "/settings/email",
294 301 action="settings_email", conditions=dict(method=["POST"]))
295 302 m.connect("admin_settings_email", "/settings/email",
296 303 action="settings_email", conditions=dict(method=["GET"]))
297 304
298 305 m.connect("admin_settings_hooks", "/settings/hooks",
299 306 action="settings_hooks", conditions=dict(method=["POST"]))
300 307 m.connect("admin_settings_hooks_delete", "/settings/hooks/delete",
301 308 action="settings_hooks", conditions=dict(method=["POST"]))
302 309 m.connect("admin_settings_hooks", "/settings/hooks",
303 310 action="settings_hooks", conditions=dict(method=["GET"]))
304 311
305 312 m.connect("admin_settings_search", "/settings/search",
306 313 action="settings_search", conditions=dict(method=["POST"]))
307 314 m.connect("admin_settings_search", "/settings/search",
308 315 action="settings_search", conditions=dict(method=["GET"]))
309 316
310 317 m.connect("admin_settings_system", "/settings/system",
311 318 action="settings_system", conditions=dict(method=["POST"]))
312 319 m.connect("admin_settings_system", "/settings/system",
313 320 action="settings_system", conditions=dict(method=["GET"]))
314 321 m.connect("admin_settings_system_update", "/settings/system/updates",
315 322 action="settings_system_update", conditions=dict(method=["GET"]))
316 323
317 324 # ADMIN MY ACCOUNT
318 325 with rmap.submapper(path_prefix=ADMIN_PREFIX,
319 326 controller='admin/my_account') as m:
320 327
321 328 m.connect("my_account", "/my_account",
322 329 action="my_account", conditions=dict(method=["GET"]))
323 330 m.connect("my_account", "/my_account",
324 331 action="my_account", conditions=dict(method=["POST"]))
325 332
326 333 m.connect("my_account_password", "/my_account/password",
327 334 action="my_account_password", conditions=dict(method=["GET"]))
328 335 m.connect("my_account_password", "/my_account/password",
329 336 action="my_account_password", conditions=dict(method=["POST"]))
330 337
331 338 m.connect("my_account_repos", "/my_account/repos",
332 339 action="my_account_repos", conditions=dict(method=["GET"]))
333 340
334 341 m.connect("my_account_watched", "/my_account/watched",
335 342 action="my_account_watched", conditions=dict(method=["GET"]))
336 343
337 344 m.connect("my_account_perms", "/my_account/perms",
338 345 action="my_account_perms", conditions=dict(method=["GET"]))
339 346
340 347 m.connect("my_account_emails", "/my_account/emails",
341 348 action="my_account_emails", conditions=dict(method=["GET"]))
342 349 m.connect("my_account_emails", "/my_account/emails",
343 350 action="my_account_emails_add", conditions=dict(method=["POST"]))
344 351 m.connect("my_account_emails_delete", "/my_account/emails/delete",
345 352 action="my_account_emails_delete", conditions=dict(method=["POST"]))
346 353
347 354 m.connect("my_account_api_keys", "/my_account/api_keys",
348 355 action="my_account_api_keys", conditions=dict(method=["GET"]))
349 356 m.connect("my_account_api_keys", "/my_account/api_keys",
350 357 action="my_account_api_keys_add", conditions=dict(method=["POST"]))
351 358 m.connect("my_account_api_keys_delete", "/my_account/api_keys/delete",
352 359 action="my_account_api_keys_delete", conditions=dict(method=["POST"]))
353 360
354 361 # ADMIN GIST
355 362 with rmap.submapper(path_prefix=ADMIN_PREFIX,
356 363 controller='admin/gists') as m:
357 364 m.connect("gists", "/gists",
358 365 action="create", conditions=dict(method=["POST"]))
359 366 m.connect("gists", "/gists",
360 367 action="index", conditions=dict(method=["GET"]))
361 368 m.connect("new_gist", "/gists/new",
362 369 action="new", conditions=dict(method=["GET"]))
363 370
364 371 m.connect("gist_delete", "/gists/{gist_id}/delete",
365 372 action="delete", conditions=dict(method=["POST"]))
366 373 m.connect("edit_gist", "/gists/{gist_id}/edit",
367 374 action="edit", conditions=dict(method=["GET", "POST"]))
368 375 m.connect("edit_gist_check_revision", "/gists/{gist_id}/edit/check_revision",
369 376 action="check_revision", conditions=dict(method=["POST"]))
370 377
371 378 m.connect("gist", "/gists/{gist_id}",
372 379 action="show", conditions=dict(method=["GET"]))
373 380 m.connect("gist_rev", "/gists/{gist_id}/{revision}",
374 381 revision="tip",
375 382 action="show", conditions=dict(method=["GET"]))
376 383 m.connect("formatted_gist", "/gists/{gist_id}/{revision}/{format}",
377 384 revision="tip",
378 385 action="show", conditions=dict(method=["GET"]))
379 386 m.connect("formatted_gist_file", "/gists/{gist_id}/{revision}/{format}/{f_path:.*}",
380 387 revision='tip',
381 388 action="show", conditions=dict(method=["GET"]))
382 389
383 390 # ADMIN MAIN PAGES
384 391 with rmap.submapper(path_prefix=ADMIN_PREFIX,
385 392 controller='admin/admin') as m:
386 393 m.connect('admin_home', '', action='index')
387 394 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
388 395 action='add_repo')
389 396 #==========================================================================
390 397 # API V2
391 398 #==========================================================================
392 399 with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='api/api',
393 400 action='_dispatch') as m:
394 401 m.connect('api', '/api')
395 402
396 403 # USER JOURNAL
397 404 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
398 405 controller='journal', action='index')
399 406 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
400 407 controller='journal', action='journal_rss')
401 408 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
402 409 controller='journal', action='journal_atom')
403 410
404 411 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
405 412 controller='journal', action="public_journal")
406 413
407 414 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
408 415 controller='journal', action="public_journal_rss")
409 416
410 417 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
411 418 controller='journal', action="public_journal_rss")
412 419
413 420 rmap.connect('public_journal_atom',
414 421 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
415 422 action="public_journal_atom")
416 423
417 424 rmap.connect('public_journal_atom_old',
418 425 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
419 426 action="public_journal_atom")
420 427
421 428 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
422 429 controller='journal', action='toggle_following',
423 430 conditions=dict(method=["POST"]))
424 431
425 432 # SEARCH
426 433 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
427 434 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
428 435 controller='search',
429 436 conditions=dict(function=check_repo))
430 437 rmap.connect('search_repo', '/{repo_name:.*?}/search',
431 438 controller='search',
432 439 conditions=dict(function=check_repo),
433 440 )
434 441
435 442 # LOGIN/LOGOUT/REGISTER/SIGN IN
436 443 rmap.connect('authentication_token', '%s/authentication_token' % ADMIN_PREFIX, controller='login', action='authentication_token')
437 444 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
438 445 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
439 446 action='logout')
440 447
441 448 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
442 449 action='register')
443 450
444 451 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
445 452 controller='login', action='password_reset')
446 453
447 454 rmap.connect('reset_password_confirmation',
448 455 '%s/password_reset_confirmation' % ADMIN_PREFIX,
449 456 controller='login', action='password_reset_confirmation')
450 457
451 458 # FEEDS
452 459 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
453 460 controller='feed', action='rss',
454 461 conditions=dict(function=check_repo))
455 462
456 463 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
457 464 controller='feed', action='atom',
458 465 conditions=dict(function=check_repo))
459 466
460 467 #==========================================================================
461 468 # REPOSITORY ROUTES
462 469 #==========================================================================
463 470 rmap.connect('repo_creating_home', '/{repo_name:.*?}/repo_creating',
464 471 controller='admin/repos', action='repo_creating')
465 472 rmap.connect('repo_check_home', '/{repo_name:.*?}/crepo_check',
466 473 controller='admin/repos', action='repo_check')
467 474
468 475 rmap.connect('summary_home', '/{repo_name:.*?}',
469 476 controller='summary',
470 477 conditions=dict(function=check_repo))
471 478
472 479 # must be here for proper group/repo catching
473 480 rmap.connect('repos_group_home', '/{group_name:.*}',
474 481 controller='admin/repo_groups', action="show_by_name",
475 482 conditions=dict(function=check_group))
476 483 rmap.connect('repo_stats_home', '/{repo_name:.*?}/statistics',
477 484 controller='summary', action='statistics',
478 485 conditions=dict(function=check_repo))
479 486
480 487 rmap.connect('repo_size', '/{repo_name:.*?}/repo_size',
481 488 controller='summary', action='repo_size',
482 489 conditions=dict(function=check_repo))
483 490
484 491 rmap.connect('repo_refs_data', '/{repo_name:.*?}/refs-data',
485 492 controller='home', action='repo_refs_data')
486 493
487 494 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision:.*}',
488 495 controller='changeset', revision='tip',
489 496 conditions=dict(function=check_repo))
490 497 rmap.connect('changeset_children', '/{repo_name:.*?}/changeset_children/{revision}',
491 498 controller='changeset', revision='tip', action="changeset_children",
492 499 conditions=dict(function=check_repo))
493 500 rmap.connect('changeset_parents', '/{repo_name:.*?}/changeset_parents/{revision}',
494 501 controller='changeset', revision='tip', action="changeset_parents",
495 502 conditions=dict(function=check_repo))
496 503
497 504 # repo edit options
498 505 rmap.connect("edit_repo", "/{repo_name:.*?}/settings",
499 506 controller='admin/repos', action="edit",
500 507 conditions=dict(method=["GET"], function=check_repo))
501 508
502 509 rmap.connect("edit_repo_perms", "/{repo_name:.*?}/settings/permissions",
503 510 controller='admin/repos', action="edit_permissions",
504 511 conditions=dict(method=["GET"], function=check_repo))
505 512 rmap.connect("edit_repo_perms_update", "/{repo_name:.*?}/settings/permissions",
506 513 controller='admin/repos', action="edit_permissions_update",
507 514 conditions=dict(method=["POST"], function=check_repo))
508 515 rmap.connect("edit_repo_perms_revoke", "/{repo_name:.*?}/settings/permissions/delete",
509 516 controller='admin/repos', action="edit_permissions_revoke",
510 517 conditions=dict(method=["POST"], function=check_repo))
511 518
512 519 rmap.connect("edit_repo_fields", "/{repo_name:.*?}/settings/fields",
513 520 controller='admin/repos', action="edit_fields",
514 521 conditions=dict(method=["GET"], function=check_repo))
515 522 rmap.connect('create_repo_fields', "/{repo_name:.*?}/settings/fields/new",
516 523 controller='admin/repos', action="create_repo_field",
517 524 conditions=dict(method=["POST"], function=check_repo))
518 525 rmap.connect('delete_repo_fields', "/{repo_name:.*?}/settings/fields/{field_id}/delete",
519 526 controller='admin/repos', action="delete_repo_field",
520 527 conditions=dict(method=["POST"], function=check_repo))
521 528
522 529 rmap.connect("edit_repo_advanced", "/{repo_name:.*?}/settings/advanced",
523 530 controller='admin/repos', action="edit_advanced",
524 531 conditions=dict(method=["GET"], function=check_repo))
525 532
526 533 rmap.connect("edit_repo_advanced_journal", "/{repo_name:.*?}/settings/advanced/journal",
527 534 controller='admin/repos', action="edit_advanced_journal",
528 535 conditions=dict(method=["POST"], function=check_repo))
529 536
530 537 rmap.connect("edit_repo_advanced_fork", "/{repo_name:.*?}/settings/advanced/fork",
531 538 controller='admin/repos', action="edit_advanced_fork",
532 539 conditions=dict(method=["POST"], function=check_repo))
533 540
534 541 rmap.connect("edit_repo_caches", "/{repo_name:.*?}/settings/caches",
535 542 controller='admin/repos', action="edit_caches",
536 543 conditions=dict(method=["GET"], function=check_repo))
537 544 rmap.connect("update_repo_caches", "/{repo_name:.*?}/settings/caches",
538 545 controller='admin/repos', action="edit_caches",
539 546 conditions=dict(method=["POST"], function=check_repo))
540 547
541 548 rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
542 549 controller='admin/repos', action="edit_remote",
543 550 conditions=dict(method=["GET"], function=check_repo))
544 551 rmap.connect("edit_repo_remote_update", "/{repo_name:.*?}/settings/remote",
545 552 controller='admin/repos', action="edit_remote",
546 553 conditions=dict(method=["POST"], function=check_repo))
547 554
548 555 rmap.connect("edit_repo_statistics", "/{repo_name:.*?}/settings/statistics",
549 556 controller='admin/repos', action="edit_statistics",
550 557 conditions=dict(method=["GET"], function=check_repo))
551 558 rmap.connect("edit_repo_statistics_update", "/{repo_name:.*?}/settings/statistics",
552 559 controller='admin/repos', action="edit_statistics",
553 560 conditions=dict(method=["POST"], function=check_repo))
554 561
555 562 # still working url for backward compat.
556 563 rmap.connect('raw_changeset_home_depraced',
557 564 '/{repo_name:.*?}/raw-changeset/{revision}',
558 565 controller='changeset', action='changeset_raw',
559 566 revision='tip', conditions=dict(function=check_repo))
560 567
561 568 ## new URLs
562 569 rmap.connect('changeset_raw_home',
563 570 '/{repo_name:.*?}/changeset-diff/{revision}',
564 571 controller='changeset', action='changeset_raw',
565 572 revision='tip', conditions=dict(function=check_repo))
566 573
567 574 rmap.connect('changeset_patch_home',
568 575 '/{repo_name:.*?}/changeset-patch/{revision}',
569 576 controller='changeset', action='changeset_patch',
570 577 revision='tip', conditions=dict(function=check_repo))
571 578
572 579 rmap.connect('changeset_download_home',
573 580 '/{repo_name:.*?}/changeset-download/{revision}',
574 581 controller='changeset', action='changeset_download',
575 582 revision='tip', conditions=dict(function=check_repo))
576 583
577 584 rmap.connect('changeset_comment',
578 585 '/{repo_name:.*?}/changeset-comment/{revision}',
579 586 controller='changeset', revision='tip', action='comment',
580 587 conditions=dict(function=check_repo))
581 588
582 589 rmap.connect('changeset_comment_delete',
583 590 '/{repo_name:.*?}/changeset-comment/{comment_id}/delete',
584 591 controller='changeset', action='delete_comment',
585 592 conditions=dict(function=check_repo, method=["POST"]))
586 593
587 594 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
588 595 controller='changeset', action='changeset_info')
589 596
590 597 rmap.connect('compare_home',
591 598 '/{repo_name:.*?}/compare',
592 599 controller='compare', action='index',
593 600 conditions=dict(function=check_repo))
594 601
595 602 rmap.connect('compare_url',
596 603 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref_name:.*?}...{other_ref_type}@{other_ref_name:.*?}',
597 604 controller='compare', action='compare',
598 605 conditions=dict(function=check_repo),
599 606 requirements=dict(
600 607 org_ref_type='(branch|book|tag|rev|__other_ref_type__)',
601 608 other_ref_type='(branch|book|tag|rev|__org_ref_type__)')
602 609 )
603 610
604 611 rmap.connect('pullrequest_home',
605 612 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
606 613 action='index', conditions=dict(function=check_repo,
607 614 method=["GET"]))
608 615
609 616 rmap.connect('pullrequest_repo_info',
610 617 '/{repo_name:.*?}/pull-request-repo-info',
611 618 controller='pullrequests', action='repo_info',
612 619 conditions=dict(function=check_repo, method=["GET"]))
613 620
614 621 rmap.connect('pullrequest',
615 622 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
616 623 action='create', conditions=dict(function=check_repo,
617 624 method=["POST"]))
618 625
619 626 rmap.connect('pullrequest_show',
620 627 '/{repo_name:.*?}/pull-request/{pull_request_id:\\d+}{extra:(/.*)?}', extra='',
621 628 controller='pullrequests',
622 629 action='show', conditions=dict(function=check_repo,
623 630 method=["GET"]))
624 631 rmap.connect('pullrequest_post',
625 632 '/{repo_name:.*?}/pull-request/{pull_request_id}',
626 633 controller='pullrequests',
627 634 action='post', conditions=dict(function=check_repo,
628 635 method=["POST"]))
629 636 rmap.connect('pullrequest_delete',
630 637 '/{repo_name:.*?}/pull-request/{pull_request_id}/delete',
631 638 controller='pullrequests',
632 639 action='delete', conditions=dict(function=check_repo,
633 640 method=["POST"]))
634 641
635 642 rmap.connect('pullrequest_show_all',
636 643 '/{repo_name:.*?}/pull-request',
637 644 controller='pullrequests',
638 645 action='show_all', conditions=dict(function=check_repo,
639 646 method=["GET"]))
640 647
641 648 rmap.connect('my_pullrequests',
642 649 '/my_pullrequests',
643 650 controller='pullrequests',
644 651 action='show_my', conditions=dict(method=["GET"]))
645 652
646 653 rmap.connect('pullrequest_comment',
647 654 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
648 655 controller='pullrequests',
649 656 action='comment', conditions=dict(function=check_repo,
650 657 method=["POST"]))
651 658
652 659 rmap.connect('pullrequest_comment_delete',
653 660 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
654 661 controller='pullrequests', action='delete_comment',
655 662 conditions=dict(function=check_repo, method=["POST"]))
656 663
657 664 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
658 665 controller='summary', conditions=dict(function=check_repo))
659 666
660 667 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
661 668 controller='changelog', conditions=dict(function=check_repo))
662 669
663 670 rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
664 671 controller='changelog', f_path=None,
665 672 conditions=dict(function=check_repo))
666 673
667 674 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
668 675 controller='changelog', action='changelog_details',
669 676 conditions=dict(function=check_repo))
670 677
671 678 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
672 679 controller='files', revision='tip', f_path='',
673 680 conditions=dict(function=check_repo))
674 681
675 682 rmap.connect('files_home_nopath', '/{repo_name:.*?}/files/{revision}',
676 683 controller='files', revision='tip', f_path='',
677 684 conditions=dict(function=check_repo))
678 685
679 686 rmap.connect('files_history_home',
680 687 '/{repo_name:.*?}/history/{revision}/{f_path:.*}',
681 688 controller='files', action='history', revision='tip', f_path='',
682 689 conditions=dict(function=check_repo))
683 690
684 691 rmap.connect('files_authors_home',
685 692 '/{repo_name:.*?}/authors/{revision}/{f_path:.*}',
686 693 controller='files', action='authors', revision='tip', f_path='',
687 694 conditions=dict(function=check_repo))
688 695
689 696 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
690 697 controller='files', action='diff', revision='tip', f_path='',
691 698 conditions=dict(function=check_repo))
692 699
693 700 rmap.connect('files_diff_2way_home', '/{repo_name:.*?}/diff-2way/{f_path:.+}',
694 701 controller='files', action='diff_2way', revision='tip', f_path='',
695 702 conditions=dict(function=check_repo))
696 703
697 704 rmap.connect('files_rawfile_home',
698 705 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
699 706 controller='files', action='rawfile', revision='tip',
700 707 f_path='', conditions=dict(function=check_repo))
701 708
702 709 rmap.connect('files_raw_home',
703 710 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
704 711 controller='files', action='raw', revision='tip', f_path='',
705 712 conditions=dict(function=check_repo))
706 713
707 714 rmap.connect('files_annotate_home',
708 715 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
709 716 controller='files', action='index', revision='tip',
710 717 f_path='', annotate=True, conditions=dict(function=check_repo))
711 718
712 719 rmap.connect('files_edit_home',
713 720 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
714 721 controller='files', action='edit', revision='tip',
715 722 f_path='', conditions=dict(function=check_repo))
716 723
717 724 rmap.connect('files_add_home',
718 725 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
719 726 controller='files', action='add', revision='tip',
720 727 f_path='', conditions=dict(function=check_repo))
721 728
722 729 rmap.connect('files_delete_home',
723 730 '/{repo_name:.*?}/delete/{revision}/{f_path:.*}',
724 731 controller='files', action='delete', revision='tip',
725 732 f_path='', conditions=dict(function=check_repo))
726 733
727 734 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
728 735 controller='files', action='archivefile',
729 736 conditions=dict(function=check_repo))
730 737
731 738 rmap.connect('files_nodelist_home',
732 739 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
733 740 controller='files', action='nodelist',
734 741 conditions=dict(function=check_repo))
735 742
736 743 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
737 744 controller='forks', action='fork_create',
738 745 conditions=dict(function=check_repo, method=["POST"]))
739 746
740 747 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
741 748 controller='forks', action='fork',
742 749 conditions=dict(function=check_repo))
743 750
744 751 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
745 752 controller='forks', action='forks',
746 753 conditions=dict(function=check_repo))
747 754
748 755 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
749 756 controller='followers', action='followers',
750 757 conditions=dict(function=check_repo))
751 758
752 759 return rmap
753 760
754 761
755 762 class UrlGenerator(object):
756 763 """Emulate pylons.url in providing a wrapper around routes.url
757 764
758 765 This code was added during migration from Pylons to Turbogears2. Pylons
759 766 already provided a wrapper like this, but Turbogears2 does not.
760 767
761 768 When the routing of Kallithea is changed to use less Routes and more
762 769 Turbogears2-style routing, this class may disappear or change.
763 770
764 771 url() (the __call__ method) returns the URL based on a route name and
765 772 arguments.
766 773 url.current() returns the URL of the current page with arguments applied.
767 774
768 775 Refer to documentation of Routes for details:
769 776 https://routes.readthedocs.io/en/latest/generating.html#generation
770 777 """
771 778 def __call__(self, *args, **kwargs):
772 779 return request.environ['routes.url'](*args, **kwargs)
773 780
774 781 def current(self, *args, **kwargs):
775 782 return request.environ['routes.url'].current(*args, **kwargs)
776 783
777 784
778 785 url = UrlGenerator()
@@ -1,431 +1,465 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.users
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Users crud controller
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 4, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from formencode import htmlfill
33 33 from tg import request, tmpl_context as c, config, app_globals
34 34 from tg.i18n import ugettext as _
35 35 from sqlalchemy.sql.expression import func
36 36 from webob.exc import HTTPFound, HTTPNotFound
37 37
38 38 import kallithea
39 39 from kallithea.config.routing import url
40 40 from kallithea.lib.exceptions import DefaultUserException, \
41 41 UserOwnsReposException, UserCreationError
42 42 from kallithea.lib import helpers as h
43 43 from kallithea.lib.auth import LoginRequired, HasPermissionAnyDecorator, \
44 44 AuthUser
45 45 from kallithea.lib import auth_modules
46 from kallithea.lib.base import BaseController, render
46 from kallithea.lib.base import BaseController, render, IfSshEnabled
47 47 from kallithea.model.api_key import ApiKeyModel
48
48 from kallithea.model.ssh_key import SshKeyModel
49 49 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
50 50 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
51 51 from kallithea.model.user import UserModel
52 52 from kallithea.model.meta import Session
53 53 from kallithea.lib.utils import action_logger
54 54 from kallithea.lib.utils2 import datetime_to_time, safe_int, generate_api_key
55 55
56 56 log = logging.getLogger(__name__)
57 57
58 58
59 59 class UsersController(BaseController):
60 60 """REST Controller styled on the Atom Publishing Protocol"""
61 61
62 62 @LoginRequired()
63 63 @HasPermissionAnyDecorator('hg.admin')
64 64 def _before(self, *args, **kwargs):
65 65 super(UsersController, self)._before(*args, **kwargs)
66 66 c.available_permissions = config['available_permissions']
67 67
68 68 def index(self, format='html'):
69 69 c.users_list = User.query().order_by(User.username) \
70 70 .filter_by(is_default_user=False) \
71 71 .order_by(func.lower(User.username)) \
72 72 .all()
73 73
74 74 users_data = []
75 75 total_records = len(c.users_list)
76 76 _tmpl_lookup = app_globals.mako_lookup
77 77 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
78 78
79 79 grav_tmpl = '<div class="gravatar">%s</div>'
80 80
81 81 username = lambda user_id, username: (
82 82 template.get_def("user_name")
83 83 .render(user_id, username, _=_, h=h, c=c))
84 84
85 85 user_actions = lambda user_id, username: (
86 86 template.get_def("user_actions")
87 87 .render(user_id, username, _=_, h=h, c=c))
88 88
89 89 for user in c.users_list:
90 90 users_data.append({
91 91 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
92 92 "raw_name": user.username,
93 93 "username": username(user.user_id, user.username),
94 94 "firstname": h.escape(user.name),
95 95 "lastname": h.escape(user.lastname),
96 96 "last_login": h.fmt_date(user.last_login),
97 97 "last_login_raw": datetime_to_time(user.last_login),
98 98 "active": h.boolicon(user.active),
99 99 "admin": h.boolicon(user.admin),
100 100 "extern_type": user.extern_type,
101 101 "extern_name": user.extern_name,
102 102 "action": user_actions(user.user_id, user.username),
103 103 })
104 104
105 105 c.data = {
106 106 "sort": None,
107 107 "dir": "asc",
108 108 "records": users_data
109 109 }
110 110
111 111 return render('admin/users/users.html')
112 112
113 113 def create(self):
114 114 c.default_extern_type = User.DEFAULT_AUTH_TYPE
115 115 c.default_extern_name = ''
116 116 user_model = UserModel()
117 117 user_form = UserForm()()
118 118 try:
119 119 form_result = user_form.to_python(dict(request.POST))
120 120 user = user_model.create(form_result)
121 121 action_logger(request.authuser, 'admin_created_user:%s' % user.username,
122 122 None, request.ip_addr)
123 123 h.flash(_('Created user %s') % user.username,
124 124 category='success')
125 125 Session().commit()
126 126 except formencode.Invalid as errors:
127 127 return htmlfill.render(
128 128 render('admin/users/user_add.html'),
129 129 defaults=errors.value,
130 130 errors=errors.error_dict or {},
131 131 prefix_error=False,
132 132 encoding="UTF-8",
133 133 force_defaults=False)
134 134 except UserCreationError as e:
135 135 h.flash(e, 'error')
136 136 except Exception:
137 137 log.error(traceback.format_exc())
138 138 h.flash(_('Error occurred during creation of user %s')
139 139 % request.POST.get('username'), category='error')
140 140 raise HTTPFound(location=url('edit_user', id=user.user_id))
141 141
142 142 def new(self, format='html'):
143 143 c.default_extern_type = User.DEFAULT_AUTH_TYPE
144 144 c.default_extern_name = ''
145 145 return render('admin/users/user_add.html')
146 146
147 147 def update(self, id):
148 148 user_model = UserModel()
149 149 user = user_model.get(id)
150 150 _form = UserForm(edit=True, old_data={'user_id': id,
151 151 'email': user.email})()
152 152 form_result = {}
153 153 try:
154 154 form_result = _form.to_python(dict(request.POST))
155 155 skip_attrs = ['extern_type', 'extern_name',
156 156 ] + auth_modules.get_managed_fields(user)
157 157
158 158 user_model.update(id, form_result, skip_attrs=skip_attrs)
159 159 usr = form_result['username']
160 160 action_logger(request.authuser, 'admin_updated_user:%s' % usr,
161 161 None, request.ip_addr)
162 162 h.flash(_('User updated successfully'), category='success')
163 163 Session().commit()
164 164 except formencode.Invalid as errors:
165 165 defaults = errors.value
166 166 e = errors.error_dict or {}
167 167 defaults.update({
168 168 'create_repo_perm': user_model.has_perm(id,
169 169 'hg.create.repository'),
170 170 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
171 171 })
172 172 return htmlfill.render(
173 173 self._render_edit_profile(user),
174 174 defaults=defaults,
175 175 errors=e,
176 176 prefix_error=False,
177 177 encoding="UTF-8",
178 178 force_defaults=False)
179 179 except Exception:
180 180 log.error(traceback.format_exc())
181 181 h.flash(_('Error occurred during update of user %s')
182 182 % form_result.get('username'), category='error')
183 183 raise HTTPFound(location=url('edit_user', id=id))
184 184
185 185 def delete(self, id):
186 186 usr = User.get_or_404(id)
187 187 try:
188 188 UserModel().delete(usr)
189 189 Session().commit()
190 190 h.flash(_('Successfully deleted user'), category='success')
191 191 except (UserOwnsReposException, DefaultUserException) as e:
192 192 h.flash(e, category='warning')
193 193 except Exception:
194 194 log.error(traceback.format_exc())
195 195 h.flash(_('An error occurred during deletion of user'),
196 196 category='error')
197 197 raise HTTPFound(location=url('users'))
198 198
199 199 def _get_user_or_raise_if_default(self, id):
200 200 try:
201 201 return User.get_or_404(id, allow_default=False)
202 202 except DefaultUserException:
203 203 h.flash(_("The default user cannot be edited"), category='warning')
204 204 raise HTTPNotFound
205 205
206 206 def _render_edit_profile(self, user):
207 207 c.user = user
208 208 c.active = 'profile'
209 209 c.perm_user = AuthUser(dbuser=user)
210 210 managed_fields = auth_modules.get_managed_fields(user)
211 211 c.readonly = lambda n: 'readonly' if n in managed_fields else None
212 212 return render('admin/users/user_edit.html')
213 213
214 214 def edit(self, id, format='html'):
215 215 user = self._get_user_or_raise_if_default(id)
216 216 defaults = user.get_dict()
217 217
218 218 return htmlfill.render(
219 219 self._render_edit_profile(user),
220 220 defaults=defaults,
221 221 encoding="UTF-8",
222 222 force_defaults=False)
223 223
224 224 def edit_advanced(self, id):
225 225 c.user = self._get_user_or_raise_if_default(id)
226 226 c.active = 'advanced'
227 227 c.perm_user = AuthUser(dbuser=c.user)
228 228
229 229 umodel = UserModel()
230 230 defaults = c.user.get_dict()
231 231 defaults.update({
232 232 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
233 233 'create_user_group_perm': umodel.has_perm(c.user,
234 234 'hg.usergroup.create.true'),
235 235 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
236 236 })
237 237 return htmlfill.render(
238 238 render('admin/users/user_edit.html'),
239 239 defaults=defaults,
240 240 encoding="UTF-8",
241 241 force_defaults=False)
242 242
243 243 def edit_api_keys(self, id):
244 244 c.user = self._get_user_or_raise_if_default(id)
245 245 c.active = 'api_keys'
246 246 show_expired = True
247 247 c.lifetime_values = [
248 248 (str(-1), _('Forever')),
249 249 (str(5), _('5 minutes')),
250 250 (str(60), _('1 hour')),
251 251 (str(60 * 24), _('1 day')),
252 252 (str(60 * 24 * 30), _('1 month')),
253 253 ]
254 254 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
255 255 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
256 256 show_expired=show_expired)
257 257 defaults = c.user.get_dict()
258 258 return htmlfill.render(
259 259 render('admin/users/user_edit.html'),
260 260 defaults=defaults,
261 261 encoding="UTF-8",
262 262 force_defaults=False)
263 263
264 264 def add_api_key(self, id):
265 265 c.user = self._get_user_or_raise_if_default(id)
266 266
267 267 lifetime = safe_int(request.POST.get('lifetime'), -1)
268 268 description = request.POST.get('description')
269 269 ApiKeyModel().create(c.user.user_id, description, lifetime)
270 270 Session().commit()
271 271 h.flash(_("API key successfully created"), category='success')
272 272 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
273 273
274 274 def delete_api_key(self, id):
275 275 c.user = self._get_user_or_raise_if_default(id)
276 276
277 277 api_key = request.POST.get('del_api_key')
278 278 if request.POST.get('del_api_key_builtin'):
279 279 c.user.api_key = generate_api_key()
280 280 Session().commit()
281 281 h.flash(_("API key successfully reset"), category='success')
282 282 elif api_key:
283 283 ApiKeyModel().delete(api_key, c.user.user_id)
284 284 Session().commit()
285 285 h.flash(_("API key successfully deleted"), category='success')
286 286
287 287 raise HTTPFound(location=url('edit_user_api_keys', id=c.user.user_id))
288 288
289 289 def update_account(self, id):
290 290 pass
291 291
292 292 def edit_perms(self, id):
293 293 c.user = self._get_user_or_raise_if_default(id)
294 294 c.active = 'perms'
295 295 c.perm_user = AuthUser(dbuser=c.user)
296 296
297 297 umodel = UserModel()
298 298 defaults = c.user.get_dict()
299 299 defaults.update({
300 300 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
301 301 'create_user_group_perm': umodel.has_perm(c.user,
302 302 'hg.usergroup.create.true'),
303 303 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
304 304 })
305 305 return htmlfill.render(
306 306 render('admin/users/user_edit.html'),
307 307 defaults=defaults,
308 308 encoding="UTF-8",
309 309 force_defaults=False)
310 310
311 311 def update_perms(self, id):
312 312 user = self._get_user_or_raise_if_default(id)
313 313
314 314 try:
315 315 form = CustomDefaultPermissionsForm()()
316 316 form_result = form.to_python(request.POST)
317 317
318 318 user_model = UserModel()
319 319
320 320 defs = UserToPerm.query() \
321 321 .filter(UserToPerm.user == user) \
322 322 .all()
323 323 for ug in defs:
324 324 Session().delete(ug)
325 325
326 326 if form_result['create_repo_perm']:
327 327 user_model.grant_perm(id, 'hg.create.repository')
328 328 else:
329 329 user_model.grant_perm(id, 'hg.create.none')
330 330 if form_result['create_user_group_perm']:
331 331 user_model.grant_perm(id, 'hg.usergroup.create.true')
332 332 else:
333 333 user_model.grant_perm(id, 'hg.usergroup.create.false')
334 334 if form_result['fork_repo_perm']:
335 335 user_model.grant_perm(id, 'hg.fork.repository')
336 336 else:
337 337 user_model.grant_perm(id, 'hg.fork.none')
338 338 h.flash(_("Updated permissions"), category='success')
339 339 Session().commit()
340 340 except Exception:
341 341 log.error(traceback.format_exc())
342 342 h.flash(_('An error occurred during permissions saving'),
343 343 category='error')
344 344 raise HTTPFound(location=url('edit_user_perms', id=id))
345 345
346 346 def edit_emails(self, id):
347 347 c.user = self._get_user_or_raise_if_default(id)
348 348 c.active = 'emails'
349 349 c.user_email_map = UserEmailMap.query() \
350 350 .filter(UserEmailMap.user == c.user).all()
351 351
352 352 defaults = c.user.get_dict()
353 353 return htmlfill.render(
354 354 render('admin/users/user_edit.html'),
355 355 defaults=defaults,
356 356 encoding="UTF-8",
357 357 force_defaults=False)
358 358
359 359 def add_email(self, id):
360 360 user = self._get_user_or_raise_if_default(id)
361 361 email = request.POST.get('new_email')
362 362 user_model = UserModel()
363 363
364 364 try:
365 365 user_model.add_extra_email(id, email)
366 366 Session().commit()
367 367 h.flash(_("Added email %s to user") % email, category='success')
368 368 except formencode.Invalid as error:
369 369 msg = error.error_dict['email']
370 370 h.flash(msg, category='error')
371 371 except Exception:
372 372 log.error(traceback.format_exc())
373 373 h.flash(_('An error occurred during email saving'),
374 374 category='error')
375 375 raise HTTPFound(location=url('edit_user_emails', id=id))
376 376
377 377 def delete_email(self, id):
378 378 user = self._get_user_or_raise_if_default(id)
379 379 email_id = request.POST.get('del_email_id')
380 380 user_model = UserModel()
381 381 user_model.delete_extra_email(id, email_id)
382 382 Session().commit()
383 383 h.flash(_("Removed email from user"), category='success')
384 384 raise HTTPFound(location=url('edit_user_emails', id=id))
385 385
386 386 def edit_ips(self, id):
387 387 c.user = self._get_user_or_raise_if_default(id)
388 388 c.active = 'ips'
389 389 c.user_ip_map = UserIpMap.query() \
390 390 .filter(UserIpMap.user == c.user).all()
391 391
392 392 c.default_user_ip_map = UserIpMap.query() \
393 393 .filter(UserIpMap.user == User.get_default_user()).all()
394 394
395 395 defaults = c.user.get_dict()
396 396 return htmlfill.render(
397 397 render('admin/users/user_edit.html'),
398 398 defaults=defaults,
399 399 encoding="UTF-8",
400 400 force_defaults=False)
401 401
402 402 def add_ip(self, id):
403 403 ip = request.POST.get('new_ip')
404 404 user_model = UserModel()
405 405
406 406 try:
407 407 user_model.add_extra_ip(id, ip)
408 408 Session().commit()
409 409 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
410 410 except formencode.Invalid as error:
411 411 msg = error.error_dict['ip']
412 412 h.flash(msg, category='error')
413 413 except Exception:
414 414 log.error(traceback.format_exc())
415 415 h.flash(_('An error occurred while adding IP address'),
416 416 category='error')
417 417
418 418 if 'default_user' in request.POST:
419 419 raise HTTPFound(location=url('admin_permissions_ips'))
420 420 raise HTTPFound(location=url('edit_user_ips', id=id))
421 421
422 422 def delete_ip(self, id):
423 423 ip_id = request.POST.get('del_ip_id')
424 424 user_model = UserModel()
425 425 user_model.delete_extra_ip(id, ip_id)
426 426 Session().commit()
427 427 h.flash(_("Removed IP address from user whitelist"), category='success')
428 428
429 429 if 'default_user' in request.POST:
430 430 raise HTTPFound(location=url('admin_permissions_ips'))
431 431 raise HTTPFound(location=url('edit_user_ips', id=id))
432
433 @IfSshEnabled
434 def edit_ssh_keys(self, id):
435 c.user = self._get_user_or_raise_if_default(id)
436 c.active = 'ssh_keys'
437 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
438 defaults = c.user.get_dict()
439 return htmlfill.render(
440 render('admin/users/user_edit.html'),
441 defaults=defaults,
442 encoding="UTF-8",
443 force_defaults=False)
444
445 @IfSshEnabled
446 def ssh_keys_add(self, id):
447 c.user = self._get_user_or_raise_if_default(id)
448
449 description = request.POST.get('description')
450 public_key = request.POST.get('public_key')
451 new_ssh_key = SshKeyModel().create(c.user.user_id,
452 description, public_key)
453 Session().commit()
454 h.flash(_("SSH key %s successfully added") % new_ssh_key.fingerprint, category='success')
455 raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
456
457 @IfSshEnabled
458 def ssh_keys_delete(self, id):
459 c.user = self._get_user_or_raise_if_default(id)
460
461 public_key = request.POST.get('del_public_key')
462 SshKeyModel().delete(public_key, c.user.user_id)
463 Session().commit()
464 h.flash(_("SSH key successfully deleted"), category='success')
465 raise HTTPFound(location=url('edit_user_ssh_keys', id=c.user.user_id))
@@ -1,185 +1,186 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3 <%block name="title">
4 4 ${_('About')}
5 5 </%block>
6 6 <%block name="header_menu">
7 7 ${self.menu('about')}
8 8 </%block>
9 9 <%def name="main()">
10 10
11 11 <div class="panel panel-primary">
12 12 <div class="panel-heading">
13 13 <h5 class="panel-title">${_('About')} Kallithea</h5>
14 14 </div>
15 15
16 16 <div class="panel-body panel-about">
17 17 <p><a href="https://kallithea-scm.org/">Kallithea</a> is a project of the
18 18 <a href="http://sfconservancy.org/">Software Freedom Conservancy, Inc.</a>
19 19 and is released under the terms of the
20 20 <a href="http://www.gnu.org/copyleft/gpl.html">GNU General Public License,
21 21 v 3.0 (GPLv3)</a>.</p>
22 22
23 23 <p>Kallithea is copyrighted by various authors, including but not
24 24 necessarily limited to the following:</p>
25 25 <ul>
26 26
27 27 <li>Copyright &copy; 2012&ndash;2019, Mads Kiilerich</li>
28 28 <li>Copyright &copy; 2012, 2014&ndash;2017, 2019, Andrej Shadura</li>
29 29 <li>Copyright &copy; 2014&ndash;2019, Thomas De Schampheleire</li>
30 30 <li>Copyright &copy; 2015&ndash;2017, 2019, Étienne Gilli</li>
31 31 <li>Copyright &copy; 2017&ndash;2019, Allan Nordhøy</li>
32 32 <li>Copyright &copy; 2018&ndash;2019, ssantos</li>
33 33 <li>Copyright &copy; 2019, Danni Randeris</li>
34 34 <li>Copyright &copy; 2019, Edmund Wong</li>
35 35 <li>Copyright &copy; 2019, Manuel Jacob</li>
36 36 <li>Copyright &copy; 2019, Wolfgang Scherer</li>
37 37 <li>Copyright &copy; 2012, 2014&ndash;2018, Dominik Ruf</li>
38 38 <li>Copyright &copy; 2014&ndash;2015, 2018, Michal Čihař</li>
39 39 <li>Copyright &copy; 2015, 2018, Branko Majic</li>
40 40 <li>Copyright &copy; 2018, Chris Rule</li>
41 41 <li>Copyright &copy; 2018, Jesús Sánchez</li>
42 42 <li>Copyright &copy; 2018, Patrick Vane</li>
43 43 <li>Copyright &copy; 2018, Pheng Heong Tan</li>
44 44 <li>Copyright &copy; 2018, Максим Якимчук</li>
45 45 <li>Copyright &copy; 2018, Марс Ямбар</li>
46 46 <li>Copyright &copy; 2012&ndash;2017, Unity Technologies</li>
47 47 <li>Copyright &copy; 2015&ndash;2017, Søren Løvborg</li>
48 48 <li>Copyright &copy; 2015, 2017, Sam Jaques</li>
49 49 <li>Copyright &copy; 2016&ndash;2017, Asterios Dimitriou</li>
50 50 <li>Copyright &copy; 2017, Alessandro Molina</li>
51 51 <li>Copyright &copy; 2017, Anton Schur</li>
52 52 <li>Copyright &copy; 2017, Ching-Chen Mao</li>
53 53 <li>Copyright &copy; 2017, Eivind Tagseth</li>
54 54 <li>Copyright &copy; 2017, FUJIWARA Katsunori</li>
55 55 <li>Copyright &copy; 2017, Holger Schramm</li>
56 56 <li>Copyright &copy; 2017, Karl Goetz</li>
57 57 <li>Copyright &copy; 2017, Lars Kruse</li>
58 58 <li>Copyright &copy; 2017, Marko Semet</li>
59 59 <li>Copyright &copy; 2017, Viktar Vauchkevich</li>
60 60 <li>Copyright &copy; 2012&ndash;2016, Takumi IINO</li>
61 61 <li>Copyright &copy; 2015&ndash;2016, Jan Heylen</li>
62 62 <li>Copyright &copy; 2015&ndash;2016, Robert Martinez</li>
63 63 <li>Copyright &copy; 2015&ndash;2016, Robert Rauch</li>
64 64 <li>Copyright &copy; 2016, Angel Ezquerra</li>
65 65 <li>Copyright &copy; 2016, Anton Shestakov</li>
66 66 <li>Copyright &copy; 2016, Brandon Jones</li>
67 67 <li>Copyright &copy; 2016, Kateryna Musina</li>
68 68 <li>Copyright &copy; 2016, Konstantin Veretennicov</li>
69 69 <li>Copyright &copy; 2016, Oscar Curero</li>
70 70 <li>Copyright &copy; 2016, Robert James Dennington</li>
71 71 <li>Copyright &copy; 2016, timeless@gmail.com</li>
72 72 <li>Copyright &copy; 2016, YFdyh000</li>
73 73 <li>Copyright &copy; 2012&ndash;2013, 2015, Aras Pranckevičius</li>
74 74 <li>Copyright &copy; 2014&ndash;2015, Christian Oyarzun</li>
75 75 <li>Copyright &copy; 2014&ndash;2015, Joseph Rivera</li>
76 76 <li>Copyright &copy; 2014&ndash;2015, Sean Farley</li>
77 77 <li>Copyright &copy; 2015, Anatoly Bubenkov</li>
78 78 <li>Copyright &copy; 2015, Andrew Bartlett</li>
79 79 <li>Copyright &copy; 2015, Balázs Úr</li>
80 80 <li>Copyright &copy; 2015, Ben Finney</li>
81 81 <li>Copyright &copy; 2015, Daniel Hobley</li>
82 82 <li>Copyright &copy; 2015, David Avigni</li>
83 83 <li>Copyright &copy; 2015, Denis Blanchette</li>
84 84 <li>Copyright &copy; 2015, duanhongyi</li>
85 85 <li>Copyright &copy; 2015, EriCSN Chang</li>
86 86 <li>Copyright &copy; 2015, Grzegorz Krason</li>
87 87 <li>Copyright &copy; 2015, Jiří Suchan</li>
88 88 <li>Copyright &copy; 2015, Kazunari Kobayashi</li>
89 89 <li>Copyright &copy; 2015, Kevin Bullock</li>
90 90 <li>Copyright &copy; 2015, kobanari</li>
91 91 <li>Copyright &copy; 2015, Marc Abramowitz</li>
92 92 <li>Copyright &copy; 2015, Marc Villetard</li>
93 93 <li>Copyright &copy; 2015, Matthias Zilk</li>
94 94 <li>Copyright &copy; 2015, Michael Pohl</li>
95 95 <li>Copyright &copy; 2015, Michael V. DePalatis</li>
96 96 <li>Copyright &copy; 2015, Morten Skaaning</li>
97 97 <li>Copyright &copy; 2015, Nick High</li>
98 98 <li>Copyright &copy; 2015, Niemand Jedermann</li>
99 99 <li>Copyright &copy; 2015, Peter Vitt</li>
100 100 <li>Copyright &copy; 2015, Ronny Pfannschmidt</li>
101 101 <li>Copyright &copy; 2015, Tuux</li>
102 102 <li>Copyright &copy; 2015, Viktar Palstsiuk</li>
103 103 <li>Copyright &copy; 2014, Ante Ilic</li>
104 104 <li>Copyright &copy; 2014, Bradley M. Kuhn</li>
105 105 <li>Copyright &copy; 2014, Calinou</li>
106 106 <li>Copyright &copy; 2014, Daniel Anderson</li>
107 107 <li>Copyright &copy; 2014, Henrik Stuart</li>
108 108 <li>Copyright &copy; 2014, Ingo von Borstel</li>
109 109 <li>Copyright &copy; 2014, Jelmer Vernooij</li>
110 110 <li>Copyright &copy; 2014, Jim Hague</li>
111 111 <li>Copyright &copy; 2014, Matt Fellows</li>
112 112 <li>Copyright &copy; 2014, Max Roman</li>
113 113 <li>Copyright &copy; 2014, Na'Tosha Bard</li>
114 114 <li>Copyright &copy; 2014, Rasmus Selsmark</li>
115 115 <li>Copyright &copy; 2014, Tim Freund</li>
116 116 <li>Copyright &copy; 2014, Travis Burtrum</li>
117 117 <li>Copyright &copy; 2014, Zoltan Gyarmati</li>
118 118 <li>Copyright &copy; 2010&ndash;2013, Marcin Kuźmiński</li>
119 119 <li>Copyright &copy; 2010&ndash;2013, RhodeCode GmbH</li>
120 120 <li>Copyright &copy; 2011, 2013, Aparkar</li>
121 121 <li>Copyright &copy; 2012&ndash;2013, xpol</li>
122 122 <li>Copyright &copy; 2013, Dennis Brakhane</li>
123 123 <li>Copyright &copy; 2013, Grzegorz Rożniecki</li>
124 <li>Copyright &copy; 2013, Ilya Beda</li>
124 125 <li>Copyright &copy; 2013, Jonathan Sternberg</li>
125 126 <li>Copyright &copy; 2013, Leonardo Carneiro</li>
126 127 <li>Copyright &copy; 2013, Magnus Ericmats</li>
127 128 <li>Copyright &copy; 2013, Martin Vium</li>
128 129 <li>Copyright &copy; 2013, Simon Lopez</li>
129 130 <li>Copyright &copy; 2011&ndash;2012, Augusto Herrmann</li>
130 131 <li>Copyright &copy; 2012, Dan Sheridan</li>
131 132 <li>Copyright &copy; 2012, H Waldo G</li>
132 133 <li>Copyright &copy; 2012, hppj</li>
133 134 <li>Copyright &copy; 2012, Indra Talip</li>
134 135 <li>Copyright &copy; 2012, mikespook</li>
135 136 <li>Copyright &copy; 2012, nansenat16</li>
136 137 <li>Copyright &copy; 2012, Philip Jameson</li>
137 138 <li>Copyright &copy; 2012, Raoul Thill</li>
138 139 <li>Copyright &copy; 2012, Tony Bussieres</li>
139 140 <li>Copyright &copy; 2012, Vincent Duvert</li>
140 141 <li>Copyright &copy; 2012, Vladislav Poluhin</li>
141 142 <li>Copyright &copy; 2012, Zachary Auclair</li>
142 143 <li>Copyright &copy; 2011, Ankit Solanki</li>
143 144 <li>Copyright &copy; 2011, Dmitri Kuznetsov</li>
144 145 <li>Copyright &copy; 2011, Jared Bunting</li>
145 146 <li>Copyright &copy; 2011, Jason Harris</li>
146 147 <li>Copyright &copy; 2011, Les Peabody</li>
147 148 <li>Copyright &copy; 2011, Liad Shani</li>
148 149 <li>Copyright &copy; 2011, Lorenzo M. Catucci</li>
149 150 <li>Copyright &copy; 2011, Matt Zuba</li>
150 151 <li>Copyright &copy; 2011, Nicolas VINOT</li>
151 152 <li>Copyright &copy; 2011, Shawn K. O'Shea</li>
152 153 <li>Copyright &copy; 2010, Łukasz Balcerzak</li>
153 154
154 155 ## We did not list the following copyright holders, given that they appeared
155 156 ## to use for-profit company affiliations in their contribution in the
156 157 ## Mercurial log and therefore I didn't know if copyright was theirs or
157 158 ## their company's.
158 159 ## Copyright &copy; 2011 Thayne Harbaugh <thayne@fusionio.com>
159 160 ## Copyright &copy; 2012 Dies Koper <diesk@fast.au.fujitsu.com>
160 161 ## Copyright &copy; 2012 Erwin Kroon <e.kroon@smartmetersolutions.nl>
161 162 ## Copyright &copy; 2012 Vincent Caron <vcaron@bearstech.com>
162 163 ##
163 164 ## These contributors' contributions may not be copyrightable:
164 165 ## philip.j@hostdime.com in 2012
165 166 ## Stefan Engel <mail@engel-stefan.de> in 2012
166 167 ## Ton Plomp <tcplomp@gmail.com> in 2013
167 168 ##
168 169 </ul>
169 170
170 171 <p>The above are the copyright holders who have submitted direct
171 172 contributions to the Kallithea repository.</p>
172 173
173 174 <p>In the <a href="https://kallithea-scm.org/repos/kallithea">Kallithea
174 175 source code</a>, there is a
175 176 <a href="https://kallithea-scm.org/repos/kallithea/files/tip/LICENSE.md">list
176 177 of third-party libraries and code that Kallithea incorporates</a>.</p>
177 178
178 179 <p>The front-end contains a <a href="${h.url('/LICENSES.txt')}">list of
179 180 software that is used to build the front-end</a> but isn't distributed as a
180 181 part of Kallithea.</p>
181 182
182 183 </div>
183 184 </div>
184 185
185 186 </%def>
@@ -1,43 +1,46 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.html"/>
3 3
4 4 <%block name="title">
5 5 ${_('%s user settings') % c.user.username}
6 6 </%block>
7 7
8 8 <%def name="breadcrumbs_links()">
9 9 ${h.link_to(_('Admin'),h.url('admin_home'))}
10 10 &raquo;
11 11 ${h.link_to(_('Users'),h.url('users'))}
12 12 &raquo;
13 13 ${c.user.username}
14 14 </%def>
15 15
16 16 <%block name="header_menu">
17 17 ${self.menu('admin')}
18 18 </%block>
19 19
20 20 <%def name="main()">
21 21 <div class="panel panel-primary">
22 22 <div class="panel-heading clearfix">
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 ##main
27 27 <div class="panel-body settings">
28 28 <ul class="nav nav-pills nav-stacked">
29 29 <li class="${'active' if c.active=='profile' else ''}"><a href="${h.url('edit_user', id=c.user.user_id)}">${_('Profile')}</a></li>
30 30 <li class="${'active' if c.active=='emails' else ''}"><a href="${h.url('edit_user_emails', id=c.user.user_id)}">${_('Emails')}</a></li>
31 %if c.ssh_enabled:
32 <li class="${'active' if c.active=='ssh_keys' else ''}"><a href="${h.url('edit_user_ssh_keys', id=c.user.user_id)}">${_('SSH Keys')}</a></li>
33 %endif
31 34 <li class="${'active' if c.active=='api_keys' else ''}"><a href="${h.url('edit_user_api_keys', id=c.user.user_id)}">${_('API Keys')}</a></li>
32 35 <li class="${'active' if c.active=='ips' else ''}"><a href="${h.url('edit_user_ips', id=c.user.user_id)}">${_('IP Whitelist')}</a></li>
33 36 <li class="${'active' if c.active=='advanced' else ''}"><a href="${h.url('edit_user_advanced', id=c.user.user_id)}">${_('Advanced')}</a></li>
34 37 <li class="${'active' if c.active=='perms' else ''}"><a href="${h.url('edit_user_perms', id=c.user.user_id)}">${_('Show Permissions')}</a></li>
35 38 </ul>
36 39
37 40 <div>
38 41 <%include file="/admin/users/user_edit_${c.active}.html"/>
39 42 </div>
40 43 </div>
41 44 </div>
42 45
43 46 </%def>
@@ -1,605 +1,653 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14
15 15 from sqlalchemy.orm.exc import NoResultFound, ObjectDeletedError
16 16
17 17 import pytest
18 18 from kallithea.tests.base import *
19 19 from kallithea.tests.fixture import Fixture
20 20 from kallithea.controllers.admin.users import UsersController
21 from kallithea.model.db import User, Permission, UserIpMap, UserApiKeys, RepoGroup
21 from kallithea.model.db import User, Permission, UserIpMap, UserApiKeys, RepoGroup, UserSshKeys
22 22 from kallithea.lib.auth import check_password
23 23 from kallithea.model.user import UserModel
24 24 from kallithea.model import validators
25 25 from kallithea.lib import helpers as h
26 26 from kallithea.model.meta import Session
27 27 from webob.exc import HTTPNotFound
28 28
29 29 from tg.util.webtest import test_context
30 30
31 31 fixture = Fixture()
32 32
33 33
34 34 @pytest.fixture
35 35 def user_and_repo_group_fail():
36 36 username = 'repogrouperr'
37 37 groupname = u'repogroup_fail'
38 38 user = fixture.create_user(name=username)
39 39 repo_group = fixture.create_repo_group(name=groupname, cur_user=username)
40 40 yield user, repo_group
41 41 # cleanup
42 42 if RepoGroup.get_by_group_name(groupname):
43 43 fixture.destroy_repo_group(repo_group)
44 44
45 45
46 46 class TestAdminUsersController(TestController):
47 47 test_user_1 = 'testme'
48 48
49 49 @classmethod
50 50 def teardown_class(cls):
51 51 if User.get_by_username(cls.test_user_1):
52 52 UserModel().delete(cls.test_user_1)
53 53 Session().commit()
54 54
55 55 def test_index(self):
56 56 self.log_user()
57 57 response = self.app.get(url('users'))
58 58 # TODO: Test response...
59 59
60 60 def test_create(self):
61 61 self.log_user()
62 62 username = 'newtestuser'
63 63 password = 'test12'
64 64 password_confirmation = password
65 65 name = u'name'
66 66 lastname = u'lastname'
67 67 email = 'mail@example.com'
68 68
69 69 response = self.app.post(url('new_user'),
70 70 {'username': username,
71 71 'password': password,
72 72 'password_confirmation': password_confirmation,
73 73 'firstname': name,
74 74 'active': True,
75 75 'lastname': lastname,
76 76 'extern_name': 'internal',
77 77 'extern_type': 'internal',
78 78 'email': email,
79 79 '_authentication_token': self.authentication_token()})
80 80 # 302 Found
81 81 # The resource was found at http://localhost/_admin/users/5/edit; you should be redirected automatically.
82 82
83 83 self.checkSessionFlash(response, '''Created user %s''' % username)
84 84
85 85 response = response.follow()
86 86 response.mustcontain("""%s user settings""" % username) # in <title>
87 87
88 88 new_user = Session().query(User). \
89 89 filter(User.username == username).one()
90 90
91 91 assert new_user.username == username
92 92 assert check_password(password, new_user.password) == True
93 93 assert new_user.name == name
94 94 assert new_user.lastname == lastname
95 95 assert new_user.email == email
96 96
97 97 def test_create_err(self):
98 98 self.log_user()
99 99 username = 'new_user'
100 100 password = ''
101 101 name = u'name'
102 102 lastname = u'lastname'
103 103 email = 'errmail.example.com'
104 104
105 105 response = self.app.post(url('new_user'),
106 106 {'username': username,
107 107 'password': password,
108 108 'name': name,
109 109 'active': False,
110 110 'lastname': lastname,
111 111 'email': email,
112 112 '_authentication_token': self.authentication_token()})
113 113
114 114 with test_context(self.app):
115 115 msg = validators.ValidUsername(False, {})._messages['system_invalid_username']
116 116 msg = h.html_escape(msg % {'username': 'new_user'})
117 117 response.mustcontain("""<span class="error-message">%s</span>""" % msg)
118 118 response.mustcontain("""<span class="error-message">Please enter a value</span>""")
119 119 response.mustcontain("""<span class="error-message">An email address must contain a single @</span>""")
120 120
121 121 def get_user():
122 122 Session().query(User).filter(User.username == username).one()
123 123
124 124 with pytest.raises(NoResultFound):
125 125 get_user(), 'found user in database'
126 126
127 127 def test_new(self):
128 128 self.log_user()
129 129 response = self.app.get(url('new_user'))
130 130
131 131 @parametrize('name,attrs',
132 132 [('firstname', {'firstname': 'new_username'}),
133 133 ('lastname', {'lastname': 'new_username'}),
134 134 ('admin', {'admin': True}),
135 135 ('admin', {'admin': False}),
136 136 ('extern_type', {'extern_type': 'ldap'}),
137 137 ('extern_type', {'extern_type': None}),
138 138 ('extern_name', {'extern_name': 'test'}),
139 139 ('extern_name', {'extern_name': None}),
140 140 ('active', {'active': False}),
141 141 ('active', {'active': True}),
142 142 ('email', {'email': 'someemail@example.com'}),
143 143 # ('new_password', {'new_password': 'foobar123',
144 144 # 'password_confirmation': 'foobar123'})
145 145 ])
146 146 def test_update(self, name, attrs):
147 147 self.log_user()
148 148 usr = fixture.create_user(self.test_user_1, password='qweqwe',
149 149 email='testme@example.com',
150 150 extern_type='internal',
151 151 extern_name=self.test_user_1,
152 152 skip_if_exists=True)
153 153 Session().commit()
154 154 params = usr.get_api_data(True)
155 155 params.update({'password_confirmation': ''})
156 156 params.update({'new_password': ''})
157 157 params.update(attrs)
158 158 if name == 'email':
159 159 params['emails'] = [attrs['email']]
160 160 if name == 'extern_type':
161 161 # cannot update this via form, expected value is original one
162 162 params['extern_type'] = "internal"
163 163 if name == 'extern_name':
164 164 # cannot update this via form, expected value is original one
165 165 params['extern_name'] = self.test_user_1
166 166 # special case since this user is not logged in yet his data is
167 167 # not filled so we use creation data
168 168
169 169 params.update({'_authentication_token': self.authentication_token()})
170 170 response = self.app.post(url('update_user', id=usr.user_id), params)
171 171 self.checkSessionFlash(response, 'User updated successfully')
172 172 params.pop('_authentication_token')
173 173
174 174 updated_user = User.get_by_username(self.test_user_1)
175 175 updated_params = updated_user.get_api_data(True)
176 176 updated_params.update({'password_confirmation': ''})
177 177 updated_params.update({'new_password': ''})
178 178
179 179 assert params == updated_params
180 180
181 181 def test_delete(self):
182 182 self.log_user()
183 183 username = 'newtestuserdeleteme'
184 184
185 185 fixture.create_user(name=username)
186 186
187 187 new_user = Session().query(User) \
188 188 .filter(User.username == username).one()
189 189 response = self.app.post(url('delete_user', id=new_user.user_id),
190 190 params={'_authentication_token': self.authentication_token()})
191 191
192 192 self.checkSessionFlash(response, 'Successfully deleted user')
193 193
194 194 def test_delete_repo_err(self):
195 195 self.log_user()
196 196 username = 'repoerr'
197 197 reponame = u'repoerr_fail'
198 198
199 199 fixture.create_user(name=username)
200 200 fixture.create_repo(name=reponame, cur_user=username)
201 201
202 202 new_user = Session().query(User) \
203 203 .filter(User.username == username).one()
204 204 response = self.app.post(url('delete_user', id=new_user.user_id),
205 205 params={'_authentication_token': self.authentication_token()})
206 206 self.checkSessionFlash(response, 'User "%s" still '
207 207 'owns 1 repositories and cannot be removed. '
208 208 'Switch owners or remove those repositories: '
209 209 '%s' % (username, reponame))
210 210
211 211 response = self.app.post(url('delete_repo', repo_name=reponame),
212 212 params={'_authentication_token': self.authentication_token()})
213 213 self.checkSessionFlash(response, 'Deleted repository %s' % reponame)
214 214
215 215 response = self.app.post(url('delete_user', id=new_user.user_id),
216 216 params={'_authentication_token': self.authentication_token()})
217 217 self.checkSessionFlash(response, 'Successfully deleted user')
218 218
219 219 def test_delete_repo_group_err(self, user_and_repo_group_fail):
220 220 new_user, repo_group = user_and_repo_group_fail
221 221 username = new_user.username
222 222 groupname = repo_group.group_name
223 223
224 224 self.log_user()
225 225
226 226 response = self.app.post(url('delete_user', id=new_user.user_id),
227 227 params={'_authentication_token': self.authentication_token()})
228 228 self.checkSessionFlash(response, 'User "%s" still '
229 229 'owns 1 repository groups and cannot be removed. '
230 230 'Switch owners or remove those repository groups: '
231 231 '%s' % (username, groupname))
232 232
233 233 # Relevant _if_ the user deletion succeeded to make sure we can render groups without owner
234 234 # rg = RepoGroup.get_by_group_name(group_name=groupname)
235 235 # response = self.app.get(url('repos_groups', id=rg.group_id))
236 236
237 237 response = self.app.post(url('delete_repo_group', group_name=groupname),
238 238 params={'_authentication_token': self.authentication_token()})
239 239 self.checkSessionFlash(response, 'Removed repository group %s' % groupname)
240 240
241 241 response = self.app.post(url('delete_user', id=new_user.user_id),
242 242 params={'_authentication_token': self.authentication_token()})
243 243 self.checkSessionFlash(response, 'Successfully deleted user')
244 244
245 245 def test_delete_user_group_err(self):
246 246 self.log_user()
247 247 username = 'usergrouperr'
248 248 groupname = u'usergroup_fail'
249 249
250 250 fixture.create_user(name=username)
251 251 ug = fixture.create_user_group(name=groupname, cur_user=username)
252 252
253 253 new_user = Session().query(User) \
254 254 .filter(User.username == username).one()
255 255 response = self.app.post(url('delete_user', id=new_user.user_id),
256 256 params={'_authentication_token': self.authentication_token()})
257 257 self.checkSessionFlash(response, 'User "%s" still '
258 258 'owns 1 user groups and cannot be removed. '
259 259 'Switch owners or remove those user groups: '
260 260 '%s' % (username, groupname))
261 261
262 262 # TODO: why do this fail?
263 263 #response = self.app.delete(url('delete_users_group', id=groupname))
264 264 #self.checkSessionFlash(response, 'Removed user group %s' % groupname)
265 265
266 266 fixture.destroy_user_group(ug.users_group_id)
267 267
268 268 response = self.app.post(url('delete_user', id=new_user.user_id),
269 269 params={'_authentication_token': self.authentication_token()})
270 270 self.checkSessionFlash(response, 'Successfully deleted user')
271 271
272 272 def test_edit(self):
273 273 self.log_user()
274 274 user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
275 275 response = self.app.get(url('edit_user', id=user.user_id))
276 276
277 277 def test_add_perm_create_repo(self):
278 278 self.log_user()
279 279 perm_none = Permission.get_by_key('hg.create.none')
280 280 perm_create = Permission.get_by_key('hg.create.repository')
281 281
282 282 user = UserModel().create_or_update(username='dummy', password='qwe',
283 283 email='dummy', firstname=u'a',
284 284 lastname=u'b')
285 285 Session().commit()
286 286 uid = user.user_id
287 287
288 288 try:
289 289 # User should have None permission on creation repository
290 290 assert UserModel().has_perm(user, perm_none) == False
291 291 assert UserModel().has_perm(user, perm_create) == False
292 292
293 293 response = self.app.post(url('edit_user_perms_update', id=uid),
294 294 params=dict(create_repo_perm=True,
295 295 _authentication_token=self.authentication_token()))
296 296
297 297 perm_none = Permission.get_by_key('hg.create.none')
298 298 perm_create = Permission.get_by_key('hg.create.repository')
299 299
300 300 # User should have None permission on creation repository
301 301 assert UserModel().has_perm(uid, perm_none) == False
302 302 assert UserModel().has_perm(uid, perm_create) == True
303 303 finally:
304 304 UserModel().delete(uid)
305 305 Session().commit()
306 306
307 307 def test_revoke_perm_create_repo(self):
308 308 self.log_user()
309 309 perm_none = Permission.get_by_key('hg.create.none')
310 310 perm_create = Permission.get_by_key('hg.create.repository')
311 311
312 312 user = UserModel().create_or_update(username='dummy', password='qwe',
313 313 email='dummy', firstname=u'a',
314 314 lastname=u'b')
315 315 Session().commit()
316 316 uid = user.user_id
317 317
318 318 try:
319 319 # User should have None permission on creation repository
320 320 assert UserModel().has_perm(user, perm_none) == False
321 321 assert UserModel().has_perm(user, perm_create) == False
322 322
323 323 response = self.app.post(url('edit_user_perms_update', id=uid),
324 324 params=dict(_authentication_token=self.authentication_token()))
325 325
326 326 perm_none = Permission.get_by_key('hg.create.none')
327 327 perm_create = Permission.get_by_key('hg.create.repository')
328 328
329 329 # User should have None permission on creation repository
330 330 assert UserModel().has_perm(uid, perm_none) == True
331 331 assert UserModel().has_perm(uid, perm_create) == False
332 332 finally:
333 333 UserModel().delete(uid)
334 334 Session().commit()
335 335
336 336 def test_add_perm_fork_repo(self):
337 337 self.log_user()
338 338 perm_none = Permission.get_by_key('hg.fork.none')
339 339 perm_fork = Permission.get_by_key('hg.fork.repository')
340 340
341 341 user = UserModel().create_or_update(username='dummy', password='qwe',
342 342 email='dummy', firstname=u'a',
343 343 lastname=u'b')
344 344 Session().commit()
345 345 uid = user.user_id
346 346
347 347 try:
348 348 # User should have None permission on creation repository
349 349 assert UserModel().has_perm(user, perm_none) == False
350 350 assert UserModel().has_perm(user, perm_fork) == False
351 351
352 352 response = self.app.post(url('edit_user_perms_update', id=uid),
353 353 params=dict(create_repo_perm=True,
354 354 _authentication_token=self.authentication_token()))
355 355
356 356 perm_none = Permission.get_by_key('hg.create.none')
357 357 perm_create = Permission.get_by_key('hg.create.repository')
358 358
359 359 # User should have None permission on creation repository
360 360 assert UserModel().has_perm(uid, perm_none) == False
361 361 assert UserModel().has_perm(uid, perm_create) == True
362 362 finally:
363 363 UserModel().delete(uid)
364 364 Session().commit()
365 365
366 366 def test_revoke_perm_fork_repo(self):
367 367 self.log_user()
368 368 perm_none = Permission.get_by_key('hg.fork.none')
369 369 perm_fork = Permission.get_by_key('hg.fork.repository')
370 370
371 371 user = UserModel().create_or_update(username='dummy', password='qwe',
372 372 email='dummy', firstname=u'a',
373 373 lastname=u'b')
374 374 Session().commit()
375 375 uid = user.user_id
376 376
377 377 try:
378 378 # User should have None permission on creation repository
379 379 assert UserModel().has_perm(user, perm_none) == False
380 380 assert UserModel().has_perm(user, perm_fork) == False
381 381
382 382 response = self.app.post(url('edit_user_perms_update', id=uid),
383 383 params=dict(_authentication_token=self.authentication_token()))
384 384
385 385 perm_none = Permission.get_by_key('hg.create.none')
386 386 perm_create = Permission.get_by_key('hg.create.repository')
387 387
388 388 # User should have None permission on creation repository
389 389 assert UserModel().has_perm(uid, perm_none) == True
390 390 assert UserModel().has_perm(uid, perm_create) == False
391 391 finally:
392 392 UserModel().delete(uid)
393 393 Session().commit()
394 394
395 395 def test_ips(self):
396 396 self.log_user()
397 397 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
398 398 response = self.app.get(url('edit_user_ips', id=user.user_id))
399 399 response.mustcontain('All IP addresses are allowed')
400 400
401 401 @parametrize('test_name,ip,ip_range,failure', [
402 402 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
403 403 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
404 404 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
405 405 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
406 406 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
407 407 ('127_bad_ip', 'foobar', 'foobar', True),
408 408 ])
409 409 def test_add_ip(self, test_name, ip, ip_range, failure, auto_clear_ip_permissions):
410 410 self.log_user()
411 411 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
412 412 user_id = user.user_id
413 413
414 414 response = self.app.post(url('edit_user_ips_update', id=user_id),
415 415 params=dict(new_ip=ip, _authentication_token=self.authentication_token()))
416 416
417 417 if failure:
418 418 self.checkSessionFlash(response, 'Please enter a valid IPv4 or IPv6 address')
419 419 response = self.app.get(url('edit_user_ips', id=user_id))
420 420 response.mustcontain(no=[ip])
421 421 response.mustcontain(no=[ip_range])
422 422
423 423 else:
424 424 response = self.app.get(url('edit_user_ips', id=user_id))
425 425 response.mustcontain(ip)
426 426 response.mustcontain(ip_range)
427 427
428 428 def test_delete_ip(self, auto_clear_ip_permissions):
429 429 self.log_user()
430 430 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
431 431 user_id = user.user_id
432 432 ip = '127.0.0.1/32'
433 433 ip_range = '127.0.0.1 - 127.0.0.1'
434 434 with test_context(self.app):
435 435 new_ip = UserModel().add_extra_ip(user_id, ip)
436 436 Session().commit()
437 437 new_ip_id = new_ip.ip_id
438 438
439 439 response = self.app.get(url('edit_user_ips', id=user_id))
440 440 response.mustcontain(ip)
441 441 response.mustcontain(ip_range)
442 442
443 443 self.app.post(url('edit_user_ips_delete', id=user_id),
444 444 params=dict(del_ip_id=new_ip_id, _authentication_token=self.authentication_token()))
445 445
446 446 response = self.app.get(url('edit_user_ips', id=user_id))
447 447 response.mustcontain('All IP addresses are allowed')
448 448 response.mustcontain(no=[ip])
449 449 response.mustcontain(no=[ip_range])
450 450
451 451 def test_api_keys(self):
452 452 self.log_user()
453 453
454 454 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
455 455 response = self.app.get(url('edit_user_api_keys', id=user.user_id))
456 456 response.mustcontain(user.api_key)
457 457 response.mustcontain('Expires: Never')
458 458
459 459 @parametrize('desc,lifetime', [
460 460 ('forever', -1),
461 461 ('5mins', 60*5),
462 462 ('30days', 60*60*24*30),
463 463 ])
464 464 def test_add_api_keys(self, desc, lifetime):
465 465 self.log_user()
466 466 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
467 467 user_id = user.user_id
468 468
469 469 response = self.app.post(url('edit_user_api_keys_update', id=user_id),
470 470 {'description': desc, 'lifetime': lifetime, '_authentication_token': self.authentication_token()})
471 471 self.checkSessionFlash(response, 'API key successfully created')
472 472 try:
473 473 response = response.follow()
474 474 user = User.get(user_id)
475 475 for api_key in user.api_keys:
476 476 response.mustcontain(api_key)
477 477 finally:
478 478 for api_key in UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all():
479 479 Session().delete(api_key)
480 480 Session().commit()
481 481
482 482 def test_remove_api_key(self):
483 483 self.log_user()
484 484 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
485 485 user_id = user.user_id
486 486
487 487 response = self.app.post(url('edit_user_api_keys_update', id=user_id),
488 488 {'description': 'desc', 'lifetime': -1, '_authentication_token': self.authentication_token()})
489 489 self.checkSessionFlash(response, 'API key successfully created')
490 490 response = response.follow()
491 491
492 492 # now delete our key
493 493 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
494 494 assert 1 == len(keys)
495 495
496 496 response = self.app.post(url('edit_user_api_keys_delete', id=user_id),
497 497 {'del_api_key': keys[0].api_key, '_authentication_token': self.authentication_token()})
498 498 self.checkSessionFlash(response, 'API key successfully deleted')
499 499 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
500 500 assert 0 == len(keys)
501 501
502 502 def test_reset_main_api_key(self):
503 503 self.log_user()
504 504 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
505 505 user_id = user.user_id
506 506 api_key = user.api_key
507 507 response = self.app.get(url('edit_user_api_keys', id=user_id))
508 508 response.mustcontain(api_key)
509 509 response.mustcontain('Expires: Never')
510 510
511 511 response = self.app.post(url('edit_user_api_keys_delete', id=user_id),
512 512 {'del_api_key_builtin': api_key, '_authentication_token': self.authentication_token()})
513 513 self.checkSessionFlash(response, 'API key successfully reset')
514 514 response = response.follow()
515 515 response.mustcontain(no=[api_key])
516 516
517 def test_add_ssh_key(self):
518 description = u'something'
519 public_key = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
520 fingerprint = u'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
521
522 self.log_user()
523 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
524 user_id = user.user_id
525
526 response = self.app.post(url('edit_user_ssh_keys', id=user_id),
527 {'description': description,
528 'public_key': public_key,
529 '_authentication_token': self.authentication_token()})
530 self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint)
531
532 response = response.follow()
533 response.mustcontain(fingerprint)
534 ssh_key = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).one()
535 assert ssh_key.fingerprint == fingerprint
536 assert ssh_key.description == description
537 Session().delete(ssh_key)
538 Session().commit()
539
540 def test_remove_ssh_key(self):
541 description = u''
542 public_key = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
543 fingerprint = u'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
544
545 self.log_user()
546 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
547 user_id = user.user_id
548
549 response = self.app.post(url('edit_user_ssh_keys', id=user_id),
550 {'description': description,
551 'public_key': public_key,
552 '_authentication_token': self.authentication_token()})
553 self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint)
554 response.follow()
555 ssh_key = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).one()
556 assert ssh_key.description == description
557
558 response = self.app.post(url('edit_user_ssh_keys_delete', id=user_id),
559 {'del_public_key': ssh_key.public_key,
560 '_authentication_token': self.authentication_token()})
561 self.checkSessionFlash(response, 'SSH key successfully deleted')
562 keys = UserSshKeys.query().all()
563 assert 0 == len(keys)
564
517 565
518 566 class TestAdminUsersController_unittest(TestController):
519 567 """ Unit tests for the users controller """
520 568
521 569 def test_get_user_or_raise_if_default(self, monkeypatch, test_context_fixture):
522 570 # flash complains about an non-existing session
523 571 def flash_mock(*args, **kwargs):
524 572 pass
525 573 monkeypatch.setattr(h, 'flash', flash_mock)
526 574
527 575 u = UsersController()
528 576 # a regular user should work correctly
529 577 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
530 578 assert u._get_user_or_raise_if_default(user.user_id) == user
531 579 # the default user should raise
532 580 with pytest.raises(HTTPNotFound):
533 581 u._get_user_or_raise_if_default(User.get_default_user().user_id)
534 582
535 583
536 584 class TestAdminUsersControllerForDefaultUser(TestController):
537 585 """
538 586 Edit actions on the default user are not allowed.
539 587 Validate that they throw a 404 exception.
540 588 """
541 589 def test_edit_default_user(self):
542 590 self.log_user()
543 591 user = User.get_default_user()
544 592 response = self.app.get(url('edit_user', id=user.user_id), status=404)
545 593
546 594 def test_edit_advanced_default_user(self):
547 595 self.log_user()
548 596 user = User.get_default_user()
549 597 response = self.app.get(url('edit_user_advanced', id=user.user_id), status=404)
550 598
551 599 # API keys
552 600 def test_edit_api_keys_default_user(self):
553 601 self.log_user()
554 602 user = User.get_default_user()
555 603 response = self.app.get(url('edit_user_api_keys', id=user.user_id), status=404)
556 604
557 605 def test_add_api_keys_default_user(self):
558 606 self.log_user()
559 607 user = User.get_default_user()
560 608 response = self.app.post(url('edit_user_api_keys_update', id=user.user_id),
561 609 {'_authentication_token': self.authentication_token()}, status=404)
562 610
563 611 def test_delete_api_keys_default_user(self):
564 612 self.log_user()
565 613 user = User.get_default_user()
566 614 response = self.app.post(url('edit_user_api_keys_delete', id=user.user_id),
567 615 {'_authentication_token': self.authentication_token()}, status=404)
568 616
569 617 # Permissions
570 618 def test_edit_perms_default_user(self):
571 619 self.log_user()
572 620 user = User.get_default_user()
573 621 response = self.app.get(url('edit_user_perms', id=user.user_id), status=404)
574 622
575 623 def test_update_perms_default_user(self):
576 624 self.log_user()
577 625 user = User.get_default_user()
578 626 response = self.app.post(url('edit_user_perms_update', id=user.user_id),
579 627 {'_authentication_token': self.authentication_token()}, status=404)
580 628
581 629 # Emails
582 630 def test_edit_emails_default_user(self):
583 631 self.log_user()
584 632 user = User.get_default_user()
585 633 response = self.app.get(url('edit_user_emails', id=user.user_id), status=404)
586 634
587 635 def test_add_emails_default_user(self):
588 636 self.log_user()
589 637 user = User.get_default_user()
590 638 response = self.app.post(url('edit_user_emails_update', id=user.user_id),
591 639 {'_authentication_token': self.authentication_token()}, status=404)
592 640
593 641 def test_delete_emails_default_user(self):
594 642 self.log_user()
595 643 user = User.get_default_user()
596 644 response = self.app.post(url('edit_user_emails_delete', id=user.user_id),
597 645 {'_authentication_token': self.authentication_token()}, status=404)
598 646
599 647 # IP addresses
600 648 # Add/delete of IP addresses for the default user is used to maintain
601 649 # the global IP whitelist and thus allowed. Only 'edit' is forbidden.
602 650 def test_edit_ip_default_user(self):
603 651 self.log_user()
604 652 user = User.get_default_user()
605 653 response = self.app.get(url('edit_user_ips', id=user.user_id), status=404)
@@ -1,96 +1,97 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Some committers are so wrong that it doesn't point at any contributor:
4 4 total_ignore = set()
5 5 total_ignore.add('*** failed to import extension hggit: No module named hggit')
6 6 total_ignore.add('<>')
7 7
8 8 # Normalize some committer names where people have contributed under different
9 9 # names or email addresses:
10 10 name_fixes = {}
11 11 name_fixes['Andrew Shadura'] = "Andrej Shadura <andrew@shadura.me>"
12 12 name_fixes['aparkar'] = "Aparkar <aparkar@icloud.com>"
13 13 name_fixes['Aras Pranckevicius'] = "Aras Pranckevičius <aras@unity3d.com>"
14 14 name_fixes['Augosto Hermann'] = "Augusto Herrmann <augusto.herrmann@planejamento.gov.br>"
15 15 name_fixes['"Bradley M. Kuhn" <bkuhn@ebb.org>'] = "Bradley M. Kuhn <bkuhn@sfconservancy.org>"
16 16 name_fixes['dmitri.kuznetsov'] = "Dmitri Kuznetsov"
17 17 name_fixes['Dmitri Kuznetsov'] = "Dmitri Kuznetsov"
18 18 name_fixes['domruf'] = "Dominik Ruf <dominikruf@gmail.com>"
19 19 name_fixes['Ingo von borstel'] = "Ingo von Borstel <kallithea@planetmaker.de>"
20 20 name_fixes['Jan Heylen'] = "Jan Heylen <heyleke@gmail.com>"
21 21 name_fixes['Jason F. Harris'] = "Jason Harris <jason@jasonfharris.com>"
22 22 name_fixes['Jelmer Vernooij'] = "Jelmer Vernooij <jelmer@samba.org>"
23 23 name_fixes['jfh <jason@jasonfharris.com>'] = "Jason Harris <jason@jasonfharris.com>"
24 24 name_fixes['Leonardo Carneiro<leonardo@unity3d.com>'] = "Leonardo Carneiro <leonardo@unity3d.com>"
25 25 name_fixes['leonardo'] = "Leonardo Carneiro <leonardo@unity3d.com>"
26 26 name_fixes['Leonardo <leo@unity3d.com>'] = "Leonardo Carneiro <leonardo@unity3d.com>"
27 27 name_fixes['Les Peabody'] = "Les Peabody <lpeabody@gmail.com>"
28 28 name_fixes['"Lorenzo M. Catucci" <lorenzo@sancho.ccd.uniroma2.it>'] = "Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>"
29 29 name_fixes['Lukasz Balcerzak'] = "Łukasz Balcerzak <lukaszbalcerzak@gmail.com>"
30 30 name_fixes['mao <mao@lins.fju.edu.tw>'] = "Ching-Chen Mao <mao@lins.fju.edu.tw>"
31 31 name_fixes['marcink'] = "Marcin Kuźmiński <marcin@python-works.com>"
32 32 name_fixes['Marcin Kuzminski'] = "Marcin Kuźmiński <marcin@python-works.com>"
33 33 name_fixes['nansenat16@null.tw'] = "nansenat16 <nansenat16@null.tw>"
34 34 name_fixes['Peter Vitt'] = "Peter Vitt <petervitt@web.de>"
35 35 name_fixes['philip.j@hostdime.com'] = "Philip Jameson <philip.j@hostdime.com>"
36 36 name_fixes['Søren Løvborg'] = "Søren Løvborg <sorenl@unity3d.com>"
37 37 name_fixes['Thomas De Schampheleire'] = "Thomas De Schampheleire <thomas.de_schampheleire@nokia.com>"
38 38 name_fixes['Hosted Weblate'] = "<>"
39 39 name_fixes['Weblate'] = "<>"
40 40 name_fixes['xpol'] = "xpol <xpolife@gmail.com>"
41 41 name_fixes['Lars <devel@sumpfralle.de>'] = "Lars Kruse <devel@sumpfralle.de>"
42 42
43 43 # Some committer email address domains that indicate that another entity might
44 44 # hold some copyright too:
45 45 domain_extra = {}
46 46 domain_extra['unity3d.com'] = "Unity Technologies"
47 47 domain_extra['rhodecode.com'] = "RhodeCode GmbH"
48 48
49 49 # Repository history show some old contributions that traditionally hasn't been
50 50 # listed in about.html - preserve that:
51 51 no_about = set(total_ignore)
52 52 # The following contributors were traditionally not listed in about.html and it
53 53 # seems unclear if the copyright is personal or belongs to a company.
54 54 no_about.add(('Thayne Harbaugh <thayne@fusionio.com>', '2011'))
55 55 no_about.add(('Dies Koper <diesk@fast.au.fujitsu.com>', '2012'))
56 56 no_about.add(('Erwin Kroon <e.kroon@smartmetersolutions.nl>', '2012'))
57 57 no_about.add(('Vincent Caron <vcaron@bearstech.com>', '2012'))
58 58 # These contributors' contributions might be too small to be copyrightable:
59 59 no_about.add(('philip.j@hostdime.com', '2012'))
60 60 no_about.add(('Stefan Engel <mail@engel-stefan.de>', '2012'))
61 61 no_about.add(('Ton Plomp <tcplomp@gmail.com>', '2013'))
62 62 # Was reworked and contributed later and shadowed by other contributions:
63 63 no_about.add(('Sean Farley <sean.michael.farley@gmail.com>', '2013'))
64 64
65 65 # Contributors in about.html and CONTRIBUTORS not appearing in repository
66 66 # history:
67 67 other = [
68 68 # Work folded into commits attributed to others:
69 ('2013', 'Ilya Beda <ir4y.ix@gmail.com>'),
69 70 ]
70 71
71 72 # Preserve contributors listed in about.html but not appearing in repository
72 73 # history:
73 74 other_about = [
74 75 ("2011", "Aparkar <aparkar@icloud.com>"),
75 76 ("2010", "RhodeCode GmbH"),
76 77 ("2011", "RhodeCode GmbH"),
77 78 ("2012", "RhodeCode GmbH"),
78 79 ("2013", "RhodeCode GmbH"),
79 80 ]
80 81
81 82 # Preserve contributors listed in CONTRIBUTORS but not appearing in repository
82 83 # history:
83 84 other_contributors = [
84 85 ("", "Andrew Kesterson <andrew@aklabs.net>"),
85 86 ("", "cejones"),
86 87 ("", "David A. Sjøen <david.sjoen@westcon.no>"),
87 88 ("", "James Rhodes <jrhodes@redpointsoftware.com.au>"),
88 89 ("", "Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>"),
89 90 ("", "larikale"),
90 91 ("", "RhodeCode GmbH"),
91 92 ("", "Sebastian Kreutzberger <sebastian@rhodecode.com>"),
92 93 ("", "Steve Romanow <slestak989@gmail.com>"),
93 94 ("", "SteveCohen"),
94 95 ("", "Thomas <thomas@rhodecode.com>"),
95 96 ("", "Thomas Waldmann <tw-public@gmx.de>"),
96 97 ]
General Comments 0
You need to be logged in to leave comments. Login now