##// END OF EJS Templates
spelling: more consistent casing of 'IP'
Mads Kiilerich -
r5125:2aeaf636 default
parent child Browse files
Show More
@@ -1,291 +1,291 b''
1 .. _installation_win_old:
1 .. _installation_win_old:
2
2
3
3
4 Installation and upgrade on Windows (XP/Vista/Server 2003/Server 2008)
4 Installation and upgrade on Windows (XP/Vista/Server 2003/Server 2008)
5 ======================================================================
5 ======================================================================
6
6
7 First time install
7 First time install
8 ::::::::::::::::::
8 ::::::::::::::::::
9
9
10 Target OS: Windows XP SP3 32bit English (Clean installation)
10 Target OS: Windows XP SP3 32bit English (Clean installation)
11 + All Windows Updates until 24-may-2012
11 + All Windows Updates until 24-may-2012
12
12
13 .. note::
13 .. note::
14
14
15 This installation is for 32bit systems, for 64bit windows you might need
15 This installation is for 32bit systems, for 64bit windows you might need
16 to download proper 64bit versions of the different packages(Windows Installer, Win32py extensions)
16 to download proper 64bit versions of the different packages(Windows Installer, Win32py extensions)
17 plus some extra tweaks.
17 plus some extra tweaks.
18 These extra steps haven been marked as "64bit".
18 These extra steps haven been marked as "64bit".
19 Tested on Windows Server 2008 R2 SP1, 9-feb-2013.
19 Tested on Windows Server 2008 R2 SP1, 9-feb-2013.
20 If you run into any 64bit related problems, please check these pages:
20 If you run into any 64bit related problems, please check these pages:
21 - http://blog.victorjabur.com/2011/06/05/compiling-python-2-7-modules-on-windows-32-and-64-using-msvc-2008-express/
21 - http://blog.victorjabur.com/2011/06/05/compiling-python-2-7-modules-on-windows-32-and-64-using-msvc-2008-express/
22 - http://bugs.python.org/issue7511
22 - http://bugs.python.org/issue7511
23
23
24 Step1 - Install Visual Studio 2008 Express
24 Step1 - Install Visual Studio 2008 Express
25 ------------------------------------------
25 ------------------------------------------
26
26
27
27
28 Optional: You can also install MinGW, but VS2008 installation is easier.
28 Optional: You can also install MinGW, but VS2008 installation is easier.
29
29
30 Download "Visual C++ 2008 Express Edition with SP1" from:
30 Download "Visual C++ 2008 Express Edition with SP1" from:
31 http://download.microsoft.com/download/E/8/E/E8EEB394-7F42-4963-A2D8-29559B738298/VS2008ExpressWithSP1ENUX1504728.iso
31 http://download.microsoft.com/download/E/8/E/E8EEB394-7F42-4963-A2D8-29559B738298/VS2008ExpressWithSP1ENUX1504728.iso
32 (if not found or relocated, google for "visual studio 2008 express" for updated link. This link was taken from http://stackoverflow.com/questions/15318560/visual-c-2008-express-download-link-dead)
32 (if not found or relocated, google for "visual studio 2008 express" for updated link. This link was taken from http://stackoverflow.com/questions/15318560/visual-c-2008-express-download-link-dead)
33
33
34 You can also download full ISO file for offline installation, just
34 You can also download full ISO file for offline installation, just
35 choose "All - Offline Install ISO image file" in the previous page and
35 choose "All - Offline Install ISO image file" in the previous page and
36 choose "Visual C++ 2008 Express" when installing.
36 choose "Visual C++ 2008 Express" when installing.
37
37
38 .. note::
38 .. note::
39
39
40 Using other versions of Visual Studio will lead to random crashes.
40 Using other versions of Visual Studio will lead to random crashes.
41 You must use Visual Studio 2008!"
41 You must use Visual Studio 2008!"
42
42
43 .. note::
43 .. note::
44
44
45 Silverlight Runtime and SQL Server 2008 Express Edition are not
45 Silverlight Runtime and SQL Server 2008 Express Edition are not
46 required, you can uncheck them
46 required, you can uncheck them
47
47
48 .. note::
48 .. note::
49
49
50 64bit: You also need to install the Microsoft Windows SDK for .NET 3.5 SP1 (.NET 4.0 won't work).
50 64bit: You also need to install the Microsoft Windows SDK for .NET 3.5 SP1 (.NET 4.0 won't work).
51 Download from: http://www.microsoft.com/en-us/download/details.aspx?id=3138
51 Download from: http://www.microsoft.com/en-us/download/details.aspx?id=3138
52
52
53 .. note::
53 .. note::
54
54
55 64bit: You also need to copy and rename a .bat file to make the Visual C++ compiler work.
55 64bit: You also need to copy and rename a .bat file to make the Visual C++ compiler work.
56 I am not sure why this is not necessary for 32bit.
56 I am not sure why this is not necessary for 32bit.
57 Copy C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\vcvars64.bat to C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\amd64\vcvarsamd64.bat
57 Copy C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\vcvars64.bat to C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\bin\amd64\vcvarsamd64.bat
58
58
59
59
60 Step2 - Install Python
60 Step2 - Install Python
61 ----------------------
61 ----------------------
62
62
63 Install Python 2.x.y (x = 6 or 7) x86 version (32bit). DO NOT USE A 3.x version.
63 Install Python 2.x.y (x = 6 or 7) x86 version (32bit). DO NOT USE A 3.x version.
64 Download Python 2.x.y from:
64 Download Python 2.x.y from:
65 http://www.python.org/download/
65 http://www.python.org/download/
66
66
67 Choose "Windows Installer" (32bit version) not "Windows X86-64
67 Choose "Windows Installer" (32bit version) not "Windows X86-64
68 Installer". While writing this guide, the latest version was v2.7.3.
68 Installer". While writing this guide, the latest version was v2.7.3.
69 Remember the specific major and minor version installed, because it will
69 Remember the specific major and minor version installed, because it will
70 be needed in the next step. In this case, it is "2.7".
70 be needed in the next step. In this case, it is "2.7".
71
71
72 .. note::
72 .. note::
73
73
74 64bit: Just download and install the 64bit version of python.
74 64bit: Just download and install the 64bit version of python.
75
75
76 Step3 - Install Win32py extensions
76 Step3 - Install Win32py extensions
77 ----------------------------------
77 ----------------------------------
78
78
79 Download pywin32 from:
79 Download pywin32 from:
80 http://sourceforge.net/projects/pywin32/files/
80 http://sourceforge.net/projects/pywin32/files/
81
81
82 - Click on "pywin32" folder
82 - Click on "pywin32" folder
83 - Click on the first folder (in this case, Build 217, maybe newer when you try)
83 - Click on the first folder (in this case, Build 217, maybe newer when you try)
84 - Choose the file ending with ".win32-py2.x.exe" -> x being the minor
84 - Choose the file ending with ".win32-py2.x.exe" -> x being the minor
85 version of Python you installed (in this case, 7)
85 version of Python you installed (in this case, 7)
86 When writing this guide, the file was:
86 When writing this guide, the file was:
87 http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/pywin32-217.win32-py2.7.exe/download
87 http://sourceforge.net/projects/pywin32/files/pywin32/Build%20217/pywin32-217.win32-py2.7.exe/download
88
88
89 .. note::
89 .. note::
90
90
91 64bit: Download and install the 64bit version.
91 64bit: Download and install the 64bit version.
92 At the time of writing you can find this at:
92 At the time of writing you can find this at:
93 http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win-amd64-py2.7.exe/download
93 http://sourceforge.net/projects/pywin32/files/pywin32/Build%20218/pywin32-218.win-amd64-py2.7.exe/download
94
94
95 Step4 - Python BIN
95 Step4 - Python BIN
96 ------------------
96 ------------------
97
97
98 Add Python BIN folder to the path
98 Add Python BIN folder to the path
99
99
100 You have to add the Python folder to the path, you can do it manually
100 You have to add the Python folder to the path, you can do it manually
101 (editing "PATH" environment variable) or using Windows Support Tools
101 (editing "PATH" environment variable) or using Windows Support Tools
102 that came preinstalled in Vista/7 and can be installed in Windows XP.
102 that came preinstalled in Vista/7 and can be installed in Windows XP.
103
103
104 - Using support tools on WINDOWS XP:
104 - Using support tools on WINDOWS XP:
105 If you use Windows XP you can install them using Windows XP CD and
105 If you use Windows XP you can install them using Windows XP CD and
106 navigating to \SUPPORT\TOOLS. There, execute Setup.EXE (not MSI).
106 navigating to \SUPPORT\TOOLS. There, execute Setup.EXE (not MSI).
107 Afterwards, open a CMD and type::
107 Afterwards, open a CMD and type::
108
108
109 SETX PATH "%PATH%;[your-python-path]" -M
109 SETX PATH "%PATH%;[your-python-path]" -M
110
110
111 Close CMD (the path variable will be updated then)
111 Close CMD (the path variable will be updated then)
112
112
113 - Using support tools on WINDOWS Vista/7:
113 - Using support tools on WINDOWS Vista/7:
114
114
115 Open a CMD and type::
115 Open a CMD and type::
116
116
117 SETX PATH "%PATH%;[your-python-path]" /M
117 SETX PATH "%PATH%;[your-python-path]" /M
118
118
119 Please substitute [your-python-path] with your Python installation path.
119 Please substitute [your-python-path] with your Python installation path.
120 Typically: C:\\Python27
120 Typically: C:\\Python27
121
121
122
122
123 Step5 - Kallithea folder structure
123 Step5 - Kallithea folder structure
124 ----------------------------------
124 ----------------------------------
125
125
126 Create a Kallithea folder structure
126 Create a Kallithea folder structure
127
127
128 This is only a example to install Kallithea, you can of course change
128 This is only a example to install Kallithea, you can of course change
129 it. However, this guide will follow the proposed structure, so please
129 it. However, this guide will follow the proposed structure, so please
130 later adapt the paths if you change them. My recommendation is to use
130 later adapt the paths if you change them. My recommendation is to use
131 folders with NO SPACES. But you can try if you are brave...
131 folders with NO SPACES. But you can try if you are brave...
132
132
133 Create the following folder structure::
133 Create the following folder structure::
134
134
135 C:\Kallithea
135 C:\Kallithea
136 C:\Kallithea\Bin
136 C:\Kallithea\Bin
137 C:\Kallithea\Env
137 C:\Kallithea\Env
138 C:\Kallithea\Repos
138 C:\Kallithea\Repos
139
139
140
140
141 Step6 - Install virtualenv
141 Step6 - Install virtualenv
142 ---------------------------
142 ---------------------------
143
143
144 Install Virtual Env for Python
144 Install Virtual Env for Python
145
145
146 Navigate to: http://www.virtualenv.org/en/latest/index.html#installation
146 Navigate to: http://www.virtualenv.org/en/latest/index.html#installation
147 Right click on "virtualenv.py" file and choose "Save link as...".
147 Right click on "virtualenv.py" file and choose "Save link as...".
148 Download to C:\\Kallithea (or whatever you want)
148 Download to C:\\Kallithea (or whatever you want)
149 (the file is located at
149 (the file is located at
150 https://raw.github.com/pypa/virtualenv/master/virtualenv.py)
150 https://raw.github.com/pypa/virtualenv/master/virtualenv.py)
151
151
152 Create a virtual Python environment in C:\\Kallithea\\Env (or similar). To
152 Create a virtual Python environment in C:\\Kallithea\\Env (or similar). To
153 do so, open a CMD (Python Path should be included in Step3), navigate
153 do so, open a CMD (Python Path should be included in Step3), navigate
154 where you downloaded "virtualenv.py", and write::
154 where you downloaded "virtualenv.py", and write::
155
155
156 python virtualenv.py C:\Kallithea\Env
156 python virtualenv.py C:\Kallithea\Env
157
157
158 (--no-site-packages is now the default behaviour of virtualenv, no need
158 (--no-site-packages is now the default behaviour of virtualenv, no need
159 to include it)
159 to include it)
160
160
161
161
162 Step7 - Install Kallithea
162 Step7 - Install Kallithea
163 -------------------------
163 -------------------------
164
164
165 Finally, install Kallithea
165 Finally, install Kallithea
166
166
167 Close previously opened command prompt/s, and open a Visual Studio 2008
167 Close previously opened command prompt/s, and open a Visual Studio 2008
168 Command Prompt (**IMPORTANT!!**). To do so, go to Start Menu, and then open
168 Command Prompt (**IMPORTANT!!**). To do so, go to Start Menu, and then open
169 "Microsoft Visual C++ 2008 Express Edition" -> "Visual Studio Tools" ->
169 "Microsoft Visual C++ 2008 Express Edition" -> "Visual Studio Tools" ->
170 "Visual Studio 2008 Command Prompt"
170 "Visual Studio 2008 Command Prompt"
171
171
172 .. note::
172 .. note::
173
173
174 64bit: For 64bit you need to modify the shortcut that is used to start the
174 64bit: For 64bit you need to modify the shortcut that is used to start the
175 Visual Studio 2008 Command Prompt. Use right-mouse click to open properties.
175 Visual Studio 2008 Command Prompt. Use right-mouse click to open properties.
176
176
177 Change commandline from::
177 Change commandline from::
178
178
179 %comspec% /k ""C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat"" x86
179 %comspec% /k ""C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat"" x86
180
180
181 to::
181 to::
182
182
183 %comspec% /k ""C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat"" amd64
183 %comspec% /k ""C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat"" amd64
184
184
185
185
186 In that CMD (loaded with VS2008 PATHs) type::
186 In that CMD (loaded with VS2008 PATHs) type::
187
187
188 cd C:\Kallithea\Env\Scripts (or similar)
188 cd C:\Kallithea\Env\Scripts (or similar)
189 activate
189 activate
190
190
191 The prompt will change into "(Env) C:\\Kallithea\\Env\\Scripts" or similar
191 The prompt will change into "(Env) C:\\Kallithea\\Env\\Scripts" or similar
192 (depending of your folder structure). Then type::
192 (depending of your folder structure). Then type::
193
193
194 pip install kallithea
194 pip install kallithea
195
195
196 (long step, please wait until fully complete)
196 (long step, please wait until fully complete)
197
197
198 Some warnings will appear, don't worry as they are normal.
198 Some warnings will appear, don't worry as they are normal.
199
199
200
200
201 Step8 - Configuring Kallithea
201 Step8 - Configuring Kallithea
202 -----------------------------
202 -----------------------------
203
203
204
204
205 steps taken from http://packages.python.org/Kallithea/setup.html
205 steps taken from http://packages.python.org/Kallithea/setup.html
206
206
207 You have to use the same Visual Studio 2008 command prompt as Step7, so
207 You have to use the same Visual Studio 2008 command prompt as Step7, so
208 if you closed it reopen it following the same commands (including the
208 if you closed it reopen it following the same commands (including the
209 "activate" one). When ready, just type::
209 "activate" one). When ready, just type::
210
210
211 cd C:\Kallithea\Bin
211 cd C:\Kallithea\Bin
212 paster make-config Kallithea production.ini
212 paster make-config Kallithea production.ini
213
213
214 Then, you must edit production.ini to fit your needs (ip address, ip
214 Then, you must edit production.ini to fit your needs (network address and
215 port, mail settings, database, whatever). I recommend using NotePad++
215 port, mail settings, database, whatever). I recommend using NotePad++
216 (free) or similar text editor, as it handles well the EndOfLine
216 (free) or similar text editor, as it handles well the EndOfLine
217 character differences between Unix and Windows
217 character differences between Unix and Windows
218 (http://notepad-plus-plus.org/)
218 (http://notepad-plus-plus.org/)
219
219
220 For the sake of simplicity lets run it with the default settings. After
220 For the sake of simplicity lets run it with the default settings. After
221 your edits (if any), in the previous Command Prompt, type::
221 your edits (if any), in the previous Command Prompt, type::
222
222
223 paster setup-db production.ini
223 paster setup-db production.ini
224
224
225 (this time a NEW database will be installed, you must follow a different
225 (this time a NEW database will be installed, you must follow a different
226 step to later UPGRADE to a newer Kallithea version)
226 step to later UPGRADE to a newer Kallithea version)
227
227
228 The script will ask you for confirmation about creating a NEW database,
228 The script will ask you for confirmation about creating a NEW database,
229 answer yes (y)
229 answer yes (y)
230 The script will ask you for repository path, answer C:\\Kallithea\\Repos
230 The script will ask you for repository path, answer C:\\Kallithea\\Repos
231 (or similar)
231 (or similar)
232 The script will ask you for admin username and password, answer "admin"
232 The script will ask you for admin username and password, answer "admin"
233 + "123456" (or whatever you want)
233 + "123456" (or whatever you want)
234 The script will ask you for admin mail, answer "admin@xxxx.com" (or
234 The script will ask you for admin mail, answer "admin@xxxx.com" (or
235 whatever you want)
235 whatever you want)
236
236
237 If you make some mistake and the script does not end, don't worry, start
237 If you make some mistake and the script does not end, don't worry, start
238 it again.
238 it again.
239
239
240
240
241 Step9 - Running Kallithea
241 Step9 - Running Kallithea
242 -------------------------
242 -------------------------
243
243
244
244
245 In the previous command prompt, being in the C:\\Kallithea\\Bin folder,
245 In the previous command prompt, being in the C:\\Kallithea\\Bin folder,
246 just type::
246 just type::
247
247
248 paster serve production.ini
248 paster serve production.ini
249
249
250 Open yout web server, and go to http://127.0.0.1:5000
250 Open yout web server, and go to http://127.0.0.1:5000
251
251
252 It works!! :-)
252 It works!! :-)
253
253
254 Remark:
254 Remark:
255 If it does not work first time, just Ctrl-C the CMD process and start it
255 If it does not work first time, just Ctrl-C the CMD process and start it
256 again. Don't forget the "http://" in Internet Explorer
256 again. Don't forget the "http://" in Internet Explorer
257
257
258
258
259
259
260 What this Guide does not cover:
260 What this Guide does not cover:
261
261
262 - Installing Celery
262 - Installing Celery
263 - Running Kallithea as Windows Service. You can investigate here:
263 - Running Kallithea as Windows Service. You can investigate here:
264
264
265 - http://pypi.python.org/pypi/wsgisvc
265 - http://pypi.python.org/pypi/wsgisvc
266 - http://ryrobes.com/python/running-python-scripts-as-a-windows-service/
266 - http://ryrobes.com/python/running-python-scripts-as-a-windows-service/
267 - http://wiki.pylonshq.com/display/pylonscookbook/How+to+run+Pylons+as+a+Windows+service
267 - http://wiki.pylonshq.com/display/pylonscookbook/How+to+run+Pylons+as+a+Windows+service
268
268
269 - Using Apache. You can investigate here:
269 - Using Apache. You can investigate here:
270
270
271 - https://groups.google.com/group/rhodecode/msg/c433074e813ffdc4
271 - https://groups.google.com/group/rhodecode/msg/c433074e813ffdc4
272
272
273
273
274 Upgrading
274 Upgrading
275 :::::::::
275 :::::::::
276
276
277 Stop running Kallithea
277 Stop running Kallithea
278 Open a CommandPrompt like in Step7 (VS2008 path + activate) and type::
278 Open a CommandPrompt like in Step7 (VS2008 path + activate) and type::
279
279
280 easy_install -U kallithea
280 easy_install -U kallithea
281 cd \Kallithea\Bin
281 cd \Kallithea\Bin
282
282
283 { backup your production.ini file now} ::
283 { backup your production.ini file now} ::
284
284
285 paster make-config Kallithea production.ini
285 paster make-config Kallithea production.ini
286
286
287 (check changes and update your production.ini accordingly) ::
287 (check changes and update your production.ini accordingly) ::
288
288
289 paster upgrade-db production.ini (update database)
289 paster upgrade-db production.ini (update database)
290
290
291 Full steps in http://packages.python.org/Kallithea/upgrade.html
291 Full steps in http://packages.python.org/Kallithea/upgrade.html
@@ -1,506 +1,506 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.users
15 kallithea.controllers.admin.users
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Users crud controller for pylons
18 Users crud controller for pylons
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 4, 2010
22 :created_on: Apr 4, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30 import formencode
30 import formencode
31
31
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from pylons import request, tmpl_context as c, url, config
33 from pylons import request, tmpl_context as c, url, config
34 from pylons.controllers.util import redirect
34 from pylons.controllers.util import redirect
35 from pylons.i18n.translation import _
35 from pylons.i18n.translation import _
36 from sqlalchemy.sql.expression import func
36 from sqlalchemy.sql.expression import func
37
37
38 import kallithea
38 import kallithea
39 from kallithea.lib.exceptions import DefaultUserException, \
39 from kallithea.lib.exceptions import DefaultUserException, \
40 UserOwnsReposException, UserCreationError
40 UserOwnsReposException, UserCreationError
41 from kallithea.lib import helpers as h
41 from kallithea.lib import helpers as h
42 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator, \
42 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator, \
43 AuthUser, generate_api_key
43 AuthUser, generate_api_key
44 import kallithea.lib.auth_modules.auth_internal
44 import kallithea.lib.auth_modules.auth_internal
45 from kallithea.lib import auth_modules
45 from kallithea.lib import auth_modules
46 from kallithea.lib.base import BaseController, render
46 from kallithea.lib.base import BaseController, render
47 from kallithea.model.api_key import ApiKeyModel
47 from kallithea.model.api_key import ApiKeyModel
48
48
49 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
49 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
50 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
50 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
51 from kallithea.model.user import UserModel
51 from kallithea.model.user import UserModel
52 from kallithea.model.meta import Session
52 from kallithea.model.meta import Session
53 from kallithea.lib.utils import action_logger
53 from kallithea.lib.utils import action_logger
54 from kallithea.lib.compat import json
54 from kallithea.lib.compat import json
55 from kallithea.lib.utils2 import datetime_to_time, safe_int
55 from kallithea.lib.utils2 import datetime_to_time, safe_int
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class UsersController(BaseController):
60 class UsersController(BaseController):
61 """REST Controller styled on the Atom Publishing Protocol"""
61 """REST Controller styled on the Atom Publishing Protocol"""
62
62
63 @LoginRequired()
63 @LoginRequired()
64 @HasPermissionAllDecorator('hg.admin')
64 @HasPermissionAllDecorator('hg.admin')
65 def __before__(self):
65 def __before__(self):
66 super(UsersController, self).__before__()
66 super(UsersController, self).__before__()
67 c.available_permissions = config['available_permissions']
67 c.available_permissions = config['available_permissions']
68 c.EXTERN_TYPE_INTERNAL = kallithea.EXTERN_TYPE_INTERNAL
68 c.EXTERN_TYPE_INTERNAL = kallithea.EXTERN_TYPE_INTERNAL
69
69
70 def index(self, format='html'):
70 def index(self, format='html'):
71 """GET /users: All items in the collection"""
71 """GET /users: All items in the collection"""
72 # url('users')
72 # url('users')
73
73
74 c.users_list = User.query().order_by(User.username)\
74 c.users_list = User.query().order_by(User.username)\
75 .filter(User.username != User.DEFAULT_USER)\
75 .filter(User.username != User.DEFAULT_USER)\
76 .order_by(func.lower(User.username))\
76 .order_by(func.lower(User.username))\
77 .all()
77 .all()
78
78
79 users_data = []
79 users_data = []
80 total_records = len(c.users_list)
80 total_records = len(c.users_list)
81 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
81 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
82 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
82 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
83
83
84 grav_tmpl = '<div class="gravatar">%s</div>'
84 grav_tmpl = '<div class="gravatar">%s</div>'
85
85
86 username = lambda user_id, username: (
86 username = lambda user_id, username: (
87 template.get_def("user_name")
87 template.get_def("user_name")
88 .render(user_id, username, _=_, h=h, c=c))
88 .render(user_id, username, _=_, h=h, c=c))
89
89
90 user_actions = lambda user_id, username: (
90 user_actions = lambda user_id, username: (
91 template.get_def("user_actions")
91 template.get_def("user_actions")
92 .render(user_id, username, _=_, h=h, c=c))
92 .render(user_id, username, _=_, h=h, c=c))
93
93
94 for user in c.users_list:
94 for user in c.users_list:
95 users_data.append({
95 users_data.append({
96 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
96 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
97 "raw_name": user.username,
97 "raw_name": user.username,
98 "username": username(user.user_id, user.username),
98 "username": username(user.user_id, user.username),
99 "firstname": h.escape(user.name),
99 "firstname": h.escape(user.name),
100 "lastname": h.escape(user.lastname),
100 "lastname": h.escape(user.lastname),
101 "last_login": h.fmt_date(user.last_login),
101 "last_login": h.fmt_date(user.last_login),
102 "last_login_raw": datetime_to_time(user.last_login),
102 "last_login_raw": datetime_to_time(user.last_login),
103 "active": h.boolicon(user.active),
103 "active": h.boolicon(user.active),
104 "admin": h.boolicon(user.admin),
104 "admin": h.boolicon(user.admin),
105 "extern_type": user.extern_type,
105 "extern_type": user.extern_type,
106 "extern_name": user.extern_name,
106 "extern_name": user.extern_name,
107 "action": user_actions(user.user_id, user.username),
107 "action": user_actions(user.user_id, user.username),
108 })
108 })
109
109
110 c.data = json.dumps({
110 c.data = json.dumps({
111 "totalRecords": total_records,
111 "totalRecords": total_records,
112 "startIndex": 0,
112 "startIndex": 0,
113 "sort": None,
113 "sort": None,
114 "dir": "asc",
114 "dir": "asc",
115 "records": users_data
115 "records": users_data
116 })
116 })
117
117
118 return render('admin/users/users.html')
118 return render('admin/users/users.html')
119
119
120 def create(self):
120 def create(self):
121 """POST /users: Create a new item"""
121 """POST /users: Create a new item"""
122 # url('users')
122 # url('users')
123 c.default_extern_type = auth_modules.auth_internal.KallitheaAuthPlugin.name
123 c.default_extern_type = auth_modules.auth_internal.KallitheaAuthPlugin.name
124 user_model = UserModel()
124 user_model = UserModel()
125 user_form = UserForm()()
125 user_form = UserForm()()
126 try:
126 try:
127 form_result = user_form.to_python(dict(request.POST))
127 form_result = user_form.to_python(dict(request.POST))
128 user = user_model.create(form_result)
128 user = user_model.create(form_result)
129 usr = form_result['username']
129 usr = form_result['username']
130 action_logger(self.authuser, 'admin_created_user:%s' % usr,
130 action_logger(self.authuser, 'admin_created_user:%s' % usr,
131 None, self.ip_addr, self.sa)
131 None, self.ip_addr, self.sa)
132 h.flash(h.literal(_('Created user %s') % h.link_to(h.escape(usr), url('edit_user', id=user.user_id))),
132 h.flash(h.literal(_('Created user %s') % h.link_to(h.escape(usr), url('edit_user', id=user.user_id))),
133 category='success')
133 category='success')
134 Session().commit()
134 Session().commit()
135 except formencode.Invalid, errors:
135 except formencode.Invalid, errors:
136 return htmlfill.render(
136 return htmlfill.render(
137 render('admin/users/user_add.html'),
137 render('admin/users/user_add.html'),
138 defaults=errors.value,
138 defaults=errors.value,
139 errors=errors.error_dict or {},
139 errors=errors.error_dict or {},
140 prefix_error=False,
140 prefix_error=False,
141 encoding="UTF-8",
141 encoding="UTF-8",
142 force_defaults=False)
142 force_defaults=False)
143 except UserCreationError, e:
143 except UserCreationError, e:
144 h.flash(e, 'error')
144 h.flash(e, 'error')
145 except Exception:
145 except Exception:
146 log.error(traceback.format_exc())
146 log.error(traceback.format_exc())
147 h.flash(_('Error occurred during creation of user %s') \
147 h.flash(_('Error occurred during creation of user %s') \
148 % request.POST.get('username'), category='error')
148 % request.POST.get('username'), category='error')
149 return redirect(url('users'))
149 return redirect(url('users'))
150
150
151 def new(self, format='html'):
151 def new(self, format='html'):
152 """GET /users/new: Form to create a new item"""
152 """GET /users/new: Form to create a new item"""
153 # url('new_user')
153 # url('new_user')
154 c.default_extern_type = auth_modules.auth_internal.KallitheaAuthPlugin.name
154 c.default_extern_type = auth_modules.auth_internal.KallitheaAuthPlugin.name
155 return render('admin/users/user_add.html')
155 return render('admin/users/user_add.html')
156
156
157 def update(self, id):
157 def update(self, id):
158 """PUT /users/id: Update an existing item"""
158 """PUT /users/id: Update an existing item"""
159 # Forms posted to this method should contain a hidden field:
159 # Forms posted to this method should contain a hidden field:
160 # <input type="hidden" name="_method" value="PUT" />
160 # <input type="hidden" name="_method" value="PUT" />
161 # Or using helpers:
161 # Or using helpers:
162 # h.form(url('update_user', id=ID),
162 # h.form(url('update_user', id=ID),
163 # method='put')
163 # method='put')
164 # url('user', id=ID)
164 # url('user', id=ID)
165 c.active = 'profile'
165 c.active = 'profile'
166 user_model = UserModel()
166 user_model = UserModel()
167 c.user = user_model.get(id)
167 c.user = user_model.get(id)
168 c.extern_type = c.user.extern_type
168 c.extern_type = c.user.extern_type
169 c.extern_name = c.user.extern_name
169 c.extern_name = c.user.extern_name
170 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
170 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
171 _form = UserForm(edit=True, old_data={'user_id': id,
171 _form = UserForm(edit=True, old_data={'user_id': id,
172 'email': c.user.email})()
172 'email': c.user.email})()
173 form_result = {}
173 form_result = {}
174 try:
174 try:
175 form_result = _form.to_python(dict(request.POST))
175 form_result = _form.to_python(dict(request.POST))
176 skip_attrs = ['extern_type', 'extern_name']
176 skip_attrs = ['extern_type', 'extern_name']
177 #TODO: plugin should define if username can be updated
177 #TODO: plugin should define if username can be updated
178 if c.extern_type != kallithea.EXTERN_TYPE_INTERNAL:
178 if c.extern_type != kallithea.EXTERN_TYPE_INTERNAL:
179 # forbid updating username for external accounts
179 # forbid updating username for external accounts
180 skip_attrs.append('username')
180 skip_attrs.append('username')
181
181
182 user_model.update(id, form_result, skip_attrs=skip_attrs)
182 user_model.update(id, form_result, skip_attrs=skip_attrs)
183 usr = form_result['username']
183 usr = form_result['username']
184 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
184 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
185 None, self.ip_addr, self.sa)
185 None, self.ip_addr, self.sa)
186 h.flash(_('User updated successfully'), category='success')
186 h.flash(_('User updated successfully'), category='success')
187 Session().commit()
187 Session().commit()
188 except formencode.Invalid, errors:
188 except formencode.Invalid, errors:
189 defaults = errors.value
189 defaults = errors.value
190 e = errors.error_dict or {}
190 e = errors.error_dict or {}
191 defaults.update({
191 defaults.update({
192 'create_repo_perm': user_model.has_perm(id,
192 'create_repo_perm': user_model.has_perm(id,
193 'hg.create.repository'),
193 'hg.create.repository'),
194 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
194 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
195 '_method': 'put'
195 '_method': 'put'
196 })
196 })
197 return htmlfill.render(
197 return htmlfill.render(
198 render('admin/users/user_edit.html'),
198 render('admin/users/user_edit.html'),
199 defaults=defaults,
199 defaults=defaults,
200 errors=e,
200 errors=e,
201 prefix_error=False,
201 prefix_error=False,
202 encoding="UTF-8",
202 encoding="UTF-8",
203 force_defaults=False)
203 force_defaults=False)
204 except Exception:
204 except Exception:
205 log.error(traceback.format_exc())
205 log.error(traceback.format_exc())
206 h.flash(_('Error occurred during update of user %s') \
206 h.flash(_('Error occurred during update of user %s') \
207 % form_result.get('username'), category='error')
207 % form_result.get('username'), category='error')
208 return redirect(url('edit_user', id=id))
208 return redirect(url('edit_user', id=id))
209
209
210 def delete(self, id):
210 def delete(self, id):
211 """DELETE /users/id: Delete an existing item"""
211 """DELETE /users/id: Delete an existing item"""
212 # Forms posted to this method should contain a hidden field:
212 # Forms posted to this method should contain a hidden field:
213 # <input type="hidden" name="_method" value="DELETE" />
213 # <input type="hidden" name="_method" value="DELETE" />
214 # Or using helpers:
214 # Or using helpers:
215 # h.form(url('delete_user', id=ID),
215 # h.form(url('delete_user', id=ID),
216 # method='delete')
216 # method='delete')
217 # url('user', id=ID)
217 # url('user', id=ID)
218 usr = User.get_or_404(id)
218 usr = User.get_or_404(id)
219 try:
219 try:
220 UserModel().delete(usr)
220 UserModel().delete(usr)
221 Session().commit()
221 Session().commit()
222 h.flash(_('Successfully deleted user'), category='success')
222 h.flash(_('Successfully deleted user'), category='success')
223 except (UserOwnsReposException, DefaultUserException), e:
223 except (UserOwnsReposException, DefaultUserException), e:
224 h.flash(e, category='warning')
224 h.flash(e, category='warning')
225 except Exception:
225 except Exception:
226 log.error(traceback.format_exc())
226 log.error(traceback.format_exc())
227 h.flash(_('An error occurred during deletion of user'),
227 h.flash(_('An error occurred during deletion of user'),
228 category='error')
228 category='error')
229 return redirect(url('users'))
229 return redirect(url('users'))
230
230
231 def show(self, id, format='html'):
231 def show(self, id, format='html'):
232 """GET /users/id: Show a specific item"""
232 """GET /users/id: Show a specific item"""
233 # url('user', id=ID)
233 # url('user', id=ID)
234 User.get_or_404(-1)
234 User.get_or_404(-1)
235
235
236 def edit(self, id, format='html'):
236 def edit(self, id, format='html'):
237 """GET /users/id/edit: Form to edit an existing item"""
237 """GET /users/id/edit: Form to edit an existing item"""
238 # url('edit_user', id=ID)
238 # url('edit_user', id=ID)
239 c.user = User.get_or_404(id)
239 c.user = User.get_or_404(id)
240 if c.user.username == User.DEFAULT_USER:
240 if c.user.username == User.DEFAULT_USER:
241 h.flash(_("You can't edit this user"), category='warning')
241 h.flash(_("You can't edit this user"), category='warning')
242 return redirect(url('users'))
242 return redirect(url('users'))
243
243
244 c.active = 'profile'
244 c.active = 'profile'
245 c.extern_type = c.user.extern_type
245 c.extern_type = c.user.extern_type
246 c.extern_name = c.user.extern_name
246 c.extern_name = c.user.extern_name
247 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
247 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
248
248
249 defaults = c.user.get_dict()
249 defaults = c.user.get_dict()
250 return htmlfill.render(
250 return htmlfill.render(
251 render('admin/users/user_edit.html'),
251 render('admin/users/user_edit.html'),
252 defaults=defaults,
252 defaults=defaults,
253 encoding="UTF-8",
253 encoding="UTF-8",
254 force_defaults=False)
254 force_defaults=False)
255
255
256 def edit_advanced(self, id):
256 def edit_advanced(self, id):
257 c.user = User.get_or_404(id)
257 c.user = User.get_or_404(id)
258 if c.user.username == User.DEFAULT_USER:
258 if c.user.username == User.DEFAULT_USER:
259 h.flash(_("You can't edit this user"), category='warning')
259 h.flash(_("You can't edit this user"), category='warning')
260 return redirect(url('users'))
260 return redirect(url('users'))
261
261
262 c.active = 'advanced'
262 c.active = 'advanced'
263 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
263 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
264
264
265 umodel = UserModel()
265 umodel = UserModel()
266 defaults = c.user.get_dict()
266 defaults = c.user.get_dict()
267 defaults.update({
267 defaults.update({
268 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
268 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
269 'create_user_group_perm': umodel.has_perm(c.user,
269 'create_user_group_perm': umodel.has_perm(c.user,
270 'hg.usergroup.create.true'),
270 'hg.usergroup.create.true'),
271 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
271 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
272 })
272 })
273 return htmlfill.render(
273 return htmlfill.render(
274 render('admin/users/user_edit.html'),
274 render('admin/users/user_edit.html'),
275 defaults=defaults,
275 defaults=defaults,
276 encoding="UTF-8",
276 encoding="UTF-8",
277 force_defaults=False)
277 force_defaults=False)
278
278
279 def edit_api_keys(self, id):
279 def edit_api_keys(self, id):
280 c.user = User.get_or_404(id)
280 c.user = User.get_or_404(id)
281 if c.user.username == User.DEFAULT_USER:
281 if c.user.username == User.DEFAULT_USER:
282 h.flash(_("You can't edit this user"), category='warning')
282 h.flash(_("You can't edit this user"), category='warning')
283 return redirect(url('users'))
283 return redirect(url('users'))
284
284
285 c.active = 'api_keys'
285 c.active = 'api_keys'
286 show_expired = True
286 show_expired = True
287 c.lifetime_values = [
287 c.lifetime_values = [
288 (str(-1), _('forever')),
288 (str(-1), _('forever')),
289 (str(5), _('5 minutes')),
289 (str(5), _('5 minutes')),
290 (str(60), _('1 hour')),
290 (str(60), _('1 hour')),
291 (str(60 * 24), _('1 day')),
291 (str(60 * 24), _('1 day')),
292 (str(60 * 24 * 30), _('1 month')),
292 (str(60 * 24 * 30), _('1 month')),
293 ]
293 ]
294 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
294 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
295 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
295 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
296 show_expired=show_expired)
296 show_expired=show_expired)
297 defaults = c.user.get_dict()
297 defaults = c.user.get_dict()
298 return htmlfill.render(
298 return htmlfill.render(
299 render('admin/users/user_edit.html'),
299 render('admin/users/user_edit.html'),
300 defaults=defaults,
300 defaults=defaults,
301 encoding="UTF-8",
301 encoding="UTF-8",
302 force_defaults=False)
302 force_defaults=False)
303
303
304 def add_api_key(self, id):
304 def add_api_key(self, id):
305 c.user = User.get_or_404(id)
305 c.user = User.get_or_404(id)
306 if c.user.username == User.DEFAULT_USER:
306 if c.user.username == User.DEFAULT_USER:
307 h.flash(_("You can't edit this user"), category='warning')
307 h.flash(_("You can't edit this user"), category='warning')
308 return redirect(url('users'))
308 return redirect(url('users'))
309
309
310 lifetime = safe_int(request.POST.get('lifetime'), -1)
310 lifetime = safe_int(request.POST.get('lifetime'), -1)
311 description = request.POST.get('description')
311 description = request.POST.get('description')
312 ApiKeyModel().create(c.user.user_id, description, lifetime)
312 ApiKeyModel().create(c.user.user_id, description, lifetime)
313 Session().commit()
313 Session().commit()
314 h.flash(_("API key successfully created"), category='success')
314 h.flash(_("API key successfully created"), category='success')
315 return redirect(url('edit_user_api_keys', id=c.user.user_id))
315 return redirect(url('edit_user_api_keys', id=c.user.user_id))
316
316
317 def delete_api_key(self, id):
317 def delete_api_key(self, id):
318 c.user = User.get_or_404(id)
318 c.user = User.get_or_404(id)
319 if c.user.username == User.DEFAULT_USER:
319 if c.user.username == User.DEFAULT_USER:
320 h.flash(_("You can't edit this user"), category='warning')
320 h.flash(_("You can't edit this user"), category='warning')
321 return redirect(url('users'))
321 return redirect(url('users'))
322
322
323 api_key = request.POST.get('del_api_key')
323 api_key = request.POST.get('del_api_key')
324 if request.POST.get('del_api_key_builtin'):
324 if request.POST.get('del_api_key_builtin'):
325 user = User.get(c.user.user_id)
325 user = User.get(c.user.user_id)
326 if user:
326 if user:
327 user.api_key = generate_api_key(user.username)
327 user.api_key = generate_api_key(user.username)
328 Session().add(user)
328 Session().add(user)
329 Session().commit()
329 Session().commit()
330 h.flash(_("API key successfully reset"), category='success')
330 h.flash(_("API key successfully reset"), category='success')
331 elif api_key:
331 elif api_key:
332 ApiKeyModel().delete(api_key, c.user.user_id)
332 ApiKeyModel().delete(api_key, c.user.user_id)
333 Session().commit()
333 Session().commit()
334 h.flash(_("API key successfully deleted"), category='success')
334 h.flash(_("API key successfully deleted"), category='success')
335
335
336 return redirect(url('edit_user_api_keys', id=c.user.user_id))
336 return redirect(url('edit_user_api_keys', id=c.user.user_id))
337
337
338 def update_account(self, id):
338 def update_account(self, id):
339 pass
339 pass
340
340
341 def edit_perms(self, id):
341 def edit_perms(self, id):
342 c.user = User.get_or_404(id)
342 c.user = User.get_or_404(id)
343 if c.user.username == User.DEFAULT_USER:
343 if c.user.username == User.DEFAULT_USER:
344 h.flash(_("You can't edit this user"), category='warning')
344 h.flash(_("You can't edit this user"), category='warning')
345 return redirect(url('users'))
345 return redirect(url('users'))
346
346
347 c.active = 'perms'
347 c.active = 'perms'
348 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
348 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
349
349
350 umodel = UserModel()
350 umodel = UserModel()
351 defaults = c.user.get_dict()
351 defaults = c.user.get_dict()
352 defaults.update({
352 defaults.update({
353 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
353 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
354 'create_user_group_perm': umodel.has_perm(c.user,
354 'create_user_group_perm': umodel.has_perm(c.user,
355 'hg.usergroup.create.true'),
355 'hg.usergroup.create.true'),
356 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
356 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
357 })
357 })
358 return htmlfill.render(
358 return htmlfill.render(
359 render('admin/users/user_edit.html'),
359 render('admin/users/user_edit.html'),
360 defaults=defaults,
360 defaults=defaults,
361 encoding="UTF-8",
361 encoding="UTF-8",
362 force_defaults=False)
362 force_defaults=False)
363
363
364 def update_perms(self, id):
364 def update_perms(self, id):
365 """PUT /users_perm/id: Update an existing item"""
365 """PUT /users_perm/id: Update an existing item"""
366 # url('user_perm', id=ID, method='put')
366 # url('user_perm', id=ID, method='put')
367 user = User.get_or_404(id)
367 user = User.get_or_404(id)
368
368
369 try:
369 try:
370 form = CustomDefaultPermissionsForm()()
370 form = CustomDefaultPermissionsForm()()
371 form_result = form.to_python(request.POST)
371 form_result = form.to_python(request.POST)
372
372
373 inherit_perms = form_result['inherit_default_permissions']
373 inherit_perms = form_result['inherit_default_permissions']
374 user.inherit_default_permissions = inherit_perms
374 user.inherit_default_permissions = inherit_perms
375 Session().add(user)
375 Session().add(user)
376 user_model = UserModel()
376 user_model = UserModel()
377
377
378 defs = UserToPerm.query()\
378 defs = UserToPerm.query()\
379 .filter(UserToPerm.user == user)\
379 .filter(UserToPerm.user == user)\
380 .all()
380 .all()
381 for ug in defs:
381 for ug in defs:
382 Session().delete(ug)
382 Session().delete(ug)
383
383
384 if form_result['create_repo_perm']:
384 if form_result['create_repo_perm']:
385 user_model.grant_perm(id, 'hg.create.repository')
385 user_model.grant_perm(id, 'hg.create.repository')
386 else:
386 else:
387 user_model.grant_perm(id, 'hg.create.none')
387 user_model.grant_perm(id, 'hg.create.none')
388 if form_result['create_user_group_perm']:
388 if form_result['create_user_group_perm']:
389 user_model.grant_perm(id, 'hg.usergroup.create.true')
389 user_model.grant_perm(id, 'hg.usergroup.create.true')
390 else:
390 else:
391 user_model.grant_perm(id, 'hg.usergroup.create.false')
391 user_model.grant_perm(id, 'hg.usergroup.create.false')
392 if form_result['fork_repo_perm']:
392 if form_result['fork_repo_perm']:
393 user_model.grant_perm(id, 'hg.fork.repository')
393 user_model.grant_perm(id, 'hg.fork.repository')
394 else:
394 else:
395 user_model.grant_perm(id, 'hg.fork.none')
395 user_model.grant_perm(id, 'hg.fork.none')
396 h.flash(_("Updated permissions"), category='success')
396 h.flash(_("Updated permissions"), category='success')
397 Session().commit()
397 Session().commit()
398 except Exception:
398 except Exception:
399 log.error(traceback.format_exc())
399 log.error(traceback.format_exc())
400 h.flash(_('An error occurred during permissions saving'),
400 h.flash(_('An error occurred during permissions saving'),
401 category='error')
401 category='error')
402 return redirect(url('edit_user_perms', id=id))
402 return redirect(url('edit_user_perms', id=id))
403
403
404 def edit_emails(self, id):
404 def edit_emails(self, id):
405 c.user = User.get_or_404(id)
405 c.user = User.get_or_404(id)
406 if c.user.username == User.DEFAULT_USER:
406 if c.user.username == User.DEFAULT_USER:
407 h.flash(_("You can't edit this user"), category='warning')
407 h.flash(_("You can't edit this user"), category='warning')
408 return redirect(url('users'))
408 return redirect(url('users'))
409
409
410 c.active = 'emails'
410 c.active = 'emails'
411 c.user_email_map = UserEmailMap.query()\
411 c.user_email_map = UserEmailMap.query()\
412 .filter(UserEmailMap.user == c.user).all()
412 .filter(UserEmailMap.user == c.user).all()
413
413
414 defaults = c.user.get_dict()
414 defaults = c.user.get_dict()
415 return htmlfill.render(
415 return htmlfill.render(
416 render('admin/users/user_edit.html'),
416 render('admin/users/user_edit.html'),
417 defaults=defaults,
417 defaults=defaults,
418 encoding="UTF-8",
418 encoding="UTF-8",
419 force_defaults=False)
419 force_defaults=False)
420
420
421 def add_email(self, id):
421 def add_email(self, id):
422 """POST /user_emails:Add an existing item"""
422 """POST /user_emails:Add an existing item"""
423 # url('user_emails', id=ID, method='put')
423 # url('user_emails', id=ID, method='put')
424
424
425 email = request.POST.get('new_email')
425 email = request.POST.get('new_email')
426 user_model = UserModel()
426 user_model = UserModel()
427
427
428 try:
428 try:
429 user_model.add_extra_email(id, email)
429 user_model.add_extra_email(id, email)
430 Session().commit()
430 Session().commit()
431 h.flash(_("Added email %s to user") % email, category='success')
431 h.flash(_("Added email %s to user") % email, category='success')
432 except formencode.Invalid, error:
432 except formencode.Invalid, error:
433 msg = error.error_dict['email']
433 msg = error.error_dict['email']
434 h.flash(msg, category='error')
434 h.flash(msg, category='error')
435 except Exception:
435 except Exception:
436 log.error(traceback.format_exc())
436 log.error(traceback.format_exc())
437 h.flash(_('An error occurred during email saving'),
437 h.flash(_('An error occurred during email saving'),
438 category='error')
438 category='error')
439 return redirect(url('edit_user_emails', id=id))
439 return redirect(url('edit_user_emails', id=id))
440
440
441 def delete_email(self, id):
441 def delete_email(self, id):
442 """DELETE /user_emails_delete/id: Delete an existing item"""
442 """DELETE /user_emails_delete/id: Delete an existing item"""
443 # url('user_emails_delete', id=ID, method='delete')
443 # url('user_emails_delete', id=ID, method='delete')
444 email_id = request.POST.get('del_email_id')
444 email_id = request.POST.get('del_email_id')
445 user_model = UserModel()
445 user_model = UserModel()
446 user_model.delete_extra_email(id, email_id)
446 user_model.delete_extra_email(id, email_id)
447 Session().commit()
447 Session().commit()
448 h.flash(_("Removed email from user"), category='success')
448 h.flash(_("Removed email from user"), category='success')
449 return redirect(url('edit_user_emails', id=id))
449 return redirect(url('edit_user_emails', id=id))
450
450
451 def edit_ips(self, id):
451 def edit_ips(self, id):
452 c.user = User.get_or_404(id)
452 c.user = User.get_or_404(id)
453 if c.user.username == User.DEFAULT_USER:
453 if c.user.username == User.DEFAULT_USER:
454 h.flash(_("You can't edit this user"), category='warning')
454 h.flash(_("You can't edit this user"), category='warning')
455 return redirect(url('users'))
455 return redirect(url('users'))
456
456
457 c.active = 'ips'
457 c.active = 'ips'
458 c.user_ip_map = UserIpMap.query()\
458 c.user_ip_map = UserIpMap.query()\
459 .filter(UserIpMap.user == c.user).all()
459 .filter(UserIpMap.user == c.user).all()
460
460
461 c.inherit_default_ips = c.user.inherit_default_permissions
461 c.inherit_default_ips = c.user.inherit_default_permissions
462 c.default_user_ip_map = UserIpMap.query()\
462 c.default_user_ip_map = UserIpMap.query()\
463 .filter(UserIpMap.user == User.get_default_user()).all()
463 .filter(UserIpMap.user == User.get_default_user()).all()
464
464
465 defaults = c.user.get_dict()
465 defaults = c.user.get_dict()
466 return htmlfill.render(
466 return htmlfill.render(
467 render('admin/users/user_edit.html'),
467 render('admin/users/user_edit.html'),
468 defaults=defaults,
468 defaults=defaults,
469 encoding="UTF-8",
469 encoding="UTF-8",
470 force_defaults=False)
470 force_defaults=False)
471
471
472 def add_ip(self, id):
472 def add_ip(self, id):
473 """POST /user_ips:Add an existing item"""
473 """POST /user_ips:Add an existing item"""
474 # url('user_ips', id=ID, method='put')
474 # url('user_ips', id=ID, method='put')
475
475
476 ip = request.POST.get('new_ip')
476 ip = request.POST.get('new_ip')
477 user_model = UserModel()
477 user_model = UserModel()
478
478
479 try:
479 try:
480 user_model.add_extra_ip(id, ip)
480 user_model.add_extra_ip(id, ip)
481 Session().commit()
481 Session().commit()
482 h.flash(_("Added ip %s to user whitelist") % ip, category='success')
482 h.flash(_("Added ip %s to user whitelist") % ip, category='success')
483 except formencode.Invalid, error:
483 except formencode.Invalid, error:
484 msg = error.error_dict['ip']
484 msg = error.error_dict['ip']
485 h.flash(msg, category='error')
485 h.flash(msg, category='error')
486 except Exception:
486 except Exception:
487 log.error(traceback.format_exc())
487 log.error(traceback.format_exc())
488 h.flash(_('An error occurred during ip saving'),
488 h.flash(_('An error occurred during ip saving'),
489 category='error')
489 category='error')
490
490
491 if 'default_user' in request.POST:
491 if 'default_user' in request.POST:
492 return redirect(url('admin_permissions_ips'))
492 return redirect(url('admin_permissions_ips'))
493 return redirect(url('edit_user_ips', id=id))
493 return redirect(url('edit_user_ips', id=id))
494
494
495 def delete_ip(self, id):
495 def delete_ip(self, id):
496 """DELETE /user_ips_delete/id: Delete an existing item"""
496 """DELETE /user_ips_delete/id: Delete an existing item"""
497 # url('user_ips_delete', id=ID, method='delete')
497 # url('user_ips_delete', id=ID, method='delete')
498 ip_id = request.POST.get('del_ip_id')
498 ip_id = request.POST.get('del_ip_id')
499 user_model = UserModel()
499 user_model = UserModel()
500 user_model.delete_extra_ip(id, ip_id)
500 user_model.delete_extra_ip(id, ip_id)
501 Session().commit()
501 Session().commit()
502 h.flash(_("Removed ip address from user whitelist"), category='success')
502 h.flash(_("Removed IP address from user whitelist"), category='success')
503
503
504 if 'default_user' in request.POST:
504 if 'default_user' in request.POST:
505 return redirect(url('admin_permissions_ips'))
505 return redirect(url('admin_permissions_ips'))
506 return redirect(url('edit_user_ips', id=id))
506 return redirect(url('edit_user_ips', id=id))
@@ -1,98 +1,98 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 import urllib
2 import urllib
3 import urllib2
3 import urllib2
4
4
5 API_SSL_SERVER = "https://www.google.com/recaptcha/api"
5 API_SSL_SERVER = "https://www.google.com/recaptcha/api"
6 API_SERVER = "http://www.google.com/recaptcha/api"
6 API_SERVER = "http://www.google.com/recaptcha/api"
7 VERIFY_SERVER = "www.google.com"
7 VERIFY_SERVER = "www.google.com"
8
8
9
9
10 class RecaptchaResponse(object):
10 class RecaptchaResponse(object):
11 def __init__(self, is_valid, error_code=None):
11 def __init__(self, is_valid, error_code=None):
12 self.is_valid = is_valid
12 self.is_valid = is_valid
13 self.error_code = error_code
13 self.error_code = error_code
14
14
15 def __repr__(self):
15 def __repr__(self):
16 return '<RecaptchaResponse:%s>' % (self.is_valid)
16 return '<RecaptchaResponse:%s>' % (self.is_valid)
17
17
18
18
19 def displayhtml(public_key, use_ssl=False, error=None):
19 def displayhtml(public_key, use_ssl=False, error=None):
20 """Gets the HTML to display for reCAPTCHA
20 """Gets the HTML to display for reCAPTCHA
21
21
22 public_key -- The public api key
22 public_key -- The public api key
23 use_ssl -- Should the request be sent over ssl?
23 use_ssl -- Should the request be sent over ssl?
24 error -- An error message to display (from RecaptchaResponse.error_code)"""
24 error -- An error message to display (from RecaptchaResponse.error_code)"""
25
25
26 error_param = ''
26 error_param = ''
27 if error:
27 if error:
28 error_param = '&error=%s' % error
28 error_param = '&error=%s' % error
29
29
30 if use_ssl:
30 if use_ssl:
31 server = API_SSL_SERVER
31 server = API_SSL_SERVER
32 else:
32 else:
33 server = API_SERVER
33 server = API_SERVER
34
34
35 return """<script type="text/javascript" src="%(ApiServer)s/challenge?k=%(PublicKey)s%(ErrorParam)s"></script>
35 return """<script type="text/javascript" src="%(ApiServer)s/challenge?k=%(PublicKey)s%(ErrorParam)s"></script>
36
36
37 <noscript>
37 <noscript>
38 <iframe src="%(ApiServer)s/noscript?k=%(PublicKey)s%(ErrorParam)s" height="300" width="500" frameborder="0"></iframe><br />
38 <iframe src="%(ApiServer)s/noscript?k=%(PublicKey)s%(ErrorParam)s" height="300" width="500" frameborder="0"></iframe><br />
39 <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
39 <textarea name="recaptcha_challenge_field" rows="3" cols="40"></textarea>
40 <input type='hidden' name='recaptcha_response_field' value='manual_challenge' />
40 <input type='hidden' name='recaptcha_response_field' value='manual_challenge' />
41 </noscript>
41 </noscript>
42 """ % {
42 """ % {
43 'ApiServer': server,
43 'ApiServer': server,
44 'PublicKey': public_key,
44 'PublicKey': public_key,
45 'ErrorParam': error_param,
45 'ErrorParam': error_param,
46 }
46 }
47
47
48
48
49 def submit(recaptcha_challenge_field, recaptcha_response_field, private_key,
49 def submit(recaptcha_challenge_field, recaptcha_response_field, private_key,
50 remoteip):
50 remoteip):
51 """
51 """
52 Submits a reCAPTCHA request for verification. Returns RecaptchaResponse
52 Submits a reCAPTCHA request for verification. Returns RecaptchaResponse
53 for the request
53 for the request
54
54
55 recaptcha_challenge_field -- The value of recaptcha_challenge_field from the form
55 recaptcha_challenge_field -- The value of recaptcha_challenge_field from the form
56 recaptcha_response_field -- The value of recaptcha_response_field from the form
56 recaptcha_response_field -- The value of recaptcha_response_field from the form
57 private_key -- your reCAPTCHA private key
57 private_key -- your reCAPTCHA private key
58 remoteip -- the user's ip address
58 remoteip -- the user's IP address
59 """
59 """
60
60
61 if not (recaptcha_response_field and recaptcha_challenge_field and
61 if not (recaptcha_response_field and recaptcha_challenge_field and
62 len(recaptcha_response_field) and len(
62 len(recaptcha_response_field) and len(
63 recaptcha_challenge_field)):
63 recaptcha_challenge_field)):
64 return RecaptchaResponse(is_valid=False,
64 return RecaptchaResponse(is_valid=False,
65 error_code='incorrect-captcha-sol')
65 error_code='incorrect-captcha-sol')
66
66
67 def encode_if_necessary(s):
67 def encode_if_necessary(s):
68 if isinstance(s, unicode):
68 if isinstance(s, unicode):
69 return s.encode('utf-8')
69 return s.encode('utf-8')
70 return s
70 return s
71
71
72 params = urllib.urlencode({
72 params = urllib.urlencode({
73 'privatekey': encode_if_necessary(private_key),
73 'privatekey': encode_if_necessary(private_key),
74 'remoteip': encode_if_necessary(remoteip),
74 'remoteip': encode_if_necessary(remoteip),
75 'challenge': encode_if_necessary(recaptcha_challenge_field),
75 'challenge': encode_if_necessary(recaptcha_challenge_field),
76 'response': encode_if_necessary(recaptcha_response_field),
76 'response': encode_if_necessary(recaptcha_response_field),
77 })
77 })
78
78
79 request = urllib2.Request(
79 request = urllib2.Request(
80 url="http://%s/recaptcha/api/verify" % VERIFY_SERVER,
80 url="http://%s/recaptcha/api/verify" % VERIFY_SERVER,
81 data=params,
81 data=params,
82 headers={
82 headers={
83 "Content-type": "application/x-www-form-urlencoded",
83 "Content-type": "application/x-www-form-urlencoded",
84 "User-agent": "reCAPTCHA Python"
84 "User-agent": "reCAPTCHA Python"
85 }
85 }
86 )
86 )
87
87
88 httpresp = urllib2.urlopen(request)
88 httpresp = urllib2.urlopen(request)
89
89
90 return_values = httpresp.read().splitlines()
90 return_values = httpresp.read().splitlines()
91 httpresp.close()
91 httpresp.close()
92
92
93 return_code = return_values[0]
93 return_code = return_values[0]
94
94
95 if return_code == "true":
95 if return_code == "true":
96 return RecaptchaResponse(is_valid=True)
96 return RecaptchaResponse(is_valid=True)
97 else:
97 else:
98 return RecaptchaResponse(is_valid=False, error_code=return_values[1])
98 return RecaptchaResponse(is_valid=False, error_code=return_values[1])
@@ -1,872 +1,872 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.utils
15 kallithea.lib.utils
16 ~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~
17
17
18 Utilities library for Kallithea
18 Utilities library for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 18, 2010
22 :created_on: Apr 18, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import os
28 import os
29 import re
29 import re
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import traceback
32 import traceback
33 import paste
33 import paste
34 import beaker
34 import beaker
35 import tarfile
35 import tarfile
36 import shutil
36 import shutil
37 import decorator
37 import decorator
38 import warnings
38 import warnings
39 from os.path import abspath
39 from os.path import abspath
40 from os.path import dirname as dn, join as jn
40 from os.path import dirname as dn, join as jn
41
41
42 from paste.script.command import Command, BadCommand
42 from paste.script.command import Command, BadCommand
43
43
44 from webhelpers.text import collapse, remove_formatting, strip_tags
44 from webhelpers.text import collapse, remove_formatting, strip_tags
45 from beaker.cache import _cache_decorate
45 from beaker.cache import _cache_decorate
46
46
47 from kallithea import BRAND
47 from kallithea import BRAND
48
48
49 from kallithea.lib.vcs.utils.hgcompat import ui, config
49 from kallithea.lib.vcs.utils.hgcompat import ui, config
50 from kallithea.lib.vcs.utils.helpers import get_scm
50 from kallithea.lib.vcs.utils.helpers import get_scm
51 from kallithea.lib.vcs.exceptions import VCSError
51 from kallithea.lib.vcs.exceptions import VCSError
52
52
53 from kallithea.lib.caching_query import FromCache
53 from kallithea.lib.caching_query import FromCache
54
54
55 from kallithea.model import meta
55 from kallithea.model import meta
56 from kallithea.model.db import Repository, User, Ui, \
56 from kallithea.model.db import Repository, User, Ui, \
57 UserLog, RepoGroup, Setting, CacheInvalidation, UserGroup
57 UserLog, RepoGroup, Setting, CacheInvalidation, UserGroup
58 from kallithea.model.meta import Session
58 from kallithea.model.meta import Session
59 from kallithea.model.repo_group import RepoGroupModel
59 from kallithea.model.repo_group import RepoGroupModel
60 from kallithea.lib.utils2 import safe_str, safe_unicode, get_current_authuser
60 from kallithea.lib.utils2 import safe_str, safe_unicode, get_current_authuser
61 from kallithea.lib.vcs.utils.fakemod import create_module
61 from kallithea.lib.vcs.utils.fakemod import create_module
62
62
63 log = logging.getLogger(__name__)
63 log = logging.getLogger(__name__)
64
64
65 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
65 REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}_.*')
66
66
67
67
68 def recursive_replace(str_, replace=' '):
68 def recursive_replace(str_, replace=' '):
69 """
69 """
70 Recursive replace of given sign to just one instance
70 Recursive replace of given sign to just one instance
71
71
72 :param str_: given string
72 :param str_: given string
73 :param replace: char to find and replace multiple instances
73 :param replace: char to find and replace multiple instances
74
74
75 Examples::
75 Examples::
76 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
76 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
77 'Mighty-Mighty-Bo-sstones'
77 'Mighty-Mighty-Bo-sstones'
78 """
78 """
79
79
80 if str_.find(replace * 2) == -1:
80 if str_.find(replace * 2) == -1:
81 return str_
81 return str_
82 else:
82 else:
83 str_ = str_.replace(replace * 2, replace)
83 str_ = str_.replace(replace * 2, replace)
84 return recursive_replace(str_, replace)
84 return recursive_replace(str_, replace)
85
85
86
86
87 def repo_name_slug(value):
87 def repo_name_slug(value):
88 """
88 """
89 Return slug of name of repository
89 Return slug of name of repository
90 This function is called on each creation/modification
90 This function is called on each creation/modification
91 of repository to prevent bad names in repo
91 of repository to prevent bad names in repo
92 """
92 """
93
93
94 slug = remove_formatting(value)
94 slug = remove_formatting(value)
95 slug = strip_tags(slug)
95 slug = strip_tags(slug)
96
96
97 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
97 for c in """`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
98 slug = slug.replace(c, '-')
98 slug = slug.replace(c, '-')
99 slug = recursive_replace(slug, '-')
99 slug = recursive_replace(slug, '-')
100 slug = collapse(slug, '-')
100 slug = collapse(slug, '-')
101 return slug
101 return slug
102
102
103
103
104 #==============================================================================
104 #==============================================================================
105 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
105 # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
106 #==============================================================================
106 #==============================================================================
107 def get_repo_slug(request):
107 def get_repo_slug(request):
108 _repo = request.environ['pylons.routes_dict'].get('repo_name')
108 _repo = request.environ['pylons.routes_dict'].get('repo_name')
109 if _repo:
109 if _repo:
110 _repo = _repo.rstrip('/')
110 _repo = _repo.rstrip('/')
111 return _repo
111 return _repo
112
112
113
113
114 def get_repo_group_slug(request):
114 def get_repo_group_slug(request):
115 _group = request.environ['pylons.routes_dict'].get('group_name')
115 _group = request.environ['pylons.routes_dict'].get('group_name')
116 if _group:
116 if _group:
117 _group = _group.rstrip('/')
117 _group = _group.rstrip('/')
118 return _group
118 return _group
119
119
120
120
121 def get_user_group_slug(request):
121 def get_user_group_slug(request):
122 _group = request.environ['pylons.routes_dict'].get('id')
122 _group = request.environ['pylons.routes_dict'].get('id')
123 _group = UserGroup.get(_group)
123 _group = UserGroup.get(_group)
124 if _group:
124 if _group:
125 return _group.users_group_name
125 return _group.users_group_name
126 return None
126 return None
127
127
128
128
129 def _extract_id_from_repo_name(repo_name):
129 def _extract_id_from_repo_name(repo_name):
130 if repo_name.startswith('/'):
130 if repo_name.startswith('/'):
131 repo_name = repo_name.lstrip('/')
131 repo_name = repo_name.lstrip('/')
132 by_id_match = re.match(r'^_(\d{1,})', repo_name)
132 by_id_match = re.match(r'^_(\d{1,})', repo_name)
133 if by_id_match:
133 if by_id_match:
134 return by_id_match.groups()[0]
134 return by_id_match.groups()[0]
135
135
136
136
137 def get_repo_by_id(repo_name):
137 def get_repo_by_id(repo_name):
138 """
138 """
139 Extracts repo_name by id from special urls. Example url is _11/repo_name
139 Extracts repo_name by id from special urls. Example url is _11/repo_name
140
140
141 :param repo_name:
141 :param repo_name:
142 :return: repo_name if matched else None
142 :return: repo_name if matched else None
143 """
143 """
144 _repo_id = _extract_id_from_repo_name(repo_name)
144 _repo_id = _extract_id_from_repo_name(repo_name)
145 if _repo_id:
145 if _repo_id:
146 from kallithea.model.db import Repository
146 from kallithea.model.db import Repository
147 repo = Repository.get(_repo_id)
147 repo = Repository.get(_repo_id)
148 if repo:
148 if repo:
149 # TODO: return repo instead of reponame? or would that be a layering violation?
149 # TODO: return repo instead of reponame? or would that be a layering violation?
150 return repo.repo_name
150 return repo.repo_name
151 return None
151 return None
152
152
153
153
154 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
154 def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
155 """
155 """
156 Action logger for various actions made by users
156 Action logger for various actions made by users
157
157
158 :param user: user that made this action, can be a unique username string or
158 :param user: user that made this action, can be a unique username string or
159 object containing user_id attribute
159 object containing user_id attribute
160 :param action: action to log, should be on of predefined unique actions for
160 :param action: action to log, should be on of predefined unique actions for
161 easy translations
161 easy translations
162 :param repo: string name of repository or object containing repo_id,
162 :param repo: string name of repository or object containing repo_id,
163 that action was made on
163 that action was made on
164 :param ipaddr: optional ip address from what the action was made
164 :param ipaddr: optional IP address from what the action was made
165 :param sa: optional sqlalchemy session
165 :param sa: optional sqlalchemy session
166
166
167 """
167 """
168
168
169 if not sa:
169 if not sa:
170 sa = meta.Session()
170 sa = meta.Session()
171 # if we don't get explicit IP address try to get one from registered user
171 # if we don't get explicit IP address try to get one from registered user
172 # in tmpl context var
172 # in tmpl context var
173 if not ipaddr:
173 if not ipaddr:
174 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
174 ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
175
175
176 if getattr(user, 'user_id', None):
176 if getattr(user, 'user_id', None):
177 user_obj = User.get(user.user_id)
177 user_obj = User.get(user.user_id)
178 elif isinstance(user, basestring):
178 elif isinstance(user, basestring):
179 user_obj = User.get_by_username(user)
179 user_obj = User.get_by_username(user)
180 else:
180 else:
181 raise Exception('You have to provide a user object or a username')
181 raise Exception('You have to provide a user object or a username')
182
182
183 if getattr(repo, 'repo_id', None):
183 if getattr(repo, 'repo_id', None):
184 repo_obj = Repository.get(repo.repo_id)
184 repo_obj = Repository.get(repo.repo_id)
185 repo_name = repo_obj.repo_name
185 repo_name = repo_obj.repo_name
186 elif isinstance(repo, basestring):
186 elif isinstance(repo, basestring):
187 repo_name = repo.lstrip('/')
187 repo_name = repo.lstrip('/')
188 repo_obj = Repository.get_by_repo_name(repo_name)
188 repo_obj = Repository.get_by_repo_name(repo_name)
189 else:
189 else:
190 repo_obj = None
190 repo_obj = None
191 repo_name = ''
191 repo_name = ''
192
192
193 user_log = UserLog()
193 user_log = UserLog()
194 user_log.user_id = user_obj.user_id
194 user_log.user_id = user_obj.user_id
195 user_log.username = user_obj.username
195 user_log.username = user_obj.username
196 user_log.action = safe_unicode(action)
196 user_log.action = safe_unicode(action)
197
197
198 user_log.repository = repo_obj
198 user_log.repository = repo_obj
199 user_log.repository_name = repo_name
199 user_log.repository_name = repo_name
200
200
201 user_log.action_date = datetime.datetime.now()
201 user_log.action_date = datetime.datetime.now()
202 user_log.user_ip = ipaddr
202 user_log.user_ip = ipaddr
203 sa.add(user_log)
203 sa.add(user_log)
204
204
205 log.info('Logging action:%s on %s by user:%s ip:%s' %
205 log.info('Logging action:%s on %s by user:%s ip:%s' %
206 (action, safe_unicode(repo), user_obj, ipaddr))
206 (action, safe_unicode(repo), user_obj, ipaddr))
207 if commit:
207 if commit:
208 sa.commit()
208 sa.commit()
209
209
210
210
211 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
211 def get_filesystem_repos(path, recursive=False, skip_removed_repos=True):
212 """
212 """
213 Scans given path for repos and return (name,(type,path)) tuple
213 Scans given path for repos and return (name,(type,path)) tuple
214
214
215 :param path: path to scan for repositories
215 :param path: path to scan for repositories
216 :param recursive: recursive search and return names with subdirs in front
216 :param recursive: recursive search and return names with subdirs in front
217 """
217 """
218
218
219 # remove ending slash for better results
219 # remove ending slash for better results
220 path = path.rstrip(os.sep)
220 path = path.rstrip(os.sep)
221 log.debug('now scanning in %s location recursive:%s...' % (path, recursive))
221 log.debug('now scanning in %s location recursive:%s...' % (path, recursive))
222
222
223 def _get_repos(p):
223 def _get_repos(p):
224 if not os.access(p, os.R_OK) or not os.access(p, os.X_OK):
224 if not os.access(p, os.R_OK) or not os.access(p, os.X_OK):
225 log.warning('ignoring repo path without access: %s' % (p,))
225 log.warning('ignoring repo path without access: %s' % (p,))
226 return
226 return
227 if not os.access(p, os.W_OK):
227 if not os.access(p, os.W_OK):
228 log.warning('repo path without write access: %s' % (p,))
228 log.warning('repo path without write access: %s' % (p,))
229 for dirpath in os.listdir(p):
229 for dirpath in os.listdir(p):
230 if os.path.isfile(os.path.join(p, dirpath)):
230 if os.path.isfile(os.path.join(p, dirpath)):
231 continue
231 continue
232 cur_path = os.path.join(p, dirpath)
232 cur_path = os.path.join(p, dirpath)
233
233
234 # skip removed repos
234 # skip removed repos
235 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
235 if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
236 continue
236 continue
237
237
238 #skip .<somethin> dirs
238 #skip .<somethin> dirs
239 if dirpath.startswith('.'):
239 if dirpath.startswith('.'):
240 continue
240 continue
241
241
242 try:
242 try:
243 scm_info = get_scm(cur_path)
243 scm_info = get_scm(cur_path)
244 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
244 yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
245 except VCSError:
245 except VCSError:
246 if not recursive:
246 if not recursive:
247 continue
247 continue
248 #check if this dir containts other repos for recursive scan
248 #check if this dir containts other repos for recursive scan
249 rec_path = os.path.join(p, dirpath)
249 rec_path = os.path.join(p, dirpath)
250 if not os.path.islink(rec_path) and os.path.isdir(rec_path):
250 if not os.path.islink(rec_path) and os.path.isdir(rec_path):
251 for inner_scm in _get_repos(rec_path):
251 for inner_scm in _get_repos(rec_path):
252 yield inner_scm
252 yield inner_scm
253
253
254 return _get_repos(path)
254 return _get_repos(path)
255
255
256
256
257 def is_valid_repo(repo_name, base_path, scm=None):
257 def is_valid_repo(repo_name, base_path, scm=None):
258 """
258 """
259 Returns True if given path is a valid repository False otherwise.
259 Returns True if given path is a valid repository False otherwise.
260 If scm param is given also compare if given scm is the same as expected
260 If scm param is given also compare if given scm is the same as expected
261 from scm parameter
261 from scm parameter
262
262
263 :param repo_name:
263 :param repo_name:
264 :param base_path:
264 :param base_path:
265 :param scm:
265 :param scm:
266
266
267 :return True: if given path is a valid repository
267 :return True: if given path is a valid repository
268 """
268 """
269 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
269 full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
270
270
271 try:
271 try:
272 scm_ = get_scm(full_path)
272 scm_ = get_scm(full_path)
273 if scm:
273 if scm:
274 return scm_[0] == scm
274 return scm_[0] == scm
275 return True
275 return True
276 except VCSError:
276 except VCSError:
277 return False
277 return False
278
278
279
279
280 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
280 def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
281 """
281 """
282 Returns True if given path is a repository group False otherwise
282 Returns True if given path is a repository group False otherwise
283
283
284 :param repo_name:
284 :param repo_name:
285 :param base_path:
285 :param base_path:
286 """
286 """
287 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
287 full_path = os.path.join(safe_str(base_path), safe_str(repo_group_name))
288
288
289 # check if it's not a repo
289 # check if it's not a repo
290 if is_valid_repo(repo_group_name, base_path):
290 if is_valid_repo(repo_group_name, base_path):
291 return False
291 return False
292
292
293 try:
293 try:
294 # we need to check bare git repos at higher level
294 # we need to check bare git repos at higher level
295 # since we might match branches/hooks/info/objects or possible
295 # since we might match branches/hooks/info/objects or possible
296 # other things inside bare git repo
296 # other things inside bare git repo
297 get_scm(os.path.dirname(full_path))
297 get_scm(os.path.dirname(full_path))
298 return False
298 return False
299 except VCSError:
299 except VCSError:
300 pass
300 pass
301
301
302 # check if it's a valid path
302 # check if it's a valid path
303 if skip_path_check or os.path.isdir(full_path):
303 if skip_path_check or os.path.isdir(full_path):
304 return True
304 return True
305
305
306 return False
306 return False
307
307
308
308
309 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
309 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
310 while True:
310 while True:
311 ok = raw_input(prompt)
311 ok = raw_input(prompt)
312 if ok in ('y', 'ye', 'yes'):
312 if ok in ('y', 'ye', 'yes'):
313 return True
313 return True
314 if ok in ('n', 'no', 'nop', 'nope'):
314 if ok in ('n', 'no', 'nop', 'nope'):
315 return False
315 return False
316 retries = retries - 1
316 retries = retries - 1
317 if retries < 0:
317 if retries < 0:
318 raise IOError
318 raise IOError
319 print complaint
319 print complaint
320
320
321 #propagated from mercurial documentation
321 #propagated from mercurial documentation
322 ui_sections = ['alias', 'auth',
322 ui_sections = ['alias', 'auth',
323 'decode/encode', 'defaults',
323 'decode/encode', 'defaults',
324 'diff', 'email',
324 'diff', 'email',
325 'extensions', 'format',
325 'extensions', 'format',
326 'merge-patterns', 'merge-tools',
326 'merge-patterns', 'merge-tools',
327 'hooks', 'http_proxy',
327 'hooks', 'http_proxy',
328 'smtp', 'patch',
328 'smtp', 'patch',
329 'paths', 'profiling',
329 'paths', 'profiling',
330 'server', 'trusted',
330 'server', 'trusted',
331 'ui', 'web', ]
331 'ui', 'web', ]
332
332
333
333
334 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
334 def make_ui(read_from='file', path=None, checkpaths=True, clear_session=True):
335 """
335 """
336 A function that will read python rc files or database
336 A function that will read python rc files or database
337 and make an mercurial ui object from read options
337 and make an mercurial ui object from read options
338
338
339 :param path: path to mercurial config file
339 :param path: path to mercurial config file
340 :param checkpaths: check the path
340 :param checkpaths: check the path
341 :param read_from: read from 'file' or 'db'
341 :param read_from: read from 'file' or 'db'
342 """
342 """
343
343
344 baseui = ui.ui()
344 baseui = ui.ui()
345
345
346 # clean the baseui object
346 # clean the baseui object
347 baseui._ocfg = config.config()
347 baseui._ocfg = config.config()
348 baseui._ucfg = config.config()
348 baseui._ucfg = config.config()
349 baseui._tcfg = config.config()
349 baseui._tcfg = config.config()
350
350
351 if read_from == 'file':
351 if read_from == 'file':
352 if not os.path.isfile(path):
352 if not os.path.isfile(path):
353 log.debug('hgrc file is not present at %s, skipping...' % path)
353 log.debug('hgrc file is not present at %s, skipping...' % path)
354 return False
354 return False
355 log.debug('reading hgrc from %s' % path)
355 log.debug('reading hgrc from %s' % path)
356 cfg = config.config()
356 cfg = config.config()
357 cfg.read(path)
357 cfg.read(path)
358 for section in ui_sections:
358 for section in ui_sections:
359 for k, v in cfg.items(section):
359 for k, v in cfg.items(section):
360 log.debug('settings ui from file: [%s] %s=%s' % (section, k, v))
360 log.debug('settings ui from file: [%s] %s=%s' % (section, k, v))
361 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
361 baseui.setconfig(safe_str(section), safe_str(k), safe_str(v))
362
362
363 elif read_from == 'db':
363 elif read_from == 'db':
364 sa = meta.Session()
364 sa = meta.Session()
365 ret = sa.query(Ui).all()
365 ret = sa.query(Ui).all()
366
366
367 hg_ui = ret
367 hg_ui = ret
368 for ui_ in hg_ui:
368 for ui_ in hg_ui:
369 if ui_.ui_active:
369 if ui_.ui_active:
370 ui_val = safe_str(ui_.ui_value)
370 ui_val = safe_str(ui_.ui_value)
371 if ui_.ui_section == 'hooks' and BRAND != 'kallithea' and ui_val.startswith('python:' + BRAND + '.lib.hooks.'):
371 if ui_.ui_section == 'hooks' and BRAND != 'kallithea' and ui_val.startswith('python:' + BRAND + '.lib.hooks.'):
372 ui_val = ui_val.replace('python:' + BRAND + '.lib.hooks.', 'python:kallithea.lib.hooks.')
372 ui_val = ui_val.replace('python:' + BRAND + '.lib.hooks.', 'python:kallithea.lib.hooks.')
373 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
373 log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
374 ui_.ui_key, ui_val)
374 ui_.ui_key, ui_val)
375 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
375 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
376 ui_val)
376 ui_val)
377 if ui_.ui_key == 'push_ssl':
377 if ui_.ui_key == 'push_ssl':
378 # force set push_ssl requirement to False, kallithea
378 # force set push_ssl requirement to False, kallithea
379 # handles that
379 # handles that
380 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
380 baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
381 False)
381 False)
382 if clear_session:
382 if clear_session:
383 meta.Session.remove()
383 meta.Session.remove()
384
384
385 # prevent interactive questions for ssh password / passphrase
385 # prevent interactive questions for ssh password / passphrase
386 ssh = baseui.config('ui', 'ssh', default='ssh')
386 ssh = baseui.config('ui', 'ssh', default='ssh')
387 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
387 baseui.setconfig('ui', 'ssh', '%s -oBatchMode=yes -oIdentitiesOnly=yes' % ssh)
388
388
389 return baseui
389 return baseui
390
390
391
391
392 def set_app_settings(config):
392 def set_app_settings(config):
393 """
393 """
394 Updates pylons config with new settings from database
394 Updates pylons config with new settings from database
395
395
396 :param config:
396 :param config:
397 """
397 """
398 hgsettings = Setting.get_app_settings()
398 hgsettings = Setting.get_app_settings()
399
399
400 for k, v in hgsettings.items():
400 for k, v in hgsettings.items():
401 config[k] = v
401 config[k] = v
402
402
403
403
404 def set_vcs_config(config):
404 def set_vcs_config(config):
405 """
405 """
406 Patch VCS config with some Kallithea specific stuff
406 Patch VCS config with some Kallithea specific stuff
407
407
408 :param config: kallithea.CONFIG
408 :param config: kallithea.CONFIG
409 """
409 """
410 import kallithea
410 import kallithea
411 from kallithea.lib.vcs import conf
411 from kallithea.lib.vcs import conf
412 from kallithea.lib.utils2 import aslist
412 from kallithea.lib.utils2 import aslist
413 conf.settings.BACKENDS = {
413 conf.settings.BACKENDS = {
414 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
414 'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
415 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
415 'git': 'kallithea.lib.vcs.backends.git.GitRepository',
416 }
416 }
417
417
418 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
418 conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
419 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
419 conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
420 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
420 conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
421 'utf8'), sep=',')
421 'utf8'), sep=',')
422
422
423
423
424 def map_groups(path):
424 def map_groups(path):
425 """
425 """
426 Given a full path to a repository, create all nested groups that this
426 Given a full path to a repository, create all nested groups that this
427 repo is inside. This function creates parent-child relationships between
427 repo is inside. This function creates parent-child relationships between
428 groups and creates default perms for all new groups.
428 groups and creates default perms for all new groups.
429
429
430 :param paths: full path to repository
430 :param paths: full path to repository
431 """
431 """
432 sa = meta.Session()
432 sa = meta.Session()
433 groups = path.split(Repository.url_sep())
433 groups = path.split(Repository.url_sep())
434 parent = None
434 parent = None
435 group = None
435 group = None
436
436
437 # last element is repo in nested groups structure
437 # last element is repo in nested groups structure
438 groups = groups[:-1]
438 groups = groups[:-1]
439 rgm = RepoGroupModel(sa)
439 rgm = RepoGroupModel(sa)
440 owner = User.get_first_admin()
440 owner = User.get_first_admin()
441 for lvl, group_name in enumerate(groups):
441 for lvl, group_name in enumerate(groups):
442 group_name = '/'.join(groups[:lvl] + [group_name])
442 group_name = '/'.join(groups[:lvl] + [group_name])
443 group = RepoGroup.get_by_group_name(group_name)
443 group = RepoGroup.get_by_group_name(group_name)
444 desc = '%s group' % group_name
444 desc = '%s group' % group_name
445
445
446 # skip folders that are now removed repos
446 # skip folders that are now removed repos
447 if REMOVED_REPO_PAT.match(group_name):
447 if REMOVED_REPO_PAT.match(group_name):
448 break
448 break
449
449
450 if group is None:
450 if group is None:
451 log.debug('creating group level: %s group_name: %s'
451 log.debug('creating group level: %s group_name: %s'
452 % (lvl, group_name))
452 % (lvl, group_name))
453 group = RepoGroup(group_name, parent)
453 group = RepoGroup(group_name, parent)
454 group.group_description = desc
454 group.group_description = desc
455 group.user = owner
455 group.user = owner
456 sa.add(group)
456 sa.add(group)
457 perm_obj = rgm._create_default_perms(group)
457 perm_obj = rgm._create_default_perms(group)
458 sa.add(perm_obj)
458 sa.add(perm_obj)
459 sa.flush()
459 sa.flush()
460
460
461 parent = group
461 parent = group
462 return group
462 return group
463
463
464
464
465 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
465 def repo2db_mapper(initial_repo_list, remove_obsolete=False,
466 install_git_hook=False, user=None):
466 install_git_hook=False, user=None):
467 """
467 """
468 maps all repos given in initial_repo_list, non existing repositories
468 maps all repos given in initial_repo_list, non existing repositories
469 are created, if remove_obsolete is True it also check for db entries
469 are created, if remove_obsolete is True it also check for db entries
470 that are not in initial_repo_list and removes them.
470 that are not in initial_repo_list and removes them.
471
471
472 :param initial_repo_list: list of repositories found by scanning methods
472 :param initial_repo_list: list of repositories found by scanning methods
473 :param remove_obsolete: check for obsolete entries in database
473 :param remove_obsolete: check for obsolete entries in database
474 :param install_git_hook: if this is True, also check and install githook
474 :param install_git_hook: if this is True, also check and install githook
475 for a repo if missing
475 for a repo if missing
476 """
476 """
477 from kallithea.model.repo import RepoModel
477 from kallithea.model.repo import RepoModel
478 from kallithea.model.scm import ScmModel
478 from kallithea.model.scm import ScmModel
479 sa = meta.Session()
479 sa = meta.Session()
480 repo_model = RepoModel()
480 repo_model = RepoModel()
481 if user is None:
481 if user is None:
482 user = User.get_first_admin()
482 user = User.get_first_admin()
483 added = []
483 added = []
484
484
485 ##creation defaults
485 ##creation defaults
486 defs = Setting.get_default_repo_settings(strip_prefix=True)
486 defs = Setting.get_default_repo_settings(strip_prefix=True)
487 enable_statistics = defs.get('repo_enable_statistics')
487 enable_statistics = defs.get('repo_enable_statistics')
488 enable_locking = defs.get('repo_enable_locking')
488 enable_locking = defs.get('repo_enable_locking')
489 enable_downloads = defs.get('repo_enable_downloads')
489 enable_downloads = defs.get('repo_enable_downloads')
490 private = defs.get('repo_private')
490 private = defs.get('repo_private')
491
491
492 for name, repo in initial_repo_list.items():
492 for name, repo in initial_repo_list.items():
493 group = map_groups(name)
493 group = map_groups(name)
494 unicode_name = safe_unicode(name)
494 unicode_name = safe_unicode(name)
495 db_repo = repo_model.get_by_repo_name(unicode_name)
495 db_repo = repo_model.get_by_repo_name(unicode_name)
496 # found repo that is on filesystem not in Kallithea database
496 # found repo that is on filesystem not in Kallithea database
497 if not db_repo:
497 if not db_repo:
498 log.info('repository %s not found, creating now' % name)
498 log.info('repository %s not found, creating now' % name)
499 added.append(name)
499 added.append(name)
500 desc = (repo.description
500 desc = (repo.description
501 if repo.description != 'unknown'
501 if repo.description != 'unknown'
502 else '%s repository' % name)
502 else '%s repository' % name)
503
503
504 new_repo = repo_model._create_repo(
504 new_repo = repo_model._create_repo(
505 repo_name=name,
505 repo_name=name,
506 repo_type=repo.alias,
506 repo_type=repo.alias,
507 description=desc,
507 description=desc,
508 repo_group=getattr(group, 'group_id', None),
508 repo_group=getattr(group, 'group_id', None),
509 owner=user,
509 owner=user,
510 enable_locking=enable_locking,
510 enable_locking=enable_locking,
511 enable_downloads=enable_downloads,
511 enable_downloads=enable_downloads,
512 enable_statistics=enable_statistics,
512 enable_statistics=enable_statistics,
513 private=private,
513 private=private,
514 state=Repository.STATE_CREATED
514 state=Repository.STATE_CREATED
515 )
515 )
516 sa.commit()
516 sa.commit()
517 # we added that repo just now, and make sure it has githook
517 # we added that repo just now, and make sure it has githook
518 # installed, and updated server info
518 # installed, and updated server info
519 if new_repo.repo_type == 'git':
519 if new_repo.repo_type == 'git':
520 git_repo = new_repo.scm_instance
520 git_repo = new_repo.scm_instance
521 ScmModel().install_git_hook(git_repo)
521 ScmModel().install_git_hook(git_repo)
522 # update repository server-info
522 # update repository server-info
523 log.debug('Running update server info')
523 log.debug('Running update server info')
524 git_repo._update_server_info()
524 git_repo._update_server_info()
525 new_repo.update_changeset_cache()
525 new_repo.update_changeset_cache()
526 elif install_git_hook:
526 elif install_git_hook:
527 if db_repo.repo_type == 'git':
527 if db_repo.repo_type == 'git':
528 ScmModel().install_git_hook(db_repo.scm_instance)
528 ScmModel().install_git_hook(db_repo.scm_instance)
529
529
530 removed = []
530 removed = []
531 # remove from database those repositories that are not in the filesystem
531 # remove from database those repositories that are not in the filesystem
532 for repo in sa.query(Repository).all():
532 for repo in sa.query(Repository).all():
533 if repo.repo_name not in initial_repo_list.keys():
533 if repo.repo_name not in initial_repo_list.keys():
534 if remove_obsolete:
534 if remove_obsolete:
535 log.debug("Removing non-existing repository found in db `%s`" %
535 log.debug("Removing non-existing repository found in db `%s`" %
536 repo.repo_name)
536 repo.repo_name)
537 try:
537 try:
538 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
538 RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
539 sa.commit()
539 sa.commit()
540 except Exception:
540 except Exception:
541 #don't hold further removals on error
541 #don't hold further removals on error
542 log.error(traceback.format_exc())
542 log.error(traceback.format_exc())
543 sa.rollback()
543 sa.rollback()
544 removed.append(repo.repo_name)
544 removed.append(repo.repo_name)
545 return added, removed
545 return added, removed
546
546
547
547
548 # set cache regions for beaker so celery can utilise it
548 # set cache regions for beaker so celery can utilise it
549 def add_cache(settings):
549 def add_cache(settings):
550 cache_settings = {'regions': None}
550 cache_settings = {'regions': None}
551 for key in settings.keys():
551 for key in settings.keys():
552 for prefix in ['beaker.cache.', 'cache.']:
552 for prefix in ['beaker.cache.', 'cache.']:
553 if key.startswith(prefix):
553 if key.startswith(prefix):
554 name = key.split(prefix)[1].strip()
554 name = key.split(prefix)[1].strip()
555 cache_settings[name] = settings[key].strip()
555 cache_settings[name] = settings[key].strip()
556 if cache_settings['regions']:
556 if cache_settings['regions']:
557 for region in cache_settings['regions'].split(','):
557 for region in cache_settings['regions'].split(','):
558 region = region.strip()
558 region = region.strip()
559 region_settings = {}
559 region_settings = {}
560 for key, value in cache_settings.items():
560 for key, value in cache_settings.items():
561 if key.startswith(region):
561 if key.startswith(region):
562 region_settings[key.split('.')[1]] = value
562 region_settings[key.split('.')[1]] = value
563 region_settings['expire'] = int(region_settings.get('expire',
563 region_settings['expire'] = int(region_settings.get('expire',
564 60))
564 60))
565 region_settings.setdefault('lock_dir',
565 region_settings.setdefault('lock_dir',
566 cache_settings.get('lock_dir'))
566 cache_settings.get('lock_dir'))
567 region_settings.setdefault('data_dir',
567 region_settings.setdefault('data_dir',
568 cache_settings.get('data_dir'))
568 cache_settings.get('data_dir'))
569
569
570 if 'type' not in region_settings:
570 if 'type' not in region_settings:
571 region_settings['type'] = cache_settings.get('type',
571 region_settings['type'] = cache_settings.get('type',
572 'memory')
572 'memory')
573 beaker.cache.cache_regions[region] = region_settings
573 beaker.cache.cache_regions[region] = region_settings
574
574
575
575
576 def load_rcextensions(root_path):
576 def load_rcextensions(root_path):
577 import kallithea
577 import kallithea
578 from kallithea.config import conf
578 from kallithea.config import conf
579
579
580 path = os.path.join(root_path, 'rcextensions', '__init__.py')
580 path = os.path.join(root_path, 'rcextensions', '__init__.py')
581 if os.path.isfile(path):
581 if os.path.isfile(path):
582 rcext = create_module('rc', path)
582 rcext = create_module('rc', path)
583 EXT = kallithea.EXTENSIONS = rcext
583 EXT = kallithea.EXTENSIONS = rcext
584 log.debug('Found rcextensions now loading %s...' % rcext)
584 log.debug('Found rcextensions now loading %s...' % rcext)
585
585
586 # Additional mappings that are not present in the pygments lexers
586 # Additional mappings that are not present in the pygments lexers
587 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
587 conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {}))
588
588
589 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
589 #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present)
590
590
591 if getattr(EXT, 'INDEX_EXTENSIONS', []):
591 if getattr(EXT, 'INDEX_EXTENSIONS', []):
592 log.debug('settings custom INDEX_EXTENSIONS')
592 log.debug('settings custom INDEX_EXTENSIONS')
593 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
593 conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', [])
594
594
595 #ADDITIONAL MAPPINGS
595 #ADDITIONAL MAPPINGS
596 log.debug('adding extra into INDEX_EXTENSIONS')
596 log.debug('adding extra into INDEX_EXTENSIONS')
597 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
597 conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', []))
598
598
599 # auto check if the module is not missing any data, set to default if is
599 # auto check if the module is not missing any data, set to default if is
600 # this will help autoupdate new feature of rcext module
600 # this will help autoupdate new feature of rcext module
601 #from kallithea.config import rcextensions
601 #from kallithea.config import rcextensions
602 #for k in dir(rcextensions):
602 #for k in dir(rcextensions):
603 # if not k.startswith('_') and not hasattr(EXT, k):
603 # if not k.startswith('_') and not hasattr(EXT, k):
604 # setattr(EXT, k, getattr(rcextensions, k))
604 # setattr(EXT, k, getattr(rcextensions, k))
605
605
606
606
607 def get_custom_lexer(extension):
607 def get_custom_lexer(extension):
608 """
608 """
609 returns a custom lexer if it's defined in rcextensions module, or None
609 returns a custom lexer if it's defined in rcextensions module, or None
610 if there's no custom lexer defined
610 if there's no custom lexer defined
611 """
611 """
612 import kallithea
612 import kallithea
613 from pygments import lexers
613 from pygments import lexers
614 #check if we didn't define this extension as other lexer
614 #check if we didn't define this extension as other lexer
615 if kallithea.EXTENSIONS and extension in kallithea.EXTENSIONS.EXTRA_LEXERS:
615 if kallithea.EXTENSIONS and extension in kallithea.EXTENSIONS.EXTRA_LEXERS:
616 _lexer_name = kallithea.EXTENSIONS.EXTRA_LEXERS[extension]
616 _lexer_name = kallithea.EXTENSIONS.EXTRA_LEXERS[extension]
617 return lexers.get_lexer_by_name(_lexer_name)
617 return lexers.get_lexer_by_name(_lexer_name)
618
618
619
619
620 #==============================================================================
620 #==============================================================================
621 # TEST FUNCTIONS AND CREATORS
621 # TEST FUNCTIONS AND CREATORS
622 #==============================================================================
622 #==============================================================================
623 def create_test_index(repo_location, config, full_index):
623 def create_test_index(repo_location, config, full_index):
624 """
624 """
625 Makes default test index
625 Makes default test index
626
626
627 :param config: test config
627 :param config: test config
628 :param full_index:
628 :param full_index:
629 """
629 """
630
630
631 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
631 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
632 from kallithea.lib.pidlock import DaemonLock, LockHeld
632 from kallithea.lib.pidlock import DaemonLock, LockHeld
633
633
634 repo_location = repo_location
634 repo_location = repo_location
635
635
636 index_location = os.path.join(config['app_conf']['index_dir'])
636 index_location = os.path.join(config['app_conf']['index_dir'])
637 if not os.path.exists(index_location):
637 if not os.path.exists(index_location):
638 os.makedirs(index_location)
638 os.makedirs(index_location)
639
639
640 try:
640 try:
641 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
641 l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
642 WhooshIndexingDaemon(index_location=index_location,
642 WhooshIndexingDaemon(index_location=index_location,
643 repo_location=repo_location)\
643 repo_location=repo_location)\
644 .run(full_index=full_index)
644 .run(full_index=full_index)
645 l.release()
645 l.release()
646 except LockHeld:
646 except LockHeld:
647 pass
647 pass
648
648
649
649
650 def create_test_env(repos_test_path, config):
650 def create_test_env(repos_test_path, config):
651 """
651 """
652 Makes a fresh database and
652 Makes a fresh database and
653 install test repository into tmp dir
653 install test repository into tmp dir
654 """
654 """
655 from kallithea.lib.db_manage import DbManage
655 from kallithea.lib.db_manage import DbManage
656 from kallithea.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
656 from kallithea.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
657
657
658 # PART ONE create db
658 # PART ONE create db
659 dbconf = config['sqlalchemy.db1.url']
659 dbconf = config['sqlalchemy.db1.url']
660 log.debug('making test db %s' % dbconf)
660 log.debug('making test db %s' % dbconf)
661
661
662 # create test dir if it doesn't exist
662 # create test dir if it doesn't exist
663 if not os.path.isdir(repos_test_path):
663 if not os.path.isdir(repos_test_path):
664 log.debug('Creating testdir %s' % repos_test_path)
664 log.debug('Creating testdir %s' % repos_test_path)
665 os.makedirs(repos_test_path)
665 os.makedirs(repos_test_path)
666
666
667 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
667 dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
668 tests=True)
668 tests=True)
669 dbmanage.create_tables(override=True)
669 dbmanage.create_tables(override=True)
670 # for tests dynamically set new root paths based on generated content
670 # for tests dynamically set new root paths based on generated content
671 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
671 dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
672 dbmanage.create_default_user()
672 dbmanage.create_default_user()
673 dbmanage.admin_prompt()
673 dbmanage.admin_prompt()
674 dbmanage.create_permissions()
674 dbmanage.create_permissions()
675 dbmanage.populate_default_permissions()
675 dbmanage.populate_default_permissions()
676 Session().commit()
676 Session().commit()
677 # PART TWO make test repo
677 # PART TWO make test repo
678 log.debug('making test vcs repositories')
678 log.debug('making test vcs repositories')
679
679
680 idx_path = config['app_conf']['index_dir']
680 idx_path = config['app_conf']['index_dir']
681 data_path = config['app_conf']['cache_dir']
681 data_path = config['app_conf']['cache_dir']
682
682
683 #clean index and data
683 #clean index and data
684 if idx_path and os.path.exists(idx_path):
684 if idx_path and os.path.exists(idx_path):
685 log.debug('remove %s' % idx_path)
685 log.debug('remove %s' % idx_path)
686 shutil.rmtree(idx_path)
686 shutil.rmtree(idx_path)
687
687
688 if data_path and os.path.exists(data_path):
688 if data_path and os.path.exists(data_path):
689 log.debug('remove %s' % data_path)
689 log.debug('remove %s' % data_path)
690 shutil.rmtree(data_path)
690 shutil.rmtree(data_path)
691
691
692 #CREATE DEFAULT TEST REPOS
692 #CREATE DEFAULT TEST REPOS
693 cur_dir = dn(dn(abspath(__file__)))
693 cur_dir = dn(dn(abspath(__file__)))
694 tar = tarfile.open(jn(cur_dir, 'tests', 'fixtures', "vcs_test_hg.tar.gz"))
694 tar = tarfile.open(jn(cur_dir, 'tests', 'fixtures', "vcs_test_hg.tar.gz"))
695 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
695 tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
696 tar.close()
696 tar.close()
697
697
698 cur_dir = dn(dn(abspath(__file__)))
698 cur_dir = dn(dn(abspath(__file__)))
699 tar = tarfile.open(jn(cur_dir, 'tests', 'fixtures', "vcs_test_git.tar.gz"))
699 tar = tarfile.open(jn(cur_dir, 'tests', 'fixtures', "vcs_test_git.tar.gz"))
700 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
700 tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
701 tar.close()
701 tar.close()
702
702
703 #LOAD VCS test stuff
703 #LOAD VCS test stuff
704 from kallithea.tests.vcs import setup_package
704 from kallithea.tests.vcs import setup_package
705 setup_package()
705 setup_package()
706
706
707
707
708 #==============================================================================
708 #==============================================================================
709 # PASTER COMMANDS
709 # PASTER COMMANDS
710 #==============================================================================
710 #==============================================================================
711 class BasePasterCommand(Command):
711 class BasePasterCommand(Command):
712 """
712 """
713 Abstract Base Class for paster commands.
713 Abstract Base Class for paster commands.
714
714
715 The celery commands are somewhat aggressive about loading
715 The celery commands are somewhat aggressive about loading
716 celery.conf, and since our module sets the `CELERY_LOADER`
716 celery.conf, and since our module sets the `CELERY_LOADER`
717 environment variable to our loader, we have to bootstrap a bit and
717 environment variable to our loader, we have to bootstrap a bit and
718 make sure we've had a chance to load the pylons config off of the
718 make sure we've had a chance to load the pylons config off of the
719 command line, otherwise everything fails.
719 command line, otherwise everything fails.
720 """
720 """
721 min_args = 1
721 min_args = 1
722 min_args_error = "Please provide a paster config file as an argument."
722 min_args_error = "Please provide a paster config file as an argument."
723 takes_config_file = 1
723 takes_config_file = 1
724 requires_config_file = True
724 requires_config_file = True
725
725
726 def notify_msg(self, msg, log=False):
726 def notify_msg(self, msg, log=False):
727 """Make a notification to user, additionally if logger is passed
727 """Make a notification to user, additionally if logger is passed
728 it logs this action using given logger
728 it logs this action using given logger
729
729
730 :param msg: message that will be printed to user
730 :param msg: message that will be printed to user
731 :param log: logging instance, to use to additionally log this message
731 :param log: logging instance, to use to additionally log this message
732
732
733 """
733 """
734 if log and isinstance(log, logging):
734 if log and isinstance(log, logging):
735 log(msg)
735 log(msg)
736
736
737 def run(self, args):
737 def run(self, args):
738 """
738 """
739 Overrides Command.run
739 Overrides Command.run
740
740
741 Checks for a config file argument and loads it.
741 Checks for a config file argument and loads it.
742 """
742 """
743 if len(args) < self.min_args:
743 if len(args) < self.min_args:
744 raise BadCommand(
744 raise BadCommand(
745 self.min_args_error % {'min_args': self.min_args,
745 self.min_args_error % {'min_args': self.min_args,
746 'actual_args': len(args)})
746 'actual_args': len(args)})
747
747
748 # Decrement because we're going to lob off the first argument.
748 # Decrement because we're going to lob off the first argument.
749 # @@ This is hacky
749 # @@ This is hacky
750 self.min_args -= 1
750 self.min_args -= 1
751 self.bootstrap_config(args[0])
751 self.bootstrap_config(args[0])
752 self.update_parser()
752 self.update_parser()
753 return super(BasePasterCommand, self).run(args[1:])
753 return super(BasePasterCommand, self).run(args[1:])
754
754
755 def update_parser(self):
755 def update_parser(self):
756 """
756 """
757 Abstract method. Allows for the class's parser to be updated
757 Abstract method. Allows for the class's parser to be updated
758 before the superclass's `run` method is called. Necessary to
758 before the superclass's `run` method is called. Necessary to
759 allow options/arguments to be passed through to the underlying
759 allow options/arguments to be passed through to the underlying
760 celery command.
760 celery command.
761 """
761 """
762 raise NotImplementedError("Abstract Method.")
762 raise NotImplementedError("Abstract Method.")
763
763
764 def bootstrap_config(self, conf):
764 def bootstrap_config(self, conf):
765 """
765 """
766 Loads the pylons configuration.
766 Loads the pylons configuration.
767 """
767 """
768 from pylons import config as pylonsconfig
768 from pylons import config as pylonsconfig
769
769
770 self.path_to_ini_file = os.path.realpath(conf)
770 self.path_to_ini_file = os.path.realpath(conf)
771 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
771 conf = paste.deploy.appconfig('config:' + self.path_to_ini_file)
772 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
772 pylonsconfig.init_app(conf.global_conf, conf.local_conf)
773
773
774 def _init_session(self):
774 def _init_session(self):
775 """
775 """
776 Inits SqlAlchemy Session
776 Inits SqlAlchemy Session
777 """
777 """
778 logging.config.fileConfig(self.path_to_ini_file)
778 logging.config.fileConfig(self.path_to_ini_file)
779 from pylons import config
779 from pylons import config
780 from kallithea.model import init_model
780 from kallithea.model import init_model
781 from kallithea.lib.utils2 import engine_from_config
781 from kallithea.lib.utils2 import engine_from_config
782
782
783 #get to remove repos !!
783 #get to remove repos !!
784 add_cache(config)
784 add_cache(config)
785 engine = engine_from_config(config, 'sqlalchemy.db1.')
785 engine = engine_from_config(config, 'sqlalchemy.db1.')
786 init_model(engine)
786 init_model(engine)
787
787
788
788
789 def check_git_version():
789 def check_git_version():
790 """
790 """
791 Checks what version of git is installed in system, and issues a warning
791 Checks what version of git is installed in system, and issues a warning
792 if it's too old for Kallithea to work properly.
792 if it's too old for Kallithea to work properly.
793 """
793 """
794 from kallithea import BACKENDS
794 from kallithea import BACKENDS
795 from kallithea.lib.vcs.backends.git.repository import GitRepository
795 from kallithea.lib.vcs.backends.git.repository import GitRepository
796 from kallithea.lib.vcs.conf import settings
796 from kallithea.lib.vcs.conf import settings
797 from distutils.version import StrictVersion
797 from distutils.version import StrictVersion
798
798
799 if 'git' not in BACKENDS:
799 if 'git' not in BACKENDS:
800 return None
800 return None
801
801
802 stdout, stderr = GitRepository._run_git_command('--version', _bare=True,
802 stdout, stderr = GitRepository._run_git_command('--version', _bare=True,
803 _safe=True)
803 _safe=True)
804
804
805 m = re.search("\d+.\d+.\d+", stdout)
805 m = re.search("\d+.\d+.\d+", stdout)
806 if m:
806 if m:
807 ver = StrictVersion(m.group(0))
807 ver = StrictVersion(m.group(0))
808 else:
808 else:
809 ver = StrictVersion('0.0.0')
809 ver = StrictVersion('0.0.0')
810
810
811 req_ver = StrictVersion('1.7.4')
811 req_ver = StrictVersion('1.7.4')
812
812
813 log.debug('Git executable: "%s" version %s detected: %s'
813 log.debug('Git executable: "%s" version %s detected: %s'
814 % (settings.GIT_EXECUTABLE_PATH, ver, stdout))
814 % (settings.GIT_EXECUTABLE_PATH, ver, stdout))
815 if stderr:
815 if stderr:
816 log.warning('Error detecting git version: %r' % stderr)
816 log.warning('Error detecting git version: %r' % stderr)
817 elif ver < req_ver:
817 elif ver < req_ver:
818 log.warning('Kallithea detected git version %s, which is too old '
818 log.warning('Kallithea detected git version %s, which is too old '
819 'for the system to function properly. '
819 'for the system to function properly. '
820 'Please upgrade to version %s or later.' % (ver, req_ver))
820 'Please upgrade to version %s or later.' % (ver, req_ver))
821 return ver
821 return ver
822
822
823
823
824 @decorator.decorator
824 @decorator.decorator
825 def jsonify(func, *args, **kwargs):
825 def jsonify(func, *args, **kwargs):
826 """Action decorator that formats output for JSON
826 """Action decorator that formats output for JSON
827
827
828 Given a function that will return content, this decorator will turn
828 Given a function that will return content, this decorator will turn
829 the result into JSON, with a content-type of 'application/json' and
829 the result into JSON, with a content-type of 'application/json' and
830 output it.
830 output it.
831
831
832 """
832 """
833 from pylons.decorators.util import get_pylons
833 from pylons.decorators.util import get_pylons
834 from kallithea.lib.compat import json
834 from kallithea.lib.compat import json
835 pylons = get_pylons(args)
835 pylons = get_pylons(args)
836 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
836 pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
837 data = func(*args, **kwargs)
837 data = func(*args, **kwargs)
838 if isinstance(data, (list, tuple)):
838 if isinstance(data, (list, tuple)):
839 msg = "JSON responses with Array envelopes are susceptible to " \
839 msg = "JSON responses with Array envelopes are susceptible to " \
840 "cross-site data leak attacks, see " \
840 "cross-site data leak attacks, see " \
841 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
841 "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
842 warnings.warn(msg, Warning, 2)
842 warnings.warn(msg, Warning, 2)
843 log.warning(msg)
843 log.warning(msg)
844 log.debug("Returning JSON wrapped action output")
844 log.debug("Returning JSON wrapped action output")
845 return json.dumps(data, encoding='utf-8')
845 return json.dumps(data, encoding='utf-8')
846
846
847
847
848 def conditional_cache(region, prefix, condition, func):
848 def conditional_cache(region, prefix, condition, func):
849 """
849 """
850
850
851 Conditional caching function use like::
851 Conditional caching function use like::
852 def _c(arg):
852 def _c(arg):
853 #heavy computation function
853 #heavy computation function
854 return data
854 return data
855
855
856 # denpending from condition the compute is wrapped in cache or not
856 # denpending from condition the compute is wrapped in cache or not
857 compute = conditional_cache('short_term', 'cache_desc', codnition=True, func=func)
857 compute = conditional_cache('short_term', 'cache_desc', codnition=True, func=func)
858 return compute(arg)
858 return compute(arg)
859
859
860 :param region: name of cache region
860 :param region: name of cache region
861 :param prefix: cache region prefix
861 :param prefix: cache region prefix
862 :param condition: condition for cache to be triggered, and return data cached
862 :param condition: condition for cache to be triggered, and return data cached
863 :param func: wrapped heavy function to compute
863 :param func: wrapped heavy function to compute
864
864
865 """
865 """
866 wrapped = func
866 wrapped = func
867 if condition:
867 if condition:
868 log.debug('conditional_cache: True, wrapping call of '
868 log.debug('conditional_cache: True, wrapping call of '
869 'func: %s into %s region cache' % (region, func))
869 'func: %s into %s region cache' % (region, func))
870 wrapped = _cache_decorate((prefix,), None, None, region)(func)
870 wrapped = _cache_decorate((prefix,), None, None, region)(func)
871
871
872 return wrapped
872 return wrapped
@@ -1,476 +1,476 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.user
15 kallithea.model.user
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 users model for Kallithea
18 users model for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 9, 2010
22 :created_on: Apr 9, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import logging
29 import logging
30 import traceback
30 import traceback
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 from sqlalchemy.exc import DatabaseError
33 from sqlalchemy.exc import DatabaseError
34
34
35 from kallithea import EXTERN_TYPE_INTERNAL
35 from kallithea import EXTERN_TYPE_INTERNAL
36 from kallithea.lib.utils2 import safe_unicode, generate_api_key, get_current_authuser
36 from kallithea.lib.utils2 import safe_unicode, generate_api_key, get_current_authuser
37 from kallithea.lib.caching_query import FromCache
37 from kallithea.lib.caching_query import FromCache
38 from kallithea.model import BaseModel
38 from kallithea.model import BaseModel
39 from kallithea.model.db import User, UserToPerm, Notification, \
39 from kallithea.model.db import User, UserToPerm, Notification, \
40 UserEmailMap, UserIpMap
40 UserEmailMap, UserIpMap
41 from kallithea.lib.exceptions import DefaultUserException, \
41 from kallithea.lib.exceptions import DefaultUserException, \
42 UserOwnsReposException
42 UserOwnsReposException
43 from kallithea.model.meta import Session
43 from kallithea.model.meta import Session
44
44
45
45
46 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
47
47
48
48
49 class UserModel(BaseModel):
49 class UserModel(BaseModel):
50 cls = User
50 cls = User
51
51
52 def get(self, user_id, cache=False):
52 def get(self, user_id, cache=False):
53 user = self.sa.query(User)
53 user = self.sa.query(User)
54 if cache:
54 if cache:
55 user = user.options(FromCache("sql_cache_short",
55 user = user.options(FromCache("sql_cache_short",
56 "get_user_%s" % user_id))
56 "get_user_%s" % user_id))
57 return user.get(user_id)
57 return user.get(user_id)
58
58
59 def get_user(self, user):
59 def get_user(self, user):
60 return self._get_user(user)
60 return self._get_user(user)
61
61
62 def get_by_username(self, username, cache=False, case_insensitive=False):
62 def get_by_username(self, username, cache=False, case_insensitive=False):
63
63
64 if case_insensitive:
64 if case_insensitive:
65 user = self.sa.query(User).filter(User.username.ilike(username))
65 user = self.sa.query(User).filter(User.username.ilike(username))
66 else:
66 else:
67 user = self.sa.query(User)\
67 user = self.sa.query(User)\
68 .filter(User.username == username)
68 .filter(User.username == username)
69 if cache:
69 if cache:
70 user = user.options(FromCache("sql_cache_short",
70 user = user.options(FromCache("sql_cache_short",
71 "get_user_%s" % username))
71 "get_user_%s" % username))
72 return user.scalar()
72 return user.scalar()
73
73
74 def get_by_email(self, email, cache=False, case_insensitive=False):
74 def get_by_email(self, email, cache=False, case_insensitive=False):
75 return User.get_by_email(email, case_insensitive, cache)
75 return User.get_by_email(email, case_insensitive, cache)
76
76
77 def get_by_api_key(self, api_key, cache=False):
77 def get_by_api_key(self, api_key, cache=False):
78 return User.get_by_api_key(api_key, cache)
78 return User.get_by_api_key(api_key, cache)
79
79
80 def create(self, form_data, cur_user=None):
80 def create(self, form_data, cur_user=None):
81 if not cur_user:
81 if not cur_user:
82 cur_user = getattr(get_current_authuser(), 'username', None)
82 cur_user = getattr(get_current_authuser(), 'username', None)
83
83
84 from kallithea.lib.hooks import log_create_user, \
84 from kallithea.lib.hooks import log_create_user, \
85 check_allowed_create_user
85 check_allowed_create_user
86 _fd = form_data
86 _fd = form_data
87 user_data = {
87 user_data = {
88 'username': _fd['username'],
88 'username': _fd['username'],
89 'password': _fd['password'],
89 'password': _fd['password'],
90 'email': _fd['email'],
90 'email': _fd['email'],
91 'firstname': _fd['firstname'],
91 'firstname': _fd['firstname'],
92 'lastname': _fd['lastname'],
92 'lastname': _fd['lastname'],
93 'active': _fd['active'],
93 'active': _fd['active'],
94 'admin': False
94 'admin': False
95 }
95 }
96 # raises UserCreationError if it's not allowed
96 # raises UserCreationError if it's not allowed
97 check_allowed_create_user(user_data, cur_user)
97 check_allowed_create_user(user_data, cur_user)
98 from kallithea.lib.auth import get_crypt_password
98 from kallithea.lib.auth import get_crypt_password
99
99
100 new_user = User()
100 new_user = User()
101 for k, v in form_data.items():
101 for k, v in form_data.items():
102 if k == 'password':
102 if k == 'password':
103 v = get_crypt_password(v)
103 v = get_crypt_password(v)
104 if k == 'firstname':
104 if k == 'firstname':
105 k = 'name'
105 k = 'name'
106 setattr(new_user, k, v)
106 setattr(new_user, k, v)
107
107
108 new_user.api_key = generate_api_key(form_data['username'])
108 new_user.api_key = generate_api_key(form_data['username'])
109 self.sa.add(new_user)
109 self.sa.add(new_user)
110
110
111 log_create_user(new_user.get_dict(), cur_user)
111 log_create_user(new_user.get_dict(), cur_user)
112 return new_user
112 return new_user
113
113
114 def create_or_update(self, username, password, email, firstname='',
114 def create_or_update(self, username, password, email, firstname='',
115 lastname='', active=True, admin=False,
115 lastname='', active=True, admin=False,
116 extern_type=None, extern_name=None, cur_user=None):
116 extern_type=None, extern_name=None, cur_user=None):
117 """
117 """
118 Creates a new instance if not found, or updates current one
118 Creates a new instance if not found, or updates current one
119
119
120 :param username:
120 :param username:
121 :param password:
121 :param password:
122 :param email:
122 :param email:
123 :param active:
123 :param active:
124 :param firstname:
124 :param firstname:
125 :param lastname:
125 :param lastname:
126 :param active:
126 :param active:
127 :param admin:
127 :param admin:
128 :param extern_name:
128 :param extern_name:
129 :param extern_type:
129 :param extern_type:
130 :param cur_user:
130 :param cur_user:
131 """
131 """
132 if not cur_user:
132 if not cur_user:
133 cur_user = getattr(get_current_authuser(), 'username', None)
133 cur_user = getattr(get_current_authuser(), 'username', None)
134
134
135 from kallithea.lib.auth import get_crypt_password, check_password
135 from kallithea.lib.auth import get_crypt_password, check_password
136 from kallithea.lib.hooks import log_create_user, \
136 from kallithea.lib.hooks import log_create_user, \
137 check_allowed_create_user
137 check_allowed_create_user
138 user_data = {
138 user_data = {
139 'username': username, 'password': password,
139 'username': username, 'password': password,
140 'email': email, 'firstname': firstname, 'lastname': lastname,
140 'email': email, 'firstname': firstname, 'lastname': lastname,
141 'active': active, 'admin': admin
141 'active': active, 'admin': admin
142 }
142 }
143 # raises UserCreationError if it's not allowed
143 # raises UserCreationError if it's not allowed
144 check_allowed_create_user(user_data, cur_user)
144 check_allowed_create_user(user_data, cur_user)
145
145
146 log.debug('Checking for %s account in Kallithea database' % username)
146 log.debug('Checking for %s account in Kallithea database' % username)
147 user = User.get_by_username(username, case_insensitive=True)
147 user = User.get_by_username(username, case_insensitive=True)
148 if user is None:
148 if user is None:
149 log.debug('creating new user %s' % username)
149 log.debug('creating new user %s' % username)
150 new_user = User()
150 new_user = User()
151 edit = False
151 edit = False
152 else:
152 else:
153 log.debug('updating user %s' % username)
153 log.debug('updating user %s' % username)
154 new_user = user
154 new_user = user
155 edit = True
155 edit = True
156
156
157 try:
157 try:
158 new_user.username = username
158 new_user.username = username
159 new_user.admin = admin
159 new_user.admin = admin
160 new_user.email = email
160 new_user.email = email
161 new_user.active = active
161 new_user.active = active
162 new_user.extern_name = safe_unicode(extern_name) \
162 new_user.extern_name = safe_unicode(extern_name) \
163 if extern_name else None
163 if extern_name else None
164 new_user.extern_type = safe_unicode(extern_type) \
164 new_user.extern_type = safe_unicode(extern_type) \
165 if extern_type else None
165 if extern_type else None
166 new_user.name = firstname
166 new_user.name = firstname
167 new_user.lastname = lastname
167 new_user.lastname = lastname
168
168
169 if not edit:
169 if not edit:
170 new_user.api_key = generate_api_key(username)
170 new_user.api_key = generate_api_key(username)
171
171
172 # set password only if creating an user or password is changed
172 # set password only if creating an user or password is changed
173 password_change = new_user.password and \
173 password_change = new_user.password and \
174 not check_password(password, new_user.password)
174 not check_password(password, new_user.password)
175 if not edit or password_change:
175 if not edit or password_change:
176 reason = 'new password' if edit else 'new user'
176 reason = 'new password' if edit else 'new user'
177 log.debug('Updating password reason=>%s' % (reason,))
177 log.debug('Updating password reason=>%s' % (reason,))
178 new_user.password = get_crypt_password(password) \
178 new_user.password = get_crypt_password(password) \
179 if password else None
179 if password else None
180
180
181 self.sa.add(new_user)
181 self.sa.add(new_user)
182
182
183 if not edit:
183 if not edit:
184 log_create_user(new_user.get_dict(), cur_user)
184 log_create_user(new_user.get_dict(), cur_user)
185 return new_user
185 return new_user
186 except (DatabaseError,):
186 except (DatabaseError,):
187 log.error(traceback.format_exc())
187 log.error(traceback.format_exc())
188 raise
188 raise
189
189
190 def create_registration(self, form_data):
190 def create_registration(self, form_data):
191 from kallithea.model.notification import NotificationModel
191 from kallithea.model.notification import NotificationModel
192 import kallithea.lib.helpers as h
192 import kallithea.lib.helpers as h
193
193
194 form_data['admin'] = False
194 form_data['admin'] = False
195 form_data['extern_name'] = EXTERN_TYPE_INTERNAL
195 form_data['extern_name'] = EXTERN_TYPE_INTERNAL
196 form_data['extern_type'] = EXTERN_TYPE_INTERNAL
196 form_data['extern_type'] = EXTERN_TYPE_INTERNAL
197 new_user = self.create(form_data)
197 new_user = self.create(form_data)
198
198
199 self.sa.add(new_user)
199 self.sa.add(new_user)
200 self.sa.flush()
200 self.sa.flush()
201
201
202 # notification to admins
202 # notification to admins
203 subject = _('New user registration')
203 subject = _('New user registration')
204 body = (
204 body = (
205 'New user registration\n'
205 'New user registration\n'
206 '---------------------\n'
206 '---------------------\n'
207 '- Username: {user.username}\n'
207 '- Username: {user.username}\n'
208 '- Full Name: {user.full_name}\n'
208 '- Full Name: {user.full_name}\n'
209 '- Email: {user.email}\n'
209 '- Email: {user.email}\n'
210 ).format(user=new_user)
210 ).format(user=new_user)
211 edit_url = h.canonical_url('edit_user', id=new_user.user_id)
211 edit_url = h.canonical_url('edit_user', id=new_user.user_id)
212 email_kwargs = {
212 email_kwargs = {
213 'registered_user_url': edit_url,
213 'registered_user_url': edit_url,
214 'new_username': new_user.username}
214 'new_username': new_user.username}
215 NotificationModel().create(created_by=new_user, subject=subject,
215 NotificationModel().create(created_by=new_user, subject=subject,
216 body=body, recipients=None,
216 body=body, recipients=None,
217 type_=Notification.TYPE_REGISTRATION,
217 type_=Notification.TYPE_REGISTRATION,
218 email_kwargs=email_kwargs)
218 email_kwargs=email_kwargs)
219
219
220 def update(self, user_id, form_data, skip_attrs=[]):
220 def update(self, user_id, form_data, skip_attrs=[]):
221 from kallithea.lib.auth import get_crypt_password
221 from kallithea.lib.auth import get_crypt_password
222
222
223 user = self.get(user_id, cache=False)
223 user = self.get(user_id, cache=False)
224 if user.username == User.DEFAULT_USER:
224 if user.username == User.DEFAULT_USER:
225 raise DefaultUserException(
225 raise DefaultUserException(
226 _("You can't Edit this user since it's "
226 _("You can't Edit this user since it's "
227 "crucial for entire application"))
227 "crucial for entire application"))
228
228
229 for k, v in form_data.items():
229 for k, v in form_data.items():
230 if k in skip_attrs:
230 if k in skip_attrs:
231 continue
231 continue
232 if k == 'new_password' and v:
232 if k == 'new_password' and v:
233 user.password = get_crypt_password(v)
233 user.password = get_crypt_password(v)
234 else:
234 else:
235 # old legacy thing orm models store firstname as name,
235 # old legacy thing orm models store firstname as name,
236 # need proper refactor to username
236 # need proper refactor to username
237 if k == 'firstname':
237 if k == 'firstname':
238 k = 'name'
238 k = 'name'
239 setattr(user, k, v)
239 setattr(user, k, v)
240 self.sa.add(user)
240 self.sa.add(user)
241
241
242 def update_user(self, user, **kwargs):
242 def update_user(self, user, **kwargs):
243 from kallithea.lib.auth import get_crypt_password
243 from kallithea.lib.auth import get_crypt_password
244
244
245 user = self._get_user(user)
245 user = self._get_user(user)
246 if user.username == User.DEFAULT_USER:
246 if user.username == User.DEFAULT_USER:
247 raise DefaultUserException(
247 raise DefaultUserException(
248 _("You can't Edit this user since it's"
248 _("You can't Edit this user since it's"
249 " crucial for entire application")
249 " crucial for entire application")
250 )
250 )
251
251
252 for k, v in kwargs.items():
252 for k, v in kwargs.items():
253 if k == 'password' and v:
253 if k == 'password' and v:
254 v = get_crypt_password(v)
254 v = get_crypt_password(v)
255
255
256 setattr(user, k, v)
256 setattr(user, k, v)
257 self.sa.add(user)
257 self.sa.add(user)
258 return user
258 return user
259
259
260 def delete(self, user, cur_user=None):
260 def delete(self, user, cur_user=None):
261 if not cur_user:
261 if not cur_user:
262 cur_user = getattr(get_current_authuser(), 'username', None)
262 cur_user = getattr(get_current_authuser(), 'username', None)
263 user = self._get_user(user)
263 user = self._get_user(user)
264
264
265 if user.username == User.DEFAULT_USER:
265 if user.username == User.DEFAULT_USER:
266 raise DefaultUserException(
266 raise DefaultUserException(
267 _(u"You can't remove this user since it's"
267 _(u"You can't remove this user since it's"
268 " crucial for entire application"))
268 " crucial for entire application"))
269 if user.repositories:
269 if user.repositories:
270 repos = [x.repo_name for x in user.repositories]
270 repos = [x.repo_name for x in user.repositories]
271 raise UserOwnsReposException(
271 raise UserOwnsReposException(
272 _(u'User "%s" still owns %s repositories and cannot be '
272 _(u'User "%s" still owns %s repositories and cannot be '
273 'removed. Switch owners or remove those repositories: %s')
273 'removed. Switch owners or remove those repositories: %s')
274 % (user.username, len(repos), ', '.join(repos)))
274 % (user.username, len(repos), ', '.join(repos)))
275 if user.repo_groups:
275 if user.repo_groups:
276 repogroups = [x.group_name for x in user.repo_groups]
276 repogroups = [x.group_name for x in user.repo_groups]
277 raise UserOwnsReposException(_(
277 raise UserOwnsReposException(_(
278 'User "%s" still owns %s repository groups and cannot be '
278 'User "%s" still owns %s repository groups and cannot be '
279 'removed. Switch owners or remove those repository groups: %s')
279 'removed. Switch owners or remove those repository groups: %s')
280 % (user.username, len(repogroups), ', '.join(repogroups)))
280 % (user.username, len(repogroups), ', '.join(repogroups)))
281 if user.user_groups:
281 if user.user_groups:
282 usergroups = [x.users_group_name for x in user.user_groups]
282 usergroups = [x.users_group_name for x in user.user_groups]
283 raise UserOwnsReposException(
283 raise UserOwnsReposException(
284 _('User "%s" still owns %s user groups and cannot be '
284 _('User "%s" still owns %s user groups and cannot be '
285 'removed. Switch owners or remove those user groups: %s')
285 'removed. Switch owners or remove those user groups: %s')
286 % (user.username, len(usergroups), ', '.join(usergroups)))
286 % (user.username, len(usergroups), ', '.join(usergroups)))
287 self.sa.delete(user)
287 self.sa.delete(user)
288
288
289 from kallithea.lib.hooks import log_delete_user
289 from kallithea.lib.hooks import log_delete_user
290 log_delete_user(user.get_dict(), cur_user)
290 log_delete_user(user.get_dict(), cur_user)
291
291
292 def reset_password_link(self, data):
292 def reset_password_link(self, data):
293 from kallithea.lib.celerylib import tasks, run_task
293 from kallithea.lib.celerylib import tasks, run_task
294 from kallithea.model.notification import EmailNotificationModel
294 from kallithea.model.notification import EmailNotificationModel
295 import kallithea.lib.helpers as h
295 import kallithea.lib.helpers as h
296
296
297 user_email = data['email']
297 user_email = data['email']
298 user = User.get_by_email(user_email)
298 user = User.get_by_email(user_email)
299 if user:
299 if user:
300 log.debug('password reset user found %s' % user)
300 log.debug('password reset user found %s' % user)
301 link = h.canonical_url('reset_password_confirmation',
301 link = h.canonical_url('reset_password_confirmation',
302 key=user.api_key)
302 key=user.api_key)
303 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
303 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
304 body = EmailNotificationModel().get_email_tmpl(
304 body = EmailNotificationModel().get_email_tmpl(
305 reg_type, 'txt',
305 reg_type, 'txt',
306 user=user.short_contact,
306 user=user.short_contact,
307 reset_url=link)
307 reset_url=link)
308 html_body = EmailNotificationModel().get_email_tmpl(
308 html_body = EmailNotificationModel().get_email_tmpl(
309 reg_type, 'html',
309 reg_type, 'html',
310 user=user.short_contact,
310 user=user.short_contact,
311 reset_url=link)
311 reset_url=link)
312 log.debug('sending email')
312 log.debug('sending email')
313 run_task(tasks.send_email, [user_email],
313 run_task(tasks.send_email, [user_email],
314 _("Password reset link"), body, html_body)
314 _("Password reset link"), body, html_body)
315 log.info('send new password mail to %s' % user_email)
315 log.info('send new password mail to %s' % user_email)
316 else:
316 else:
317 log.debug("password reset email %s not found" % user_email)
317 log.debug("password reset email %s not found" % user_email)
318
318
319 return True
319 return True
320
320
321 def reset_password(self, data):
321 def reset_password(self, data):
322 from kallithea.lib.celerylib import tasks, run_task
322 from kallithea.lib.celerylib import tasks, run_task
323 from kallithea.lib import auth
323 from kallithea.lib import auth
324 user_email = data['email']
324 user_email = data['email']
325 user = User.get_by_email(user_email)
325 user = User.get_by_email(user_email)
326 new_passwd = auth.PasswordGenerator().gen_password(
326 new_passwd = auth.PasswordGenerator().gen_password(
327 8, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
327 8, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
328 if user:
328 if user:
329 user.password = auth.get_crypt_password(new_passwd)
329 user.password = auth.get_crypt_password(new_passwd)
330 Session().add(user)
330 Session().add(user)
331 Session().commit()
331 Session().commit()
332 log.info('change password for %s' % user_email)
332 log.info('change password for %s' % user_email)
333 if new_passwd is None:
333 if new_passwd is None:
334 raise Exception('unable to generate new password')
334 raise Exception('unable to generate new password')
335
335
336 run_task(tasks.send_email, [user_email],
336 run_task(tasks.send_email, [user_email],
337 _('Your new password'),
337 _('Your new password'),
338 _('Your new Kallithea password:%s') % (new_passwd,))
338 _('Your new Kallithea password:%s') % (new_passwd,))
339 log.info('send new password mail to %s' % user_email)
339 log.info('send new password mail to %s' % user_email)
340
340
341 return True
341 return True
342
342
343 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
343 def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
344 """
344 """
345 Fetches auth_user by user_id,or api_key if present.
345 Fetches auth_user by user_id,or api_key if present.
346 Fills auth_user attributes with those taken from database.
346 Fills auth_user attributes with those taken from database.
347 Additionally sets is_authenticated if lookup fails
347 Additionally sets is_authenticated if lookup fails
348 present in database
348 present in database
349
349
350 :param auth_user: instance of user to set attributes
350 :param auth_user: instance of user to set attributes
351 :param user_id: user id to fetch by
351 :param user_id: user id to fetch by
352 :param api_key: API key to fetch by
352 :param api_key: API key to fetch by
353 :param username: username to fetch by
353 :param username: username to fetch by
354 """
354 """
355 if user_id is None and api_key is None and username is None:
355 if user_id is None and api_key is None and username is None:
356 raise Exception('You need to pass user_id, api_key or username')
356 raise Exception('You need to pass user_id, api_key or username')
357
357
358 dbuser = None
358 dbuser = None
359 if user_id is not None:
359 if user_id is not None:
360 dbuser = self.get(user_id)
360 dbuser = self.get(user_id)
361 elif api_key is not None:
361 elif api_key is not None:
362 dbuser = self.get_by_api_key(api_key)
362 dbuser = self.get_by_api_key(api_key)
363 elif username is not None:
363 elif username is not None:
364 dbuser = self.get_by_username(username)
364 dbuser = self.get_by_username(username)
365
365
366 if dbuser is not None and dbuser.active:
366 if dbuser is not None and dbuser.active:
367 log.debug('filling %s data' % dbuser)
367 log.debug('filling %s data' % dbuser)
368 for k, v in dbuser.get_dict().iteritems():
368 for k, v in dbuser.get_dict().iteritems():
369 if k not in ['api_keys', 'permissions']:
369 if k not in ['api_keys', 'permissions']:
370 setattr(auth_user, k, v)
370 setattr(auth_user, k, v)
371 return True
371 return True
372 return False
372 return False
373
373
374 def has_perm(self, user, perm):
374 def has_perm(self, user, perm):
375 perm = self._get_perm(perm)
375 perm = self._get_perm(perm)
376 user = self._get_user(user)
376 user = self._get_user(user)
377
377
378 return UserToPerm.query().filter(UserToPerm.user == user)\
378 return UserToPerm.query().filter(UserToPerm.user == user)\
379 .filter(UserToPerm.permission == perm).scalar() is not None
379 .filter(UserToPerm.permission == perm).scalar() is not None
380
380
381 def grant_perm(self, user, perm):
381 def grant_perm(self, user, perm):
382 """
382 """
383 Grant user global permissions
383 Grant user global permissions
384
384
385 :param user:
385 :param user:
386 :param perm:
386 :param perm:
387 """
387 """
388 user = self._get_user(user)
388 user = self._get_user(user)
389 perm = self._get_perm(perm)
389 perm = self._get_perm(perm)
390 # if this permission is already granted skip it
390 # if this permission is already granted skip it
391 _perm = UserToPerm.query()\
391 _perm = UserToPerm.query()\
392 .filter(UserToPerm.user == user)\
392 .filter(UserToPerm.user == user)\
393 .filter(UserToPerm.permission == perm)\
393 .filter(UserToPerm.permission == perm)\
394 .scalar()
394 .scalar()
395 if _perm:
395 if _perm:
396 return
396 return
397 new = UserToPerm()
397 new = UserToPerm()
398 new.user = user
398 new.user = user
399 new.permission = perm
399 new.permission = perm
400 self.sa.add(new)
400 self.sa.add(new)
401 return new
401 return new
402
402
403 def revoke_perm(self, user, perm):
403 def revoke_perm(self, user, perm):
404 """
404 """
405 Revoke users global permissions
405 Revoke users global permissions
406
406
407 :param user:
407 :param user:
408 :param perm:
408 :param perm:
409 """
409 """
410 user = self._get_user(user)
410 user = self._get_user(user)
411 perm = self._get_perm(perm)
411 perm = self._get_perm(perm)
412
412
413 UserToPerm.query().filter(
413 UserToPerm.query().filter(
414 UserToPerm.user == user,
414 UserToPerm.user == user,
415 UserToPerm.permission == perm,
415 UserToPerm.permission == perm,
416 ).delete()
416 ).delete()
417
417
418 def add_extra_email(self, user, email):
418 def add_extra_email(self, user, email):
419 """
419 """
420 Adds email address to UserEmailMap
420 Adds email address to UserEmailMap
421
421
422 :param user:
422 :param user:
423 :param email:
423 :param email:
424 """
424 """
425 from kallithea.model import forms
425 from kallithea.model import forms
426 form = forms.UserExtraEmailForm()()
426 form = forms.UserExtraEmailForm()()
427 data = form.to_python(dict(email=email))
427 data = form.to_python(dict(email=email))
428 user = self._get_user(user)
428 user = self._get_user(user)
429
429
430 obj = UserEmailMap()
430 obj = UserEmailMap()
431 obj.user = user
431 obj.user = user
432 obj.email = data['email']
432 obj.email = data['email']
433 self.sa.add(obj)
433 self.sa.add(obj)
434 return obj
434 return obj
435
435
436 def delete_extra_email(self, user, email_id):
436 def delete_extra_email(self, user, email_id):
437 """
437 """
438 Removes email address from UserEmailMap
438 Removes email address from UserEmailMap
439
439
440 :param user:
440 :param user:
441 :param email_id:
441 :param email_id:
442 """
442 """
443 user = self._get_user(user)
443 user = self._get_user(user)
444 obj = UserEmailMap.query().get(email_id)
444 obj = UserEmailMap.query().get(email_id)
445 if obj:
445 if obj:
446 self.sa.delete(obj)
446 self.sa.delete(obj)
447
447
448 def add_extra_ip(self, user, ip):
448 def add_extra_ip(self, user, ip):
449 """
449 """
450 Adds ip address to UserIpMap
450 Adds IP address to UserIpMap
451
451
452 :param user:
452 :param user:
453 :param ip:
453 :param ip:
454 """
454 """
455 from kallithea.model import forms
455 from kallithea.model import forms
456 form = forms.UserExtraIpForm()()
456 form = forms.UserExtraIpForm()()
457 data = form.to_python(dict(ip=ip))
457 data = form.to_python(dict(ip=ip))
458 user = self._get_user(user)
458 user = self._get_user(user)
459
459
460 obj = UserIpMap()
460 obj = UserIpMap()
461 obj.user = user
461 obj.user = user
462 obj.ip_addr = data['ip']
462 obj.ip_addr = data['ip']
463 self.sa.add(obj)
463 self.sa.add(obj)
464 return obj
464 return obj
465
465
466 def delete_extra_ip(self, user, ip_id):
466 def delete_extra_ip(self, user, ip_id):
467 """
467 """
468 Removes ip address from UserIpMap
468 Removes IP address from UserIpMap
469
469
470 :param user:
470 :param user:
471 :param ip_id:
471 :param ip_id:
472 """
472 """
473 user = self._get_user(user)
473 user = self._get_user(user)
474 obj = UserIpMap.query().get(ip_id)
474 obj = UserIpMap.query().get(ip_id)
475 if obj:
475 if obj:
476 self.sa.delete(obj)
476 self.sa.delete(obj)
@@ -1,43 +1,43 b''
1 <div class="ips_wrap">
1 <div class="ips_wrap">
2 <table class="noborder">
2 <table class="noborder">
3 %if c.user_ip_map:
3 %if c.user_ip_map:
4 %for ip in c.user_ip_map:
4 %for ip in c.user_ip_map:
5 <tr>
5 <tr>
6 <td><div class="ip">${ip.ip_addr}</div></td>
6 <td><div class="ip">${ip.ip_addr}</div></td>
7 <td><div class="ip">${h.ip_range(ip.ip_addr)}</div></td>
7 <td><div class="ip">${h.ip_range(ip.ip_addr)}</div></td>
8 <td>
8 <td>
9 ${h.form(url('edit_user_ips', id=c.user.user_id),method='delete')}
9 ${h.form(url('edit_user_ips', id=c.user.user_id),method='delete')}
10 ${h.hidden('del_ip_id',ip.ip_id)}
10 ${h.hidden('del_ip_id',ip.ip_id)}
11 ${h.hidden('default_user', 'True')}
11 ${h.hidden('default_user', 'True')}
12 <i class="icon-minus-circled" style="color:#FF4444"></i> ${h.submit('remove_',_('delete'),id="remove_ip_%s" % ip.ip_id,
12 <i class="icon-minus-circled" style="color:#FF4444"></i> ${h.submit('remove_',_('delete'),id="remove_ip_%s" % ip.ip_id,
13 class_="action_button", onclick="return confirm('"+_('Confirm to delete this ip: %s') % ip.ip_addr+"');")}
13 class_="action_button", onclick="return confirm('"+_('Confirm to delete this IP address: %s') % ip.ip_addr+"');")}
14 ${h.end_form()}
14 ${h.end_form()}
15 </td>
15 </td>
16 </tr>
16 </tr>
17 %endfor
17 %endfor
18 %else:
18 %else:
19 <tr><td><div class="ip">${_('All IP addresses are allowed.')}</div></td></tr>
19 <tr><td><div class="ip">${_('All IP addresses are allowed.')}</div></td></tr>
20 %endif
20 %endif
21 </table>
21 </table>
22 </div>
22 </div>
23
23
24 ${h.form(url('edit_user_ips', id=c.user.user_id),method='put')}
24 ${h.form(url('edit_user_ips', id=c.user.user_id),method='put')}
25 <div class="form">
25 <div class="form">
26 <!-- fields -->
26 <!-- fields -->
27 <div class="fields">
27 <div class="fields">
28 <div class="field">
28 <div class="field">
29 <div class="label">
29 <div class="label">
30 <label for="new_ip">${_('New ip address')}:</label>
30 <label for="new_ip">${_('New IP address')}:</label>
31 </div>
31 </div>
32 <div class="input">
32 <div class="input">
33 ${h.hidden('default_user', 'True')}
33 ${h.hidden('default_user', 'True')}
34 ${h.text('new_ip', class_='medium')}
34 ${h.text('new_ip', class_='medium')}
35 </div>
35 </div>
36 </div>
36 </div>
37 <div class="buttons">
37 <div class="buttons">
38 ${h.submit('save',_('Add'),class_="btn")}
38 ${h.submit('save',_('Add'),class_="btn")}
39 ${h.reset('reset',_('Reset'),class_="btn")}
39 ${h.reset('reset',_('Reset'),class_="btn")}
40 </div>
40 </div>
41 </div>
41 </div>
42 </div>
42 </div>
43 ${h.end_form()}
43 ${h.end_form()}
@@ -1,55 +1,55 b''
1 <div class="ips_wrap">
1 <div class="ips_wrap">
2 <table class="noborder">
2 <table class="noborder">
3 %if c.default_user_ip_map and c.inherit_default_ips:
3 %if c.default_user_ip_map and c.inherit_default_ips:
4 %for ip in c.default_user_ip_map:
4 %for ip in c.default_user_ip_map:
5 <tr>
5 <tr>
6 <td><div class="ip">${ip.ip_addr}</div></td>
6 <td><div class="ip">${ip.ip_addr}</div></td>
7 <td><div class="ip">${h.ip_range(ip.ip_addr)}</div></td>
7 <td><div class="ip">${h.ip_range(ip.ip_addr)}</div></td>
8 <td>${h.literal(_('Inherited from %s') % h.link_to('*default*',h.url('admin_permissions_ips')))}</td>
8 <td>${h.literal(_('Inherited from %s') % h.link_to('*default*',h.url('admin_permissions_ips')))}</td>
9 </tr>
9 </tr>
10 %endfor
10 %endfor
11 %endif
11 %endif
12
12
13 %if c.user_ip_map:
13 %if c.user_ip_map:
14 %for ip in c.user_ip_map:
14 %for ip in c.user_ip_map:
15 <tr>
15 <tr>
16 <td><div class="ip">${ip.ip_addr}</div></td>
16 <td><div class="ip">${ip.ip_addr}</div></td>
17 <td><div class="ip">${h.ip_range(ip.ip_addr)}</div></td>
17 <td><div class="ip">${h.ip_range(ip.ip_addr)}</div></td>
18 <td>
18 <td>
19 ${h.form(url('edit_user_ips', id=c.user.user_id),method='delete')}
19 ${h.form(url('edit_user_ips', id=c.user.user_id),method='delete')}
20 ${h.hidden('del_ip_id',ip.ip_id)}
20 ${h.hidden('del_ip_id',ip.ip_id)}
21 <i class="icon-minus-circled" style="color:#FF4444"></i>
21 <i class="icon-minus-circled" style="color:#FF4444"></i>
22 ${h.submit('remove_',_('delete'),id="remove_ip_%s" % ip.ip_id,
22 ${h.submit('remove_',_('delete'),id="remove_ip_%s" % ip.ip_id,
23 class_="action_button", onclick="return confirm('"+_('Confirm to delete this ip: %s') % ip.ip_addr+"');")}
23 class_="action_button", onclick="return confirm('"+_('Confirm to delete this IP address: %s') % ip.ip_addr+"');")}
24 ${h.end_form()}
24 ${h.end_form()}
25 </td>
25 </td>
26 </tr>
26 </tr>
27 %endfor
27 %endfor
28 %endif
28 %endif
29 %if not c.default_user_ip_map and not c.user_ip_map:
29 %if not c.default_user_ip_map and not c.user_ip_map:
30 <tr><td><div class="ip">${_('All IP addresses are allowed.')}</div></td></tr>
30 <tr><td><div class="ip">${_('All IP addresses are allowed.')}</div></td></tr>
31 %endif
31 %endif
32 </table>
32 </table>
33 </div>
33 </div>
34
34
35 <div>
35 <div>
36 ${h.form(url('edit_user_ips', id=c.user.user_id),method='put')}
36 ${h.form(url('edit_user_ips', id=c.user.user_id),method='put')}
37 <div class="form">
37 <div class="form">
38 <!-- fields -->
38 <!-- fields -->
39 <div class="fields">
39 <div class="fields">
40 <div class="field">
40 <div class="field">
41 <div class="label">
41 <div class="label">
42 <label for="new_ip">${_('New ip address')}:</label>
42 <label for="new_ip">${_('New IP address')}:</label>
43 </div>
43 </div>
44 <div class="input">
44 <div class="input">
45 ${h.text('new_ip', class_='medium')}
45 ${h.text('new_ip', class_='medium')}
46 </div>
46 </div>
47 </div>
47 </div>
48 <div class="buttons">
48 <div class="buttons">
49 ${h.submit('save',_('Add'),class_="btn")}
49 ${h.submit('save',_('Add'),class_="btn")}
50 ${h.reset('reset',_('Reset'),class_="btn")}
50 ${h.reset('reset',_('Reset'),class_="btn")}
51 </div>
51 </div>
52 </div>
52 </div>
53 </div>
53 </div>
54 ${h.end_form()}
54 ${h.end_form()}
55 </div>
55 </div>
General Comments 0
You need to be logged in to leave comments. Login now