##// END OF EJS Templates
AuthUser: Drop ip_addr field...
Søren Løvborg -
r5211:4a2a66bf default
parent child Browse files
Show More
@@ -1,272 +1,272 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.my_account
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 my account controller for Kallithea admin
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: August 20, 2013
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from sqlalchemy import func
33 33 from formencode import htmlfill
34 34 from pylons import request, tmpl_context as c, url
35 35 from pylons.controllers.util import redirect
36 36 from pylons.i18n.translation import _
37 37
38 38 from kallithea import EXTERN_TYPE_INTERNAL
39 39 from kallithea.lib import helpers as h
40 40 from kallithea.lib.auth import LoginRequired, NotAnonymous, AuthUser
41 41 from kallithea.lib.base import BaseController, render
42 42 from kallithea.lib.utils2 import generate_api_key, safe_int
43 43 from kallithea.lib.compat import json
44 44 from kallithea.model.db import Repository, \
45 45 UserEmailMap, UserApiKeys, User, UserFollowing
46 46 from kallithea.model.forms import UserForm, PasswordChangeForm
47 47 from kallithea.model.user import UserModel
48 48 from kallithea.model.repo import RepoModel
49 49 from kallithea.model.api_key import ApiKeyModel
50 50 from kallithea.model.meta import Session
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class MyAccountController(BaseController):
56 56 """REST Controller styled on the Atom Publishing Protocol"""
57 57 # To properly map this controller, ensure your config/routing.py
58 58 # file has a resource setup:
59 59 # map.resource('setting', 'settings', controller='admin/settings',
60 60 # path_prefix='/admin', name_prefix='admin_')
61 61
62 62 @LoginRequired()
63 63 @NotAnonymous()
64 64 def __before__(self):
65 65 super(MyAccountController, self).__before__()
66 66
67 67 def __load_data(self):
68 68 c.user = User.get(self.authuser.user_id)
69 69 if c.user.username == User.DEFAULT_USER:
70 70 h.flash(_("You can't edit this user since it's"
71 71 " crucial for entire application"), category='warning')
72 72 return redirect(url('users'))
73 73 c.EXTERN_TYPE_INTERNAL = EXTERN_TYPE_INTERNAL
74 74
75 75 def _load_my_repos_data(self, watched=False):
76 76 if watched:
77 77 admin = False
78 78 repos_list = [x.follows_repository for x in
79 79 Session().query(UserFollowing).filter(
80 80 UserFollowing.user_id ==
81 81 self.authuser.user_id).all()]
82 82 else:
83 83 admin = True
84 84 repos_list = Session().query(Repository)\
85 85 .filter(Repository.user_id ==
86 86 self.authuser.user_id)\
87 87 .order_by(func.lower(Repository.repo_name)).all()
88 88
89 89 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
90 90 admin=admin)
91 91 #json used to render the grid
92 92 return json.dumps(repos_data)
93 93
94 94 def my_account(self):
95 95 """
96 96 GET /_admin/my_account Displays info about my account
97 97 """
98 98 # url('my_account')
99 99 c.active = 'profile'
100 100 self.__load_data()
101 c.perm_user = AuthUser(user_id=self.authuser.user_id,
102 ip_addr=self.ip_addr)
101 c.perm_user = AuthUser(user_id=self.authuser.user_id)
102 c.ip_addr = self.ip_addr
103 103 c.extern_type = c.user.extern_type
104 104 c.extern_name = c.user.extern_name
105 105
106 106 defaults = c.user.get_dict()
107 107 update = False
108 108 if request.POST:
109 109 _form = UserForm(edit=True,
110 110 old_data={'user_id': self.authuser.user_id,
111 111 'email': self.authuser.email})()
112 112 form_result = {}
113 113 try:
114 114 post_data = dict(request.POST)
115 115 post_data['new_password'] = ''
116 116 post_data['password_confirmation'] = ''
117 117 form_result = _form.to_python(post_data)
118 118 # skip updating those attrs for my account
119 119 skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
120 120 'new_password', 'password_confirmation']
121 121 #TODO: plugin should define if username can be updated
122 122 if c.extern_type != EXTERN_TYPE_INTERNAL:
123 123 # forbid updating username for external accounts
124 124 skip_attrs.append('username')
125 125
126 126 UserModel().update(self.authuser.user_id, form_result,
127 127 skip_attrs=skip_attrs)
128 128 h.flash(_('Your account was updated successfully'),
129 129 category='success')
130 130 Session().commit()
131 131 update = True
132 132
133 133 except formencode.Invalid, errors:
134 134 return htmlfill.render(
135 135 render('admin/my_account/my_account.html'),
136 136 defaults=errors.value,
137 137 errors=errors.error_dict or {},
138 138 prefix_error=False,
139 139 encoding="UTF-8",
140 140 force_defaults=False)
141 141 except Exception:
142 142 log.error(traceback.format_exc())
143 143 h.flash(_('Error occurred during update of user %s') \
144 144 % form_result.get('username'), category='error')
145 145 if update:
146 146 return redirect('my_account')
147 147 return htmlfill.render(
148 148 render('admin/my_account/my_account.html'),
149 149 defaults=defaults,
150 150 encoding="UTF-8",
151 151 force_defaults=False)
152 152
153 153 def my_account_password(self):
154 154 c.active = 'password'
155 155 self.__load_data()
156 156 if request.POST:
157 157 _form = PasswordChangeForm(self.authuser.username)()
158 158 try:
159 159 form_result = _form.to_python(request.POST)
160 160 UserModel().update(self.authuser.user_id, form_result)
161 161 Session().commit()
162 162 h.flash(_("Successfully updated password"), category='success')
163 163 except formencode.Invalid as errors:
164 164 return htmlfill.render(
165 165 render('admin/my_account/my_account.html'),
166 166 defaults=errors.value,
167 167 errors=errors.error_dict or {},
168 168 prefix_error=False,
169 169 encoding="UTF-8",
170 170 force_defaults=False)
171 171 except Exception:
172 172 log.error(traceback.format_exc())
173 173 h.flash(_('Error occurred during update of user password'),
174 174 category='error')
175 175 return render('admin/my_account/my_account.html')
176 176
177 177 def my_account_repos(self):
178 178 c.active = 'repos'
179 179 self.__load_data()
180 180
181 181 #json used to render the grid
182 182 c.data = self._load_my_repos_data()
183 183 return render('admin/my_account/my_account.html')
184 184
185 185 def my_account_watched(self):
186 186 c.active = 'watched'
187 187 self.__load_data()
188 188
189 189 #json used to render the grid
190 190 c.data = self._load_my_repos_data(watched=True)
191 191 return render('admin/my_account/my_account.html')
192 192
193 193 def my_account_perms(self):
194 194 c.active = 'perms'
195 195 self.__load_data()
196 c.perm_user = AuthUser(user_id=self.authuser.user_id,
197 ip_addr=self.ip_addr)
196 c.perm_user = AuthUser(user_id=self.authuser.user_id)
197 c.ip_addr = self.ip_addr
198 198
199 199 return render('admin/my_account/my_account.html')
200 200
201 201 def my_account_emails(self):
202 202 c.active = 'emails'
203 203 self.__load_data()
204 204
205 205 c.user_email_map = UserEmailMap.query()\
206 206 .filter(UserEmailMap.user == c.user).all()
207 207 return render('admin/my_account/my_account.html')
208 208
209 209 def my_account_emails_add(self):
210 210 email = request.POST.get('new_email')
211 211
212 212 try:
213 213 UserModel().add_extra_email(self.authuser.user_id, email)
214 214 Session().commit()
215 215 h.flash(_("Added email %s to user") % email, category='success')
216 216 except formencode.Invalid, error:
217 217 msg = error.error_dict['email']
218 218 h.flash(msg, category='error')
219 219 except Exception:
220 220 log.error(traceback.format_exc())
221 221 h.flash(_('An error occurred during email saving'),
222 222 category='error')
223 223 return redirect(url('my_account_emails'))
224 224
225 225 def my_account_emails_delete(self):
226 226 email_id = request.POST.get('del_email_id')
227 227 user_model = UserModel()
228 228 user_model.delete_extra_email(self.authuser.user_id, email_id)
229 229 Session().commit()
230 230 h.flash(_("Removed email from user"), category='success')
231 231 return redirect(url('my_account_emails'))
232 232
233 233 def my_account_api_keys(self):
234 234 c.active = 'api_keys'
235 235 self.__load_data()
236 236 show_expired = True
237 237 c.lifetime_values = [
238 238 (str(-1), _('Forever')),
239 239 (str(5), _('5 minutes')),
240 240 (str(60), _('1 hour')),
241 241 (str(60 * 24), _('1 day')),
242 242 (str(60 * 24 * 30), _('1 month')),
243 243 ]
244 244 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
245 245 c.user_api_keys = ApiKeyModel().get_api_keys(self.authuser.user_id,
246 246 show_expired=show_expired)
247 247 return render('admin/my_account/my_account.html')
248 248
249 249 def my_account_api_keys_add(self):
250 250 lifetime = safe_int(request.POST.get('lifetime'), -1)
251 251 description = request.POST.get('description')
252 252 ApiKeyModel().create(self.authuser.user_id, description, lifetime)
253 253 Session().commit()
254 254 h.flash(_("API key successfully created"), category='success')
255 255 return redirect(url('my_account_api_keys'))
256 256
257 257 def my_account_api_keys_delete(self):
258 258 api_key = request.POST.get('del_api_key')
259 259 user_id = self.authuser.user_id
260 260 if request.POST.get('del_api_key_builtin'):
261 261 user = User.get(user_id)
262 262 if user:
263 263 user.api_key = generate_api_key(user.username)
264 264 Session().add(user)
265 265 Session().commit()
266 266 h.flash(_("API key successfully reset"), category='success')
267 267 elif api_key:
268 268 ApiKeyModel().delete(api_key, self.authuser.user_id)
269 269 Session().commit()
270 270 h.flash(_("API key successfully deleted"), category='success')
271 271
272 272 return redirect(url('my_account_api_keys'))
@@ -1,485 +1,489 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.admin.users
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Users crud controller for pylons
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 4, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import logging
29 29 import traceback
30 30 import formencode
31 31
32 32 from formencode import htmlfill
33 33 from pylons import request, tmpl_context as c, url, config
34 34 from pylons.controllers.util import redirect
35 35 from pylons.i18n.translation import _
36 36 from sqlalchemy.sql.expression import func
37 37 from webob.exc import HTTPNotFound
38 38
39 39 import kallithea
40 40 from kallithea.lib.exceptions import DefaultUserException, \
41 41 UserOwnsReposException, UserCreationError
42 42 from kallithea.lib import helpers as h
43 43 from kallithea.lib.auth import LoginRequired, HasPermissionAllDecorator, \
44 44 AuthUser
45 45 import kallithea.lib.auth_modules.auth_internal
46 46 from kallithea.lib import auth_modules
47 47 from kallithea.lib.base import BaseController, render
48 48 from kallithea.model.api_key import ApiKeyModel
49 49
50 50 from kallithea.model.db import User, UserEmailMap, UserIpMap, UserToPerm
51 51 from kallithea.model.forms import UserForm, CustomDefaultPermissionsForm
52 52 from kallithea.model.user import UserModel
53 53 from kallithea.model.meta import Session
54 54 from kallithea.lib.utils import action_logger
55 55 from kallithea.lib.compat import json
56 56 from kallithea.lib.utils2 import datetime_to_time, safe_int, generate_api_key
57 57
58 58 log = logging.getLogger(__name__)
59 59
60 60
61 61 class UsersController(BaseController):
62 62 """REST Controller styled on the Atom Publishing Protocol"""
63 63
64 64 @LoginRequired()
65 65 @HasPermissionAllDecorator('hg.admin')
66 66 def __before__(self):
67 67 super(UsersController, self).__before__()
68 68 c.available_permissions = config['available_permissions']
69 69 c.EXTERN_TYPE_INTERNAL = kallithea.EXTERN_TYPE_INTERNAL
70 70
71 71 def index(self, format='html'):
72 72 """GET /users: All items in the collection"""
73 73 # url('users')
74 74
75 75 c.users_list = User.query().order_by(User.username)\
76 76 .filter(User.username != User.DEFAULT_USER)\
77 77 .order_by(func.lower(User.username))\
78 78 .all()
79 79
80 80 users_data = []
81 81 total_records = len(c.users_list)
82 82 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
83 83 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
84 84
85 85 grav_tmpl = '<div class="gravatar">%s</div>'
86 86
87 87 username = lambda user_id, username: (
88 88 template.get_def("user_name")
89 89 .render(user_id, username, _=_, h=h, c=c))
90 90
91 91 user_actions = lambda user_id, username: (
92 92 template.get_def("user_actions")
93 93 .render(user_id, username, _=_, h=h, c=c))
94 94
95 95 for user in c.users_list:
96 96 users_data.append({
97 97 "gravatar": grav_tmpl % h.gravatar(user.email, size=20),
98 98 "raw_name": user.username,
99 99 "username": username(user.user_id, user.username),
100 100 "firstname": h.escape(user.name),
101 101 "lastname": h.escape(user.lastname),
102 102 "last_login": h.fmt_date(user.last_login),
103 103 "last_login_raw": datetime_to_time(user.last_login),
104 104 "active": h.boolicon(user.active),
105 105 "admin": h.boolicon(user.admin),
106 106 "extern_type": user.extern_type,
107 107 "extern_name": user.extern_name,
108 108 "action": user_actions(user.user_id, user.username),
109 109 })
110 110
111 111 c.data = json.dumps({
112 112 "totalRecords": total_records,
113 113 "startIndex": 0,
114 114 "sort": None,
115 115 "dir": "asc",
116 116 "records": users_data
117 117 })
118 118
119 119 return render('admin/users/users.html')
120 120
121 121 def create(self):
122 122 """POST /users: Create a new item"""
123 123 # url('users')
124 124 c.default_extern_type = auth_modules.auth_internal.KallitheaAuthPlugin.name
125 125 user_model = UserModel()
126 126 user_form = UserForm()()
127 127 try:
128 128 form_result = user_form.to_python(dict(request.POST))
129 129 user = user_model.create(form_result)
130 130 usr = form_result['username']
131 131 action_logger(self.authuser, 'admin_created_user:%s' % usr,
132 132 None, self.ip_addr, self.sa)
133 133 h.flash(h.literal(_('Created user %s') % h.link_to(h.escape(usr), url('edit_user', id=user.user_id))),
134 134 category='success')
135 135 Session().commit()
136 136 except formencode.Invalid, errors:
137 137 return htmlfill.render(
138 138 render('admin/users/user_add.html'),
139 139 defaults=errors.value,
140 140 errors=errors.error_dict or {},
141 141 prefix_error=False,
142 142 encoding="UTF-8",
143 143 force_defaults=False)
144 144 except UserCreationError, e:
145 145 h.flash(e, 'error')
146 146 except Exception:
147 147 log.error(traceback.format_exc())
148 148 h.flash(_('Error occurred during creation of user %s') \
149 149 % request.POST.get('username'), category='error')
150 150 return redirect(url('users'))
151 151
152 152 def new(self, format='html'):
153 153 """GET /users/new: Form to create a new item"""
154 154 # url('new_user')
155 155 c.default_extern_type = auth_modules.auth_internal.KallitheaAuthPlugin.name
156 156 return render('admin/users/user_add.html')
157 157
158 158 def update(self, id):
159 159 """PUT /users/id: Update an existing item"""
160 160 # Forms posted to this method should contain a hidden field:
161 161 # <input type="hidden" name="_method" value="PUT" />
162 162 # Or using helpers:
163 163 # h.form(url('update_user', id=ID),
164 164 # method='put')
165 165 # url('user', id=ID)
166 166 c.active = 'profile'
167 167 user_model = UserModel()
168 168 c.user = user_model.get(id)
169 169 c.extern_type = c.user.extern_type
170 170 c.extern_name = c.user.extern_name
171 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
171 c.perm_user = AuthUser(user_id=id)
172 c.ip_addr = self.ip_addr
172 173 _form = UserForm(edit=True, old_data={'user_id': id,
173 174 'email': c.user.email})()
174 175 form_result = {}
175 176 try:
176 177 form_result = _form.to_python(dict(request.POST))
177 178 skip_attrs = ['extern_type', 'extern_name']
178 179 #TODO: plugin should define if username can be updated
179 180 if c.extern_type != kallithea.EXTERN_TYPE_INTERNAL:
180 181 # forbid updating username for external accounts
181 182 skip_attrs.append('username')
182 183
183 184 user_model.update(id, form_result, skip_attrs=skip_attrs)
184 185 usr = form_result['username']
185 186 action_logger(self.authuser, 'admin_updated_user:%s' % usr,
186 187 None, self.ip_addr, self.sa)
187 188 h.flash(_('User updated successfully'), category='success')
188 189 Session().commit()
189 190 except formencode.Invalid, errors:
190 191 defaults = errors.value
191 192 e = errors.error_dict or {}
192 193 defaults.update({
193 194 'create_repo_perm': user_model.has_perm(id,
194 195 'hg.create.repository'),
195 196 'fork_repo_perm': user_model.has_perm(id, 'hg.fork.repository'),
196 197 '_method': 'put'
197 198 })
198 199 return htmlfill.render(
199 200 render('admin/users/user_edit.html'),
200 201 defaults=defaults,
201 202 errors=e,
202 203 prefix_error=False,
203 204 encoding="UTF-8",
204 205 force_defaults=False)
205 206 except Exception:
206 207 log.error(traceback.format_exc())
207 208 h.flash(_('Error occurred during update of user %s') \
208 209 % form_result.get('username'), category='error')
209 210 return redirect(url('edit_user', id=id))
210 211
211 212 def delete(self, id):
212 213 """DELETE /users/id: Delete an existing item"""
213 214 # Forms posted to this method should contain a hidden field:
214 215 # <input type="hidden" name="_method" value="DELETE" />
215 216 # Or using helpers:
216 217 # h.form(url('delete_user', id=ID),
217 218 # method='delete')
218 219 # url('user', id=ID)
219 220 usr = User.get_or_404(id)
220 221 try:
221 222 UserModel().delete(usr)
222 223 Session().commit()
223 224 h.flash(_('Successfully deleted user'), category='success')
224 225 except (UserOwnsReposException, DefaultUserException), e:
225 226 h.flash(e, category='warning')
226 227 except Exception:
227 228 log.error(traceback.format_exc())
228 229 h.flash(_('An error occurred during deletion of user'),
229 230 category='error')
230 231 return redirect(url('users'))
231 232
232 233 def show(self, id, format='html'):
233 234 """GET /users/id: Show a specific item"""
234 235 # url('user', id=ID)
235 236 User.get_or_404(-1)
236 237
237 238 def _get_user_or_raise_if_default(self, id):
238 239 try:
239 240 return User.get_or_404(id, allow_default=False)
240 241 except DefaultUserException:
241 242 h.flash(_("The default user cannot be edited"), category='warning')
242 243 raise HTTPNotFound
243 244
244 245 def edit(self, id, format='html'):
245 246 """GET /users/id/edit: Form to edit an existing item"""
246 247 # url('edit_user', id=ID)
247 248 c.user = self._get_user_or_raise_if_default(id)
248 249 c.active = 'profile'
249 250 c.extern_type = c.user.extern_type
250 251 c.extern_name = c.user.extern_name
251 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
252 c.perm_user = AuthUser(user_id=id)
253 c.ip_addr = self.ip_addr
252 254
253 255 defaults = c.user.get_dict()
254 256 return htmlfill.render(
255 257 render('admin/users/user_edit.html'),
256 258 defaults=defaults,
257 259 encoding="UTF-8",
258 260 force_defaults=False)
259 261
260 262 def edit_advanced(self, id):
261 263 c.user = self._get_user_or_raise_if_default(id)
262 264 c.active = 'advanced'
263 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
265 c.perm_user = AuthUser(user_id=id)
266 c.ip_addr = self.ip_addr
264 267
265 268 umodel = UserModel()
266 269 defaults = c.user.get_dict()
267 270 defaults.update({
268 271 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
269 272 'create_user_group_perm': umodel.has_perm(c.user,
270 273 'hg.usergroup.create.true'),
271 274 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
272 275 })
273 276 return htmlfill.render(
274 277 render('admin/users/user_edit.html'),
275 278 defaults=defaults,
276 279 encoding="UTF-8",
277 280 force_defaults=False)
278 281
279 282 def edit_api_keys(self, id):
280 283 c.user = self._get_user_or_raise_if_default(id)
281 284 c.active = 'api_keys'
282 285 show_expired = True
283 286 c.lifetime_values = [
284 287 (str(-1), _('Forever')),
285 288 (str(5), _('5 minutes')),
286 289 (str(60), _('1 hour')),
287 290 (str(60 * 24), _('1 day')),
288 291 (str(60 * 24 * 30), _('1 month')),
289 292 ]
290 293 c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
291 294 c.user_api_keys = ApiKeyModel().get_api_keys(c.user.user_id,
292 295 show_expired=show_expired)
293 296 defaults = c.user.get_dict()
294 297 return htmlfill.render(
295 298 render('admin/users/user_edit.html'),
296 299 defaults=defaults,
297 300 encoding="UTF-8",
298 301 force_defaults=False)
299 302
300 303 def add_api_key(self, id):
301 304 c.user = self._get_user_or_raise_if_default(id)
302 305
303 306 lifetime = safe_int(request.POST.get('lifetime'), -1)
304 307 description = request.POST.get('description')
305 308 ApiKeyModel().create(c.user.user_id, description, lifetime)
306 309 Session().commit()
307 310 h.flash(_("API key successfully created"), category='success')
308 311 return redirect(url('edit_user_api_keys', id=c.user.user_id))
309 312
310 313 def delete_api_key(self, id):
311 314 c.user = self._get_user_or_raise_if_default(id)
312 315
313 316 api_key = request.POST.get('del_api_key')
314 317 if request.POST.get('del_api_key_builtin'):
315 318 user = User.get(c.user.user_id)
316 319 if user:
317 320 user.api_key = generate_api_key(user.username)
318 321 Session().add(user)
319 322 Session().commit()
320 323 h.flash(_("API key successfully reset"), category='success')
321 324 elif api_key:
322 325 ApiKeyModel().delete(api_key, c.user.user_id)
323 326 Session().commit()
324 327 h.flash(_("API key successfully deleted"), category='success')
325 328
326 329 return redirect(url('edit_user_api_keys', id=c.user.user_id))
327 330
328 331 def update_account(self, id):
329 332 pass
330 333
331 334 def edit_perms(self, id):
332 335 c.user = self._get_user_or_raise_if_default(id)
333 336 c.active = 'perms'
334 c.perm_user = AuthUser(user_id=id, ip_addr=self.ip_addr)
337 c.perm_user = AuthUser(user_id=id)
338 c.ip_addr = self.ip_addr
335 339
336 340 umodel = UserModel()
337 341 defaults = c.user.get_dict()
338 342 defaults.update({
339 343 'create_repo_perm': umodel.has_perm(c.user, 'hg.create.repository'),
340 344 'create_user_group_perm': umodel.has_perm(c.user,
341 345 'hg.usergroup.create.true'),
342 346 'fork_repo_perm': umodel.has_perm(c.user, 'hg.fork.repository'),
343 347 })
344 348 return htmlfill.render(
345 349 render('admin/users/user_edit.html'),
346 350 defaults=defaults,
347 351 encoding="UTF-8",
348 352 force_defaults=False)
349 353
350 354 def update_perms(self, id):
351 355 """PUT /users_perm/id: Update an existing item"""
352 356 # url('user_perm', id=ID, method='put')
353 357 user = self._get_user_or_raise_if_default(id)
354 358
355 359 try:
356 360 form = CustomDefaultPermissionsForm()()
357 361 form_result = form.to_python(request.POST)
358 362
359 363 inherit_perms = form_result['inherit_default_permissions']
360 364 user.inherit_default_permissions = inherit_perms
361 365 Session().add(user)
362 366 user_model = UserModel()
363 367
364 368 defs = UserToPerm.query()\
365 369 .filter(UserToPerm.user == user)\
366 370 .all()
367 371 for ug in defs:
368 372 Session().delete(ug)
369 373
370 374 if form_result['create_repo_perm']:
371 375 user_model.grant_perm(id, 'hg.create.repository')
372 376 else:
373 377 user_model.grant_perm(id, 'hg.create.none')
374 378 if form_result['create_user_group_perm']:
375 379 user_model.grant_perm(id, 'hg.usergroup.create.true')
376 380 else:
377 381 user_model.grant_perm(id, 'hg.usergroup.create.false')
378 382 if form_result['fork_repo_perm']:
379 383 user_model.grant_perm(id, 'hg.fork.repository')
380 384 else:
381 385 user_model.grant_perm(id, 'hg.fork.none')
382 386 h.flash(_("Updated permissions"), category='success')
383 387 Session().commit()
384 388 except Exception:
385 389 log.error(traceback.format_exc())
386 390 h.flash(_('An error occurred during permissions saving'),
387 391 category='error')
388 392 return redirect(url('edit_user_perms', id=id))
389 393
390 394 def edit_emails(self, id):
391 395 c.user = self._get_user_or_raise_if_default(id)
392 396 c.active = 'emails'
393 397 c.user_email_map = UserEmailMap.query()\
394 398 .filter(UserEmailMap.user == c.user).all()
395 399
396 400 defaults = c.user.get_dict()
397 401 return htmlfill.render(
398 402 render('admin/users/user_edit.html'),
399 403 defaults=defaults,
400 404 encoding="UTF-8",
401 405 force_defaults=False)
402 406
403 407 def add_email(self, id):
404 408 """POST /user_emails:Add an existing item"""
405 409 # url('user_emails', id=ID, method='put')
406 410 user = self._get_user_or_raise_if_default(id)
407 411 email = request.POST.get('new_email')
408 412 user_model = UserModel()
409 413
410 414 try:
411 415 user_model.add_extra_email(id, email)
412 416 Session().commit()
413 417 h.flash(_("Added email %s to user") % email, category='success')
414 418 except formencode.Invalid, error:
415 419 msg = error.error_dict['email']
416 420 h.flash(msg, category='error')
417 421 except Exception:
418 422 log.error(traceback.format_exc())
419 423 h.flash(_('An error occurred during email saving'),
420 424 category='error')
421 425 return redirect(url('edit_user_emails', id=id))
422 426
423 427 def delete_email(self, id):
424 428 """DELETE /user_emails_delete/id: Delete an existing item"""
425 429 # url('user_emails_delete', id=ID, method='delete')
426 430 user = self._get_user_or_raise_if_default(id)
427 431 email_id = request.POST.get('del_email_id')
428 432 user_model = UserModel()
429 433 user_model.delete_extra_email(id, email_id)
430 434 Session().commit()
431 435 h.flash(_("Removed email from user"), category='success')
432 436 return redirect(url('edit_user_emails', id=id))
433 437
434 438 def edit_ips(self, id):
435 439 c.user = self._get_user_or_raise_if_default(id)
436 440 c.active = 'ips'
437 441 c.user_ip_map = UserIpMap.query()\
438 442 .filter(UserIpMap.user == c.user).all()
439 443
440 444 c.inherit_default_ips = c.user.inherit_default_permissions
441 445 c.default_user_ip_map = UserIpMap.query()\
442 446 .filter(UserIpMap.user == User.get_default_user()).all()
443 447
444 448 defaults = c.user.get_dict()
445 449 return htmlfill.render(
446 450 render('admin/users/user_edit.html'),
447 451 defaults=defaults,
448 452 encoding="UTF-8",
449 453 force_defaults=False)
450 454
451 455 def add_ip(self, id):
452 456 """POST /user_ips:Add an existing item"""
453 457 # url('user_ips', id=ID, method='put')
454 458
455 459 ip = request.POST.get('new_ip')
456 460 user_model = UserModel()
457 461
458 462 try:
459 463 user_model.add_extra_ip(id, ip)
460 464 Session().commit()
461 465 h.flash(_("Added IP address %s to user whitelist") % ip, category='success')
462 466 except formencode.Invalid, error:
463 467 msg = error.error_dict['ip']
464 468 h.flash(msg, category='error')
465 469 except Exception:
466 470 log.error(traceback.format_exc())
467 471 h.flash(_('An error occurred during ip saving'),
468 472 category='error')
469 473
470 474 if 'default_user' in request.POST:
471 475 return redirect(url('admin_permissions_ips'))
472 476 return redirect(url('edit_user_ips', id=id))
473 477
474 478 def delete_ip(self, id):
475 479 """DELETE /user_ips_delete/id: Delete an existing item"""
476 480 # url('user_ips_delete', id=ID, method='delete')
477 481 ip_id = request.POST.get('del_ip_id')
478 482 user_model = UserModel()
479 483 user_model.delete_extra_ip(id, ip_id)
480 484 Session().commit()
481 485 h.flash(_("Removed IP address from user whitelist"), category='success')
482 486
483 487 if 'default_user' in request.POST:
484 488 return redirect(url('admin_permissions_ips'))
485 489 return redirect(url('edit_user_ips', id=id))
@@ -1,303 +1,303 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.api
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 JSON RPC controller
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Aug 20, 2011
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28 import inspect
29 29 import logging
30 30 import types
31 31 import traceback
32 32 import time
33 33
34 34 from paste.response import replace_header
35 35 from pylons.controllers import WSGIController
36 36
37 37 from webob.exc import HTTPError
38 38
39 39 from kallithea.model.db import User
40 40 from kallithea.model import meta
41 41 from kallithea.lib.compat import izip_longest, json
42 42 from kallithea.lib.auth import AuthUser
43 43 from kallithea.lib.base import _get_ip_addr as _get_ip, _get_access_path
44 44 from kallithea.lib.utils2 import safe_unicode, safe_str
45 45
46 46 log = logging.getLogger('JSONRPC')
47 47
48 48
49 49 class JSONRPCError(BaseException):
50 50
51 51 def __init__(self, message):
52 52 self.message = message
53 53 super(JSONRPCError, self).__init__()
54 54
55 55 def __str__(self):
56 56 return safe_str(self.message)
57 57
58 58
59 59 def jsonrpc_error(message, retid=None, code=None):
60 60 """
61 61 Generate a Response object with a JSON-RPC error body
62 62
63 63 :param code:
64 64 :param retid:
65 65 :param message:
66 66 """
67 67 from pylons.controllers.util import Response
68 68 return Response(
69 69 body=json.dumps(dict(id=retid, result=None, error=message)),
70 70 status=code,
71 71 content_type='application/json'
72 72 )
73 73
74 74
75 75 class JSONRPCController(WSGIController):
76 76 """
77 77 A WSGI-speaking JSON-RPC controller class
78 78
79 79 See the specification:
80 80 <http://json-rpc.org/wiki/specification>`.
81 81
82 82 Valid controller return values should be json-serializable objects.
83 83
84 84 Sub-classes should catch their exceptions and raise JSONRPCError
85 85 if they want to pass meaningful errors to the client.
86 86
87 87 """
88 88
89 89 def _get_ip_addr(self, environ):
90 90 return _get_ip(environ)
91 91
92 92 def _get_method_args(self):
93 93 """
94 94 Return `self._rpc_args` to dispatched controller method
95 95 chosen by __call__
96 96 """
97 97 return self._rpc_args
98 98
99 99 def __call__(self, environ, start_response):
100 100 """
101 101 Parse the request body as JSON, look up the method on the
102 102 controller and if it exists, dispatch to it.
103 103 """
104 104 try:
105 105 return self._handle_request(environ, start_response)
106 106 finally:
107 107 meta.Session.remove()
108 108
109 109 def _handle_request(self, environ, start_response):
110 110 start = time.time()
111 111 ip_addr = self.ip_addr = self._get_ip_addr(environ)
112 112 self._req_id = None
113 113 if 'CONTENT_LENGTH' not in environ:
114 114 log.debug("No Content-Length")
115 115 return jsonrpc_error(retid=self._req_id,
116 116 message="No Content-Length in request")
117 117 else:
118 118 length = environ['CONTENT_LENGTH'] or 0
119 119 length = int(environ['CONTENT_LENGTH'])
120 120 log.debug('Content-Length: %s' % length)
121 121
122 122 if length == 0:
123 123 log.debug("Content-Length is 0")
124 124 return jsonrpc_error(retid=self._req_id,
125 125 message="Content-Length is 0")
126 126
127 127 raw_body = environ['wsgi.input'].read(length)
128 128
129 129 try:
130 130 json_body = json.loads(raw_body)
131 131 except ValueError, e:
132 132 # catch JSON errors Here
133 133 return jsonrpc_error(retid=self._req_id,
134 134 message="JSON parse error ERR:%s RAW:%r"
135 135 % (e, raw_body))
136 136
137 137 # check AUTH based on API key
138 138 try:
139 139 self._req_api_key = json_body['api_key']
140 140 self._req_id = json_body['id']
141 141 self._req_method = json_body['method']
142 142 self._request_params = json_body['args']
143 143 if not isinstance(self._request_params, dict):
144 144 self._request_params = {}
145 145
146 146 log.debug(
147 147 'method: %s, params: %s' % (self._req_method,
148 148 self._request_params)
149 149 )
150 150 except KeyError, e:
151 151 return jsonrpc_error(retid=self._req_id,
152 152 message='Incorrect JSON query missing %s' % e)
153 153
154 154 # check if we can find this session using api_key
155 155 try:
156 156 u = User.get_by_api_key(self._req_api_key)
157 157 if u is None:
158 158 return jsonrpc_error(retid=self._req_id,
159 159 message='Invalid API key')
160 160
161 161 #check if we are allowed to use this IP
162 auth_u = AuthUser(u.user_id, self._req_api_key, ip_addr=ip_addr)
163 if not auth_u.ip_allowed:
162 auth_u = AuthUser(u.user_id, self._req_api_key)
163 if not auth_u.is_ip_allowed(ip_addr):
164 164 return jsonrpc_error(retid=self._req_id,
165 165 message='request from IP:%s not allowed' % (ip_addr,))
166 166 else:
167 167 log.info('Access for IP:%s allowed' % (ip_addr,))
168 168
169 169 except Exception, e:
170 170 return jsonrpc_error(retid=self._req_id,
171 171 message='Invalid API key')
172 172
173 173 self._error = None
174 174 try:
175 175 self._func = self._find_method()
176 176 except AttributeError, e:
177 177 return jsonrpc_error(retid=self._req_id,
178 178 message=str(e))
179 179
180 180 # now that we have a method, add self._req_params to
181 181 # self.kargs and dispatch control to WGIController
182 182 argspec = inspect.getargspec(self._func)
183 183 arglist = argspec[0][1:]
184 184 defaults = map(type, argspec[3] or [])
185 185 default_empty = types.NotImplementedType
186 186
187 187 # kw arguments required by this method
188 188 func_kwargs = dict(izip_longest(reversed(arglist), reversed(defaults),
189 189 fillvalue=default_empty))
190 190
191 191 # this is little trick to inject logged in user for
192 192 # perms decorators to work they expect the controller class to have
193 193 # authuser attribute set
194 194 self.authuser = auth_u
195 195
196 196 # This attribute will need to be first param of a method that uses
197 197 # api_key, which is translated to instance of user at that name
198 198 USER_SESSION_ATTR = 'apiuser'
199 199
200 200 if USER_SESSION_ATTR not in arglist:
201 201 return jsonrpc_error(
202 202 retid=self._req_id,
203 203 message='This method [%s] does not support '
204 204 'authentication (missing %s param)' % (
205 205 self._func.__name__, USER_SESSION_ATTR)
206 206 )
207 207
208 208 # get our arglist and check if we provided them as args
209 209 for arg, default in func_kwargs.iteritems():
210 210 if arg == USER_SESSION_ATTR:
211 211 # USER_SESSION_ATTR is something translated from API key and
212 212 # this is checked before so we don't need validate it
213 213 continue
214 214
215 215 # skip the required param check if it's default value is
216 216 # NotImplementedType (default_empty)
217 217 if default == default_empty and arg not in self._request_params:
218 218 return jsonrpc_error(
219 219 retid=self._req_id,
220 220 message=(
221 221 'Missing non optional `%s` arg in JSON DATA' % arg
222 222 )
223 223 )
224 224
225 225 self._rpc_args = {USER_SESSION_ATTR: u}
226 226
227 227 self._rpc_args.update(self._request_params)
228 228
229 229 self._rpc_args['action'] = self._req_method
230 230 self._rpc_args['environ'] = environ
231 231 self._rpc_args['start_response'] = start_response
232 232
233 233 status = []
234 234 headers = []
235 235 exc_info = []
236 236
237 237 def change_content(new_status, new_headers, new_exc_info=None):
238 238 status.append(new_status)
239 239 headers.extend(new_headers)
240 240 exc_info.append(new_exc_info)
241 241
242 242 output = WSGIController.__call__(self, environ, change_content)
243 243 output = list(output)
244 244 headers.append(('Content-Length', str(len(output[0]))))
245 245 replace_header(headers, 'Content-Type', 'application/json')
246 246 start_response(status[0], headers, exc_info[0])
247 247 log.info('IP: %s Request to %s time: %.3fs' % (
248 248 self._get_ip_addr(environ),
249 249 safe_unicode(_get_access_path(environ)), time.time() - start)
250 250 )
251 251 return output
252 252
253 253 def _dispatch_call(self):
254 254 """
255 255 Implement dispatch interface specified by WSGIController
256 256 """
257 257 raw_response = ''
258 258 try:
259 259 raw_response = self._inspect_call(self._func)
260 260 if isinstance(raw_response, HTTPError):
261 261 self._error = str(raw_response)
262 262 except JSONRPCError, e:
263 263 self._error = safe_str(e)
264 264 except Exception, e:
265 265 log.error('Encountered unhandled exception: %s'
266 266 % (traceback.format_exc(),))
267 267 json_exc = JSONRPCError('Internal server error')
268 268 self._error = safe_str(json_exc)
269 269
270 270 if self._error is not None:
271 271 raw_response = None
272 272
273 273 response = dict(id=self._req_id, result=raw_response, error=self._error)
274 274 try:
275 275 return json.dumps(response)
276 276 except TypeError, e:
277 277 log.error('API FAILED. Error encoding response: %s' % e)
278 278 return json.dumps(
279 279 dict(
280 280 id=self._req_id,
281 281 result=None,
282 282 error="Error encoding response"
283 283 )
284 284 )
285 285
286 286 def _find_method(self):
287 287 """
288 288 Return method named by `self._req_method` in controller if able
289 289 """
290 290 log.debug('Trying to find JSON-RPC method: %s' % (self._req_method,))
291 291 if self._req_method.startswith('_'):
292 292 raise AttributeError("Method not allowed")
293 293
294 294 try:
295 295 func = getattr(self, self._req_method, None)
296 296 except UnicodeEncodeError:
297 297 raise AttributeError("Problem decoding unicode in requested "
298 298 "method name.")
299 299
300 300 if isinstance(func, types.MethodType):
301 301 return func
302 302 else:
303 303 raise AttributeError("No such method: %s" % (self._req_method,))
@@ -1,283 +1,283 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.controllers.login
16 16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17 17
18 18 Login controller for Kallithea
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 22, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27
28 28
29 29 import logging
30 30 import formencode
31 31 import datetime
32 32 import urlparse
33 33
34 34 from formencode import htmlfill
35 35 from webob.exc import HTTPFound
36 36 from pylons.i18n.translation import _
37 37 from pylons.controllers.util import redirect
38 38 from pylons import request, session, tmpl_context as c, url
39 39
40 40 import kallithea.lib.helpers as h
41 41 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
42 42 from kallithea.lib.auth_modules import importplugin
43 43 from kallithea.lib.base import BaseController, render
44 44 from kallithea.lib.exceptions import UserCreationError
45 45 from kallithea.lib.utils2 import safe_str
46 46 from kallithea.model.db import User, Setting
47 47 from kallithea.model.forms import LoginForm, RegisterForm, PasswordResetForm
48 48 from kallithea.model.user import UserModel
49 49 from kallithea.model.meta import Session
50 50
51 51
52 52 log = logging.getLogger(__name__)
53 53
54 54
55 55 class LoginController(BaseController):
56 56
57 57 def __before__(self):
58 58 super(LoginController, self).__before__()
59 59
60 60 def _store_user_in_session(self, username, remember=False):
61 61 user = User.get_by_username(username, case_insensitive=True)
62 62 auth_user = AuthUser(user.user_id)
63 63 auth_user.set_authenticated()
64 64 cs = auth_user.get_cookie_store()
65 65 session['authuser'] = cs
66 66 user.update_lastlogin()
67 67 Session().commit()
68 68
69 69 # If they want to be remembered, update the cookie
70 70 if remember:
71 71 _year = (datetime.datetime.now() +
72 72 datetime.timedelta(seconds=60 * 60 * 24 * 365))
73 73 session._set_cookie_expires(_year)
74 74
75 75 session.save()
76 76
77 77 log.info('user %s is now authenticated and stored in '
78 78 'session, session attrs %s' % (username, cs))
79 79
80 80 # dumps session attrs back to cookie
81 81 session._update_cookie_out()
82 82
83 83 def _validate_came_from(self, came_from):
84 84 """Return True if came_from is valid and can and should be used"""
85 85 if not came_from:
86 86 return False
87 87
88 88 parsed = urlparse.urlparse(came_from)
89 89 server_parsed = urlparse.urlparse(url.current())
90 90 allowed_schemes = ['http', 'https']
91 91 if parsed.scheme and parsed.scheme not in allowed_schemes:
92 92 log.error('Suspicious URL scheme detected %s for url %s' %
93 93 (parsed.scheme, parsed))
94 94 return False
95 95 if server_parsed.netloc != parsed.netloc:
96 96 log.error('Suspicious NETLOC detected %s for url %s server url '
97 97 'is: %s' % (parsed.netloc, parsed, server_parsed))
98 98 return False
99 99 return True
100 100
101 101 def _redirect_to_origin(self, origin):
102 102 '''redirect to the original page, preserving any get arguments given'''
103 103 request.GET.pop('came_from', None)
104 104 raise HTTPFound(location=url(origin, **request.GET))
105 105
106 106 def index(self):
107 107 c.came_from = safe_str(request.GET.get('came_from', ''))
108 108 if not self._validate_came_from(c.came_from):
109 109 c.came_from = url('home')
110 110
111 111 not_default = self.authuser.username != User.DEFAULT_USER
112 ip_allowed = self.authuser.ip_allowed
112 ip_allowed = self.authuser.is_ip_allowed(self.ip_addr)
113 113
114 114 # redirect if already logged in
115 115 if self.authuser.is_authenticated and not_default and ip_allowed:
116 116 return self._redirect_to_origin(c.came_from)
117 117
118 118 if request.POST:
119 119 # import Login Form validator class
120 120 login_form = LoginForm()
121 121 try:
122 122 session.invalidate()
123 123 c.form_result = login_form.to_python(dict(request.POST))
124 124 # form checks for username/password, now we're authenticated
125 125 self._store_user_in_session(
126 126 username=c.form_result['username'],
127 127 remember=c.form_result['remember'])
128 128 return self._redirect_to_origin(c.came_from)
129 129
130 130 except formencode.Invalid, errors:
131 131 defaults = errors.value
132 132 # remove password from filling in form again
133 133 del defaults['password']
134 134 return htmlfill.render(
135 135 render('/login.html'),
136 136 defaults=errors.value,
137 137 errors=errors.error_dict or {},
138 138 prefix_error=False,
139 139 encoding="UTF-8",
140 140 force_defaults=False)
141 141 except UserCreationError, e:
142 142 # container auth or other auth functions that create users on
143 143 # the fly can throw this exception signaling that there's issue
144 144 # with user creation, explanation should be provided in
145 145 # Exception itself
146 146 h.flash(e, 'error')
147 147
148 148 # check if we use container plugin, and try to login using it.
149 149 auth_plugins = Setting.get_auth_plugins()
150 150 if any((importplugin(name).is_container_auth for name in auth_plugins)):
151 151 from kallithea.lib import auth_modules
152 152 try:
153 153 auth_info = auth_modules.authenticate('', '', request.environ)
154 154 except UserCreationError, e:
155 155 log.error(e)
156 156 h.flash(e, 'error')
157 157 # render login, with flash message about limit
158 158 return render('/login.html')
159 159
160 160 if auth_info:
161 161 self._store_user_in_session(auth_info.get('username'))
162 162 return self._redirect_to_origin(c.came_from)
163 163
164 164 return render('/login.html')
165 165
166 166 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
167 167 'hg.register.manual_activate')
168 168 def register(self):
169 169 c.auto_active = 'hg.register.auto_activate' in User.get_default_user()\
170 170 .AuthUser.permissions['global']
171 171
172 172 settings = Setting.get_app_settings()
173 173 captcha_private_key = settings.get('captcha_private_key')
174 174 c.captcha_active = bool(captcha_private_key)
175 175 c.captcha_public_key = settings.get('captcha_public_key')
176 176
177 177 if request.POST:
178 178 register_form = RegisterForm()()
179 179 try:
180 180 form_result = register_form.to_python(dict(request.POST))
181 181 form_result['active'] = c.auto_active
182 182
183 183 if c.captcha_active:
184 184 from kallithea.lib.recaptcha import submit
185 185 response = submit(request.POST.get('recaptcha_challenge_field'),
186 186 request.POST.get('recaptcha_response_field'),
187 187 private_key=captcha_private_key,
188 188 remoteip=self.ip_addr)
189 189 if c.captcha_active and not response.is_valid:
190 190 _value = form_result
191 191 _msg = _('Bad captcha')
192 192 error_dict = {'recaptcha_field': _msg}
193 193 raise formencode.Invalid(_msg, _value, None,
194 194 error_dict=error_dict)
195 195
196 196 UserModel().create_registration(form_result)
197 197 h.flash(_('You have successfully registered into Kallithea'),
198 198 category='success')
199 199 Session().commit()
200 200 return redirect(url('login_home'))
201 201
202 202 except formencode.Invalid, errors:
203 203 return htmlfill.render(
204 204 render('/register.html'),
205 205 defaults=errors.value,
206 206 errors=errors.error_dict or {},
207 207 prefix_error=False,
208 208 encoding="UTF-8",
209 209 force_defaults=False)
210 210 except UserCreationError, e:
211 211 # container auth or other auth functions that create users on
212 212 # the fly can throw this exception signaling that there's issue
213 213 # with user creation, explanation should be provided in
214 214 # Exception itself
215 215 h.flash(e, 'error')
216 216
217 217 return render('/register.html')
218 218
219 219 def password_reset(self):
220 220 settings = Setting.get_app_settings()
221 221 captcha_private_key = settings.get('captcha_private_key')
222 222 c.captcha_active = bool(captcha_private_key)
223 223 c.captcha_public_key = settings.get('captcha_public_key')
224 224
225 225 if request.POST:
226 226 password_reset_form = PasswordResetForm()()
227 227 try:
228 228 form_result = password_reset_form.to_python(dict(request.POST))
229 229 if c.captcha_active:
230 230 from kallithea.lib.recaptcha import submit
231 231 response = submit(request.POST.get('recaptcha_challenge_field'),
232 232 request.POST.get('recaptcha_response_field'),
233 233 private_key=captcha_private_key,
234 234 remoteip=self.ip_addr)
235 235 if c.captcha_active and not response.is_valid:
236 236 _value = form_result
237 237 _msg = _('Bad captcha')
238 238 error_dict = {'recaptcha_field': _msg}
239 239 raise formencode.Invalid(_msg, _value, None,
240 240 error_dict=error_dict)
241 241 UserModel().reset_password_link(form_result)
242 242 h.flash(_('Your password reset link was sent'),
243 243 category='success')
244 244 return redirect(url('login_home'))
245 245
246 246 except formencode.Invalid, errors:
247 247 return htmlfill.render(
248 248 render('/password_reset.html'),
249 249 defaults=errors.value,
250 250 errors=errors.error_dict or {},
251 251 prefix_error=False,
252 252 encoding="UTF-8",
253 253 force_defaults=False)
254 254
255 255 return render('/password_reset.html')
256 256
257 257 def password_reset_confirmation(self):
258 258 if request.GET and request.GET.get('key'):
259 259 try:
260 260 user = User.get_by_api_key(request.GET.get('key'))
261 261 data = dict(email=user.email)
262 262 UserModel().reset_password(data)
263 263 h.flash(_('Your password reset was successful, '
264 264 'new password has been sent to your email'),
265 265 category='success')
266 266 except Exception, e:
267 267 log.error(e)
268 268 return redirect(url('reset_password'))
269 269
270 270 return redirect(url('login_home'))
271 271
272 272 def logout(self):
273 273 session.delete()
274 274 log.info('Logging out and deleting session for user')
275 275 redirect(url('home'))
276 276
277 277 def authentication_token(self):
278 278 """Return the CSRF protection token for the session - just like it
279 279 could have been screen scrabed from a page with a form.
280 280 Only intended for testing but might also be useful for other kinds
281 281 of automation.
282 282 """
283 283 return h.authentication_token()
@@ -1,1293 +1,1288 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 """
15 15 kallithea.lib.auth
16 16 ~~~~~~~~~~~~~~~~~~
17 17
18 18 authentication and permission libraries
19 19
20 20 This file was forked by the Kallithea project in July 2014.
21 21 Original author and date, and relevant copyright and licensing information is below:
22 22 :created_on: Apr 4, 2010
23 23 :author: marcink
24 24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 25 :license: GPLv3, see LICENSE.md for more details.
26 26 """
27 27 from __future__ import with_statement
28 28 import time
29 29 import random
30 30 import logging
31 31 import traceback
32 32 import hashlib
33 33 import itertools
34 34 import collections
35 35
36 36 from tempfile import _RandomNameSequence
37 37 from decorator import decorator
38 38
39 39 from pylons import url, request
40 40 from pylons.controllers.util import abort, redirect
41 41 from pylons.i18n.translation import _
42 42 from webhelpers.pylonslib import secure_form
43 43 from sqlalchemy import or_
44 44 from sqlalchemy.orm.exc import ObjectDeletedError
45 45 from sqlalchemy.orm import joinedload
46 46
47 47 from kallithea import __platform__, is_windows, is_unix
48 48 from kallithea.lib.vcs.utils.lazy import LazyProperty
49 49 from kallithea.model import meta
50 50 from kallithea.model.meta import Session
51 51 from kallithea.model.user import UserModel
52 52 from kallithea.model.db import User, Repository, Permission, \
53 53 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
54 54 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
55 55 UserGroup, UserApiKeys
56 56
57 57 from kallithea.lib.utils2 import safe_unicode, aslist
58 58 from kallithea.lib.utils import get_repo_slug, get_repo_group_slug, \
59 59 get_user_group_slug, conditional_cache
60 60 from kallithea.lib.caching_query import FromCache
61 61
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 class PasswordGenerator(object):
67 67 """
68 68 This is a simple class for generating password from different sets of
69 69 characters
70 70 usage::
71 71
72 72 passwd_gen = PasswordGenerator()
73 73 #print 8-letter password containing only big and small letters
74 74 of alphabet
75 75 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
76 76 """
77 77 ALPHABETS_NUM = r'''1234567890'''
78 78 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
79 79 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
80 80 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
81 81 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
82 82 + ALPHABETS_NUM + ALPHABETS_SPECIAL
83 83 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
84 84 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
85 85 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
86 86 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
87 87
88 88 def __init__(self, passwd=''):
89 89 self.passwd = passwd
90 90
91 91 def gen_password(self, length, type_=None):
92 92 if type_ is None:
93 93 type_ = self.ALPHABETS_FULL
94 94 self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
95 95 return self.passwd
96 96
97 97
98 98 class KallitheaCrypto(object):
99 99
100 100 @classmethod
101 101 def hash_string(cls, str_):
102 102 """
103 103 Cryptographic function used for password hashing based on pybcrypt
104 104 or pycrypto in windows
105 105
106 106 :param password: password to hash
107 107 """
108 108 if is_windows:
109 109 from hashlib import sha256
110 110 return sha256(str_).hexdigest()
111 111 elif is_unix:
112 112 import bcrypt
113 113 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
114 114 else:
115 115 raise Exception('Unknown or unsupported platform %s' \
116 116 % __platform__)
117 117
118 118 @classmethod
119 119 def hash_check(cls, password, hashed):
120 120 """
121 121 Checks matching password with it's hashed value, runs different
122 122 implementation based on platform it runs on
123 123
124 124 :param password: password
125 125 :param hashed: password in hashed form
126 126 """
127 127
128 128 if is_windows:
129 129 from hashlib import sha256
130 130 return sha256(password).hexdigest() == hashed
131 131 elif is_unix:
132 132 import bcrypt
133 133 return bcrypt.hashpw(password, hashed) == hashed
134 134 else:
135 135 raise Exception('Unknown or unsupported platform %s' \
136 136 % __platform__)
137 137
138 138
139 139 def get_crypt_password(password):
140 140 return KallitheaCrypto.hash_string(password)
141 141
142 142
143 143 def check_password(password, hashed):
144 144 return KallitheaCrypto.hash_check(password, hashed)
145 145
146 146 class CookieStoreWrapper(object):
147 147
148 148 def __init__(self, cookie_store):
149 149 self.cookie_store = cookie_store
150 150
151 151 def __repr__(self):
152 152 return 'CookieStore<%s>' % (self.cookie_store)
153 153
154 154 def get(self, key, other=None):
155 155 if isinstance(self.cookie_store, dict):
156 156 return self.cookie_store.get(key, other)
157 157 elif isinstance(self.cookie_store, AuthUser):
158 158 return self.cookie_store.__dict__.get(key, other)
159 159
160 160
161 161
162 162 def _cached_perms_data(user_id, user_is_admin, user_inherit_default_permissions,
163 163 explicit, algo):
164 164 RK = 'repositories'
165 165 GK = 'repositories_groups'
166 166 UK = 'user_groups'
167 167 GLOBAL = 'global'
168 168 PERM_WEIGHTS = Permission.PERM_WEIGHTS
169 169 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
170 170
171 171 def _choose_perm(new_perm, cur_perm):
172 172 new_perm_val = PERM_WEIGHTS[new_perm]
173 173 cur_perm_val = PERM_WEIGHTS[cur_perm]
174 174 if algo == 'higherwin':
175 175 if new_perm_val > cur_perm_val:
176 176 return new_perm
177 177 return cur_perm
178 178 elif algo == 'lowerwin':
179 179 if new_perm_val < cur_perm_val:
180 180 return new_perm
181 181 return cur_perm
182 182
183 183 #======================================================================
184 184 # fetch default permissions
185 185 #======================================================================
186 186 default_user = User.get_by_username('default', cache=True)
187 187 default_user_id = default_user.user_id
188 188
189 189 default_repo_perms = Permission.get_default_perms(default_user_id)
190 190 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
191 191 default_user_group_perms = Permission.get_default_user_group_perms(default_user_id)
192 192
193 193 if user_is_admin:
194 194 #==================================================================
195 195 # admin user have all default rights for repositories
196 196 # and groups set to admin
197 197 #==================================================================
198 198 permissions[GLOBAL].add('hg.admin')
199 199 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
200 200
201 201 # repositories
202 202 for perm in default_repo_perms:
203 203 r_k = perm.UserRepoToPerm.repository.repo_name
204 204 p = 'repository.admin'
205 205 permissions[RK][r_k] = p
206 206
207 207 # repository groups
208 208 for perm in default_repo_groups_perms:
209 209 rg_k = perm.UserRepoGroupToPerm.group.group_name
210 210 p = 'group.admin'
211 211 permissions[GK][rg_k] = p
212 212
213 213 # user groups
214 214 for perm in default_user_group_perms:
215 215 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
216 216 p = 'usergroup.admin'
217 217 permissions[UK][u_k] = p
218 218 return permissions
219 219
220 220 #==================================================================
221 221 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
222 222 #==================================================================
223 223 uid = user_id
224 224
225 225 # default global permissions taken from the default user
226 226 default_global_perms = UserToPerm.query()\
227 227 .filter(UserToPerm.user_id == default_user_id)\
228 228 .options(joinedload(UserToPerm.permission))
229 229
230 230 for perm in default_global_perms:
231 231 permissions[GLOBAL].add(perm.permission.permission_name)
232 232
233 233 # defaults for repositories, taken from default user
234 234 for perm in default_repo_perms:
235 235 r_k = perm.UserRepoToPerm.repository.repo_name
236 236 if perm.Repository.private and not (perm.Repository.user_id == uid):
237 237 # disable defaults for private repos,
238 238 p = 'repository.none'
239 239 elif perm.Repository.user_id == uid:
240 240 # set admin if owner
241 241 p = 'repository.admin'
242 242 else:
243 243 p = perm.Permission.permission_name
244 244
245 245 permissions[RK][r_k] = p
246 246
247 247 # defaults for repository groups taken from default user permission
248 248 # on given group
249 249 for perm in default_repo_groups_perms:
250 250 rg_k = perm.UserRepoGroupToPerm.group.group_name
251 251 p = perm.Permission.permission_name
252 252 permissions[GK][rg_k] = p
253 253
254 254 # defaults for user groups taken from default user permission
255 255 # on given user group
256 256 for perm in default_user_group_perms:
257 257 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
258 258 p = perm.Permission.permission_name
259 259 permissions[UK][u_k] = p
260 260
261 261 #======================================================================
262 262 # !! OVERRIDE GLOBALS !! with user permissions if any found
263 263 #======================================================================
264 264 # those can be configured from groups or users explicitly
265 265 _configurable = set([
266 266 'hg.fork.none', 'hg.fork.repository',
267 267 'hg.create.none', 'hg.create.repository',
268 268 'hg.usergroup.create.false', 'hg.usergroup.create.true'
269 269 ])
270 270
271 271 # USER GROUPS comes first
272 272 # user group global permissions
273 273 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
274 274 .options(joinedload(UserGroupToPerm.permission))\
275 275 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
276 276 UserGroupMember.users_group_id))\
277 277 .filter(UserGroupMember.user_id == uid)\
278 278 .order_by(UserGroupToPerm.users_group_id)\
279 279 .all()
280 280 # need to group here by groups since user can be in more than
281 281 # one group
282 282 _grouped = [[x, list(y)] for x, y in
283 283 itertools.groupby(user_perms_from_users_groups,
284 284 lambda x:x.users_group)]
285 285 for gr, perms in _grouped:
286 286 # since user can be in multiple groups iterate over them and
287 287 # select the lowest permissions first (more explicit)
288 288 ##TODO: do this^^
289 289 if not gr.inherit_default_permissions:
290 290 # NEED TO IGNORE all configurable permissions and
291 291 # replace them with explicitly set
292 292 permissions[GLOBAL] = permissions[GLOBAL]\
293 293 .difference(_configurable)
294 294 for perm in perms:
295 295 permissions[GLOBAL].add(perm.permission.permission_name)
296 296
297 297 # user specific global permissions
298 298 user_perms = Session().query(UserToPerm)\
299 299 .options(joinedload(UserToPerm.permission))\
300 300 .filter(UserToPerm.user_id == uid).all()
301 301
302 302 if not user_inherit_default_permissions:
303 303 # NEED TO IGNORE all configurable permissions and
304 304 # replace them with explicitly set
305 305 permissions[GLOBAL] = permissions[GLOBAL]\
306 306 .difference(_configurable)
307 307
308 308 for perm in user_perms:
309 309 permissions[GLOBAL].add(perm.permission.permission_name)
310 310 ## END GLOBAL PERMISSIONS
311 311
312 312 #======================================================================
313 313 # !! PERMISSIONS FOR REPOSITORIES !!
314 314 #======================================================================
315 315 #======================================================================
316 316 # check if user is part of user groups for this repository and
317 317 # fill in his permission from it. _choose_perm decides of which
318 318 # permission should be selected based on selected method
319 319 #======================================================================
320 320
321 321 # user group for repositories permissions
322 322 user_repo_perms_from_users_groups = \
323 323 Session().query(UserGroupRepoToPerm, Permission, Repository,)\
324 324 .join((Repository, UserGroupRepoToPerm.repository_id ==
325 325 Repository.repo_id))\
326 326 .join((Permission, UserGroupRepoToPerm.permission_id ==
327 327 Permission.permission_id))\
328 328 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
329 329 UserGroupMember.users_group_id))\
330 330 .filter(UserGroupMember.user_id == uid)\
331 331 .all()
332 332
333 333 multiple_counter = collections.defaultdict(int)
334 334 for perm in user_repo_perms_from_users_groups:
335 335 r_k = perm.UserGroupRepoToPerm.repository.repo_name
336 336 multiple_counter[r_k] += 1
337 337 p = perm.Permission.permission_name
338 338 cur_perm = permissions[RK][r_k]
339 339
340 340 if perm.Repository.user_id == uid:
341 341 # set admin if owner
342 342 p = 'repository.admin'
343 343 else:
344 344 if multiple_counter[r_k] > 1:
345 345 p = _choose_perm(p, cur_perm)
346 346 permissions[RK][r_k] = p
347 347
348 348 # user explicit permissions for repositories, overrides any specified
349 349 # by the group permission
350 350 user_repo_perms = Permission.get_default_perms(uid)
351 351 for perm in user_repo_perms:
352 352 r_k = perm.UserRepoToPerm.repository.repo_name
353 353 cur_perm = permissions[RK][r_k]
354 354 # set admin if owner
355 355 if perm.Repository.user_id == uid:
356 356 p = 'repository.admin'
357 357 else:
358 358 p = perm.Permission.permission_name
359 359 if not explicit:
360 360 p = _choose_perm(p, cur_perm)
361 361 permissions[RK][r_k] = p
362 362
363 363 #======================================================================
364 364 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
365 365 #======================================================================
366 366 #======================================================================
367 367 # check if user is part of user groups for this repository groups and
368 368 # fill in his permission from it. _choose_perm decides of which
369 369 # permission should be selected based on selected method
370 370 #======================================================================
371 371 # user group for repo groups permissions
372 372 user_repo_group_perms_from_users_groups = \
373 373 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup)\
374 374 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
375 375 .join((Permission, UserGroupRepoGroupToPerm.permission_id
376 376 == Permission.permission_id))\
377 377 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
378 378 == UserGroupMember.users_group_id))\
379 379 .filter(UserGroupMember.user_id == uid)\
380 380 .all()
381 381
382 382 multiple_counter = collections.defaultdict(int)
383 383 for perm in user_repo_group_perms_from_users_groups:
384 384 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
385 385 multiple_counter[g_k] += 1
386 386 p = perm.Permission.permission_name
387 387 cur_perm = permissions[GK][g_k]
388 388 if multiple_counter[g_k] > 1:
389 389 p = _choose_perm(p, cur_perm)
390 390 permissions[GK][g_k] = p
391 391
392 392 # user explicit permissions for repository groups
393 393 user_repo_groups_perms = Permission.get_default_group_perms(uid)
394 394 for perm in user_repo_groups_perms:
395 395 rg_k = perm.UserRepoGroupToPerm.group.group_name
396 396 p = perm.Permission.permission_name
397 397 cur_perm = permissions[GK][rg_k]
398 398 if not explicit:
399 399 p = _choose_perm(p, cur_perm)
400 400 permissions[GK][rg_k] = p
401 401
402 402 #======================================================================
403 403 # !! PERMISSIONS FOR USER GROUPS !!
404 404 #======================================================================
405 405 # user group for user group permissions
406 406 user_group_user_groups_perms = \
407 407 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup)\
408 408 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
409 409 == UserGroup.users_group_id))\
410 410 .join((Permission, UserGroupUserGroupToPerm.permission_id
411 411 == Permission.permission_id))\
412 412 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
413 413 == UserGroupMember.users_group_id))\
414 414 .filter(UserGroupMember.user_id == uid)\
415 415 .all()
416 416
417 417 multiple_counter = collections.defaultdict(int)
418 418 for perm in user_group_user_groups_perms:
419 419 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
420 420 multiple_counter[g_k] += 1
421 421 p = perm.Permission.permission_name
422 422 cur_perm = permissions[UK][g_k]
423 423 if multiple_counter[g_k] > 1:
424 424 p = _choose_perm(p, cur_perm)
425 425 permissions[UK][g_k] = p
426 426
427 427 #user explicit permission for user groups
428 428 user_user_groups_perms = Permission.get_default_user_group_perms(uid)
429 429 for perm in user_user_groups_perms:
430 430 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
431 431 p = perm.Permission.permission_name
432 432 cur_perm = permissions[UK][u_k]
433 433 if not explicit:
434 434 p = _choose_perm(p, cur_perm)
435 435 permissions[UK][u_k] = p
436 436
437 437 return permissions
438 438
439 439
440 440 def allowed_api_access(controller_name, whitelist=None, api_key=None):
441 441 """
442 442 Check if given controller_name is in whitelist API access
443 443 """
444 444 if not whitelist:
445 445 from kallithea import CONFIG
446 446 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
447 447 sep=',')
448 448 log.debug('whitelist of API access is: %s' % (whitelist))
449 449 api_access_valid = controller_name in whitelist
450 450 if api_access_valid:
451 451 log.debug('controller:%s is in API whitelist' % (controller_name))
452 452 else:
453 453 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
454 454 if api_key:
455 455 #if we use API key and don't have access it's a warning
456 456 log.warning(msg)
457 457 else:
458 458 log.debug(msg)
459 459 return api_access_valid
460 460
461 461
462 462 class AuthUser(object):
463 463 """
464 464 A simple object that handles all attributes of user in Kallithea
465 465
466 466 It does lookup based on API key,given user, or user present in session
467 467 Then it fills all required information for such user. It also checks if
468 468 anonymous access is enabled and if so, it returns default user as logged in
469 469 """
470 470
471 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
471 def __init__(self, user_id=None, api_key=None, username=None):
472 472
473 473 self.user_id = user_id
474 474 self._api_key = api_key
475 475
476 476 self.api_key = None
477 477 self.username = username
478 self.ip_addr = ip_addr
479 478 self.name = ''
480 479 self.lastname = ''
481 480 self.email = ''
482 481 self.is_authenticated = False
483 482 self.admin = False
484 483 self.inherit_default_permissions = False
485 484
486 485 self.propagate_data()
487 486 self._instance = None
488 487
489 488 @LazyProperty
490 489 def permissions(self):
491 490 return self.get_perms(user=self, cache=False)
492 491
493 492 @property
494 493 def api_keys(self):
495 494 return self.get_api_keys()
496 495
497 496 def propagate_data(self):
498 497 user_model = UserModel()
499 498 self.anonymous_user = User.get_default_user(cache=True)
500 499 is_user_loaded = False
501 500
502 501 # lookup by userid
503 502 if self.user_id is not None and self.user_id != self.anonymous_user.user_id:
504 503 log.debug('Auth User lookup by USER ID %s' % self.user_id)
505 504 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
506 505
507 506 # try go get user by API key
508 507 elif self._api_key and self._api_key != self.anonymous_user.api_key:
509 508 log.debug('Auth User lookup by API key %s' % self._api_key)
510 509 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
511 510
512 511 # lookup by username
513 512 elif self.username:
514 513 log.debug('Auth User lookup by USER NAME %s' % self.username)
515 514 is_user_loaded = user_model.fill_data(self, username=self.username)
516 515 else:
517 516 log.debug('No data in %s that could been used to log in' % self)
518 517
519 518 if not is_user_loaded:
520 519 # if we cannot authenticate user try anonymous
521 520 if self.anonymous_user.active:
522 521 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
523 522 # then we set this user is logged in
524 523 self.is_authenticated = True
525 524 else:
526 525 self.user_id = None
527 526 self.username = None
528 527 self.is_authenticated = False
529 528
530 529 if not self.username:
531 530 self.username = 'None'
532 531
533 532 log.debug('Auth User is now %s' % self)
534 533
535 534 def get_perms(self, user, explicit=True, algo='higherwin', cache=False):
536 535 """
537 536 Fills user permission attribute with permissions taken from database
538 537 works for permissions given for repositories, and for permissions that
539 538 are granted to groups
540 539
541 540 :param user: instance of User object from database
542 541 :param explicit: In case there are permissions both for user and a group
543 542 that user is part of, explicit flag will define if user will
544 543 explicitly override permissions from group, if it's False it will
545 544 make decision based on the algo
546 545 :param algo: algorithm to decide what permission should be choose if
547 546 it's multiple defined, eg user in two different groups. It also
548 547 decides if explicit flag is turned off how to specify the permission
549 548 for case when user is in a group + have defined separate permission
550 549 """
551 550 user_id = user.user_id
552 551 user_is_admin = user.is_admin
553 552 user_inherit_default_permissions = user.inherit_default_permissions
554 553
555 554 log.debug('Getting PERMISSION tree')
556 555 compute = conditional_cache('short_term', 'cache_desc',
557 556 condition=cache, func=_cached_perms_data)
558 557 return compute(user_id, user_is_admin,
559 558 user_inherit_default_permissions, explicit, algo)
560 559
561 560 def get_api_keys(self):
562 561 api_keys = [self.api_key]
563 562 for api_key in UserApiKeys.query()\
564 563 .filter(UserApiKeys.user_id == self.user_id)\
565 564 .filter(or_(UserApiKeys.expires == -1,
566 565 UserApiKeys.expires >= time.time())).all():
567 566 api_keys.append(api_key.api_key)
568 567
569 568 return api_keys
570 569
571 570 @property
572 571 def is_admin(self):
573 572 return self.admin
574 573
575 574 @property
576 575 def repositories_admin(self):
577 576 """
578 577 Returns list of repositories you're an admin of
579 578 """
580 579 return [x[0] for x in self.permissions['repositories'].iteritems()
581 580 if x[1] == 'repository.admin']
582 581
583 582 @property
584 583 def repository_groups_admin(self):
585 584 """
586 585 Returns list of repository groups you're an admin of
587 586 """
588 587 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
589 588 if x[1] == 'group.admin']
590 589
591 590 @property
592 591 def user_groups_admin(self):
593 592 """
594 593 Returns list of user groups you're an admin of
595 594 """
596 595 return [x[0] for x in self.permissions['user_groups'].iteritems()
597 596 if x[1] == 'usergroup.admin']
598 597
599 @property
600 def ip_allowed(self):
598 def is_ip_allowed(self, ip_addr):
601 599 """
602 Checks if ip_addr used in constructor is allowed from defined list of
603 allowed ip_addresses for user
604
605 :returns: boolean, True if ip is in allowed ip range
600 Determine if `ip_addr` is on the list of allowed IP addresses
601 for this user.
606 602 """
607 # check IP
608 603 inherit = self.inherit_default_permissions
609 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
604 return AuthUser.check_ip_allowed(self.user_id, ip_addr,
610 605 inherit_from_default=inherit)
611 606
612 607 @classmethod
613 608 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
614 609 allowed_ips = AuthUser.get_allowed_ips(user_id, cache=True,
615 610 inherit_from_default=inherit_from_default)
616 611 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
617 612 log.debug('IP:%s is in range of %s' % (ip_addr, allowed_ips))
618 613 return True
619 614 else:
620 615 log.info('Access for IP:%s forbidden, '
621 616 'not in %s' % (ip_addr, allowed_ips))
622 617 return False
623 618
624 619 def __repr__(self):
625 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
626 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
620 return "<AuthUser('id:%s[%s] auth:%s')>"\
621 % (self.user_id, self.username, self.is_authenticated)
627 622
628 623 def set_authenticated(self, authenticated=True):
629 624 if self.user_id != self.anonymous_user.user_id:
630 625 self.is_authenticated = authenticated
631 626
632 627 def get_cookie_store(self):
633 628 return {'username': self.username,
634 629 'user_id': self.user_id,
635 630 'is_authenticated': self.is_authenticated}
636 631
637 632 @classmethod
638 633 def from_cookie_store(cls, cookie_store):
639 634 """
640 635 Creates AuthUser from a cookie store
641 636
642 637 :param cls:
643 638 :param cookie_store:
644 639 """
645 640 user_id = cookie_store.get('user_id')
646 641 username = cookie_store.get('username')
647 642 api_key = cookie_store.get('api_key')
648 643 return AuthUser(user_id, api_key, username)
649 644
650 645 @classmethod
651 646 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
652 647 _set = set()
653 648
654 649 if inherit_from_default:
655 650 default_ips = UserIpMap.query().filter(UserIpMap.user ==
656 651 User.get_default_user(cache=True))
657 652 if cache:
658 653 default_ips = default_ips.options(FromCache("sql_cache_short",
659 654 "get_user_ips_default"))
660 655
661 656 # populate from default user
662 657 for ip in default_ips:
663 658 try:
664 659 _set.add(ip.ip_addr)
665 660 except ObjectDeletedError:
666 661 # since we use heavy caching sometimes it happens that we get
667 662 # deleted objects here, we just skip them
668 663 pass
669 664
670 665 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
671 666 if cache:
672 667 user_ips = user_ips.options(FromCache("sql_cache_short",
673 668 "get_user_ips_%s" % user_id))
674 669
675 670 for ip in user_ips:
676 671 try:
677 672 _set.add(ip.ip_addr)
678 673 except ObjectDeletedError:
679 674 # since we use heavy caching sometimes it happens that we get
680 675 # deleted objects here, we just skip them
681 676 pass
682 677 return _set or set(['0.0.0.0/0', '::/0'])
683 678
684 679
685 680 def set_available_permissions(config):
686 681 """
687 682 This function will propagate pylons globals with all available defined
688 683 permission given in db. We don't want to check each time from db for new
689 684 permissions since adding a new permission also requires application restart
690 685 ie. to decorate new views with the newly created permission
691 686
692 687 :param config: current pylons config instance
693 688
694 689 """
695 690 log.info('getting information about all available permissions')
696 691 try:
697 692 sa = meta.Session
698 693 all_perms = sa.query(Permission).all()
699 694 config['available_permissions'] = [x.permission_name for x in all_perms]
700 695 finally:
701 696 meta.Session.remove()
702 697
703 698
704 699 #==============================================================================
705 700 # CHECK DECORATORS
706 701 #==============================================================================
707 702
708 703 def redirect_to_login(message=None):
709 704 from kallithea.lib import helpers as h
710 705 p = url.current()
711 706 if message:
712 707 h.flash(h.literal(message), category='warning')
713 708 log.debug('Redirecting to login page, origin: %s' % p)
714 709 return redirect(url('login_home', came_from=p, **request.GET))
715 710
716 711 class LoginRequired(object):
717 712 """
718 713 Must be logged in to execute this function else
719 714 redirect to login page
720 715
721 716 :param api_access: if enabled this checks only for valid auth token
722 717 and grants access based on valid token
723 718 """
724 719
725 720 def __init__(self, api_access=False):
726 721 self.api_access = api_access
727 722
728 723 def __call__(self, func):
729 724 return decorator(self.__wrapper, func)
730 725
731 726 def __wrapper(self, func, *fargs, **fkwargs):
732 cls = fargs[0]
733 user = cls.authuser
734 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
727 controller = fargs[0]
728 user = controller.authuser
729 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
735 730 log.debug('Checking access for user %s @ %s' % (user, loc))
736 731
737 732 # check if our IP is allowed
738 if not user.ip_allowed:
739 return redirect_to_login(_('IP %s not allowed' % (user.ip_addr)))
733 if not user.is_ip_allowed(controller.ip_addr):
734 return redirect_to_login(_('IP %s not allowed') % controller.ip_addr)
740 735
741 736 # check if we used an API key and it's a valid one
742 737 api_key = request.GET.get('api_key')
743 738 if api_key is not None:
744 739 # explicit controller is enabled or API is in our whitelist
745 740 if self.api_access or allowed_api_access(loc, api_key=api_key):
746 741 if api_key in user.api_keys:
747 742 log.info('user %s authenticated with API key ****%s @ %s'
748 743 % (user, api_key[-4:], loc))
749 744 return func(*fargs, **fkwargs)
750 745 else:
751 746 log.warning('API key ****%s is NOT valid' % api_key[-4:])
752 747 return redirect_to_login(_('Invalid API key'))
753 748 else:
754 749 # controller does not allow API access
755 750 log.warning('API access to %s is not allowed' % loc)
756 751 return abort(403)
757 752
758 753 # CSRF protection - POSTs with session auth must contain correct token
759 754 if request.POST and user.is_authenticated:
760 755 token = request.POST.get(secure_form.token_key)
761 756 if not token or token != secure_form.authentication_token():
762 757 log.error('CSRF check failed')
763 758 return abort(403)
764 759
765 760 # regular user authentication
766 761 if user.is_authenticated:
767 762 log.info('user %s authenticated with regular auth @ %s' % (user, loc))
768 763 return func(*fargs, **fkwargs)
769 764 else:
770 765 log.warning('user %s NOT authenticated with regular auth @ %s' % (user, loc))
771 766 return redirect_to_login()
772 767
773 768 class NotAnonymous(object):
774 769 """
775 770 Must be logged in to execute this function else
776 771 redirect to login page"""
777 772
778 773 def __call__(self, func):
779 774 return decorator(self.__wrapper, func)
780 775
781 776 def __wrapper(self, func, *fargs, **fkwargs):
782 777 cls = fargs[0]
783 778 self.user = cls.authuser
784 779
785 780 log.debug('Checking if user is not anonymous @%s' % cls)
786 781
787 782 anonymous = self.user.username == User.DEFAULT_USER
788 783
789 784 if anonymous:
790 785 return redirect_to_login(_('You need to be a registered user to '
791 786 'perform this action'))
792 787 else:
793 788 return func(*fargs, **fkwargs)
794 789
795 790
796 791 class PermsDecorator(object):
797 792 """Base class for controller decorators"""
798 793
799 794 def __init__(self, *required_perms):
800 795 self.required_perms = set(required_perms)
801 796 self.user_perms = None
802 797
803 798 def __call__(self, func):
804 799 return decorator(self.__wrapper, func)
805 800
806 801 def __wrapper(self, func, *fargs, **fkwargs):
807 802 cls = fargs[0]
808 803 self.user = cls.authuser
809 804 self.user_perms = self.user.permissions
810 805 log.debug('checking %s permissions %s for %s %s',
811 806 self.__class__.__name__, self.required_perms, cls, self.user)
812 807
813 808 if self.check_permissions():
814 809 log.debug('Permission granted for %s %s' % (cls, self.user))
815 810 return func(*fargs, **fkwargs)
816 811
817 812 else:
818 813 log.debug('Permission denied for %s %s' % (cls, self.user))
819 814 anonymous = self.user.username == User.DEFAULT_USER
820 815
821 816 if anonymous:
822 817 return redirect_to_login(_('You need to be signed in to view this page'))
823 818 else:
824 819 # redirect with forbidden ret code
825 820 return abort(403)
826 821
827 822 def check_permissions(self):
828 823 """Dummy function for overriding"""
829 824 raise Exception('You have to write this function in child class')
830 825
831 826
832 827 class HasPermissionAllDecorator(PermsDecorator):
833 828 """
834 829 Checks for access permission for all given predicates. All of them
835 830 have to be meet in order to fulfill the request
836 831 """
837 832
838 833 def check_permissions(self):
839 834 if self.required_perms.issubset(self.user_perms.get('global')):
840 835 return True
841 836 return False
842 837
843 838
844 839 class HasPermissionAnyDecorator(PermsDecorator):
845 840 """
846 841 Checks for access permission for any of given predicates. In order to
847 842 fulfill the request any of predicates must be meet
848 843 """
849 844
850 845 def check_permissions(self):
851 846 if self.required_perms.intersection(self.user_perms.get('global')):
852 847 return True
853 848 return False
854 849
855 850
856 851 class HasRepoPermissionAllDecorator(PermsDecorator):
857 852 """
858 853 Checks for access permission for all given predicates for specific
859 854 repository. All of them have to be meet in order to fulfill the request
860 855 """
861 856
862 857 def check_permissions(self):
863 858 repo_name = get_repo_slug(request)
864 859 try:
865 860 user_perms = set([self.user_perms['repositories'][repo_name]])
866 861 except KeyError:
867 862 return False
868 863 if self.required_perms.issubset(user_perms):
869 864 return True
870 865 return False
871 866
872 867
873 868 class HasRepoPermissionAnyDecorator(PermsDecorator):
874 869 """
875 870 Checks for access permission for any of given predicates for specific
876 871 repository. In order to fulfill the request any of predicates must be meet
877 872 """
878 873
879 874 def check_permissions(self):
880 875 repo_name = get_repo_slug(request)
881 876 try:
882 877 user_perms = set([self.user_perms['repositories'][repo_name]])
883 878 except KeyError:
884 879 return False
885 880
886 881 if self.required_perms.intersection(user_perms):
887 882 return True
888 883 return False
889 884
890 885
891 886 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
892 887 """
893 888 Checks for access permission for all given predicates for specific
894 889 repository group. All of them have to be meet in order to fulfill the request
895 890 """
896 891
897 892 def check_permissions(self):
898 893 group_name = get_repo_group_slug(request)
899 894 try:
900 895 user_perms = set([self.user_perms['repositories_groups'][group_name]])
901 896 except KeyError:
902 897 return False
903 898
904 899 if self.required_perms.issubset(user_perms):
905 900 return True
906 901 return False
907 902
908 903
909 904 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
910 905 """
911 906 Checks for access permission for any of given predicates for specific
912 907 repository group. In order to fulfill the request any of predicates must be meet
913 908 """
914 909
915 910 def check_permissions(self):
916 911 group_name = get_repo_group_slug(request)
917 912 try:
918 913 user_perms = set([self.user_perms['repositories_groups'][group_name]])
919 914 except KeyError:
920 915 return False
921 916
922 917 if self.required_perms.intersection(user_perms):
923 918 return True
924 919 return False
925 920
926 921
927 922 class HasUserGroupPermissionAllDecorator(PermsDecorator):
928 923 """
929 924 Checks for access permission for all given predicates for specific
930 925 user group. All of them have to be meet in order to fulfill the request
931 926 """
932 927
933 928 def check_permissions(self):
934 929 group_name = get_user_group_slug(request)
935 930 try:
936 931 user_perms = set([self.user_perms['user_groups'][group_name]])
937 932 except KeyError:
938 933 return False
939 934
940 935 if self.required_perms.issubset(user_perms):
941 936 return True
942 937 return False
943 938
944 939
945 940 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
946 941 """
947 942 Checks for access permission for any of given predicates for specific
948 943 user group. In order to fulfill the request any of predicates must be meet
949 944 """
950 945
951 946 def check_permissions(self):
952 947 group_name = get_user_group_slug(request)
953 948 try:
954 949 user_perms = set([self.user_perms['user_groups'][group_name]])
955 950 except KeyError:
956 951 return False
957 952
958 953 if self.required_perms.intersection(user_perms):
959 954 return True
960 955 return False
961 956
962 957
963 958 #==============================================================================
964 959 # CHECK FUNCTIONS
965 960 #==============================================================================
966 961 class PermsFunction(object):
967 962 """Base function for other check functions"""
968 963
969 964 def __init__(self, *perms):
970 965 self.required_perms = set(perms)
971 966 self.user_perms = None
972 967 self.repo_name = None
973 968 self.group_name = None
974 969
975 970 def __call__(self, check_location='', user=None):
976 971 if not user:
977 972 #TODO: remove this someday,put as user as attribute here
978 973 user = request.user
979 974
980 975 # init auth user if not already given
981 976 if not isinstance(user, AuthUser):
982 977 user = AuthUser(user.user_id)
983 978
984 979 cls_name = self.__class__.__name__
985 980 check_scope = {
986 981 'HasPermissionAll': '',
987 982 'HasPermissionAny': '',
988 983 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
989 984 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
990 985 'HasRepoGroupPermissionAll': 'group:%s' % self.group_name,
991 986 'HasRepoGroupPermissionAny': 'group:%s' % self.group_name,
992 987 }.get(cls_name, '?')
993 988 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
994 989 self.required_perms, user, check_scope,
995 990 check_location or 'unspecified location')
996 991 if not user:
997 992 log.debug('Empty request user')
998 993 return False
999 994 self.user_perms = user.permissions
1000 995 if self.check_permissions():
1001 996 log.debug('Permission to %s granted for user: %s @ %s'
1002 997 % (check_scope, user,
1003 998 check_location or 'unspecified location'))
1004 999 return True
1005 1000
1006 1001 else:
1007 1002 log.debug('Permission to %s denied for user: %s @ %s'
1008 1003 % (check_scope, user,
1009 1004 check_location or 'unspecified location'))
1010 1005 return False
1011 1006
1012 1007 def check_permissions(self):
1013 1008 """Dummy function for overriding"""
1014 1009 raise Exception('You have to write this function in child class')
1015 1010
1016 1011
1017 1012 class HasPermissionAll(PermsFunction):
1018 1013 def check_permissions(self):
1019 1014 if self.required_perms.issubset(self.user_perms.get('global')):
1020 1015 return True
1021 1016 return False
1022 1017
1023 1018
1024 1019 class HasPermissionAny(PermsFunction):
1025 1020 def check_permissions(self):
1026 1021 if self.required_perms.intersection(self.user_perms.get('global')):
1027 1022 return True
1028 1023 return False
1029 1024
1030 1025
1031 1026 class HasRepoPermissionAll(PermsFunction):
1032 1027 def __call__(self, repo_name=None, check_location='', user=None):
1033 1028 self.repo_name = repo_name
1034 1029 return super(HasRepoPermissionAll, self).__call__(check_location, user)
1035 1030
1036 1031 def check_permissions(self):
1037 1032 if not self.repo_name:
1038 1033 self.repo_name = get_repo_slug(request)
1039 1034
1040 1035 try:
1041 1036 self._user_perms = set(
1042 1037 [self.user_perms['repositories'][self.repo_name]]
1043 1038 )
1044 1039 except KeyError:
1045 1040 return False
1046 1041 if self.required_perms.issubset(self._user_perms):
1047 1042 return True
1048 1043 return False
1049 1044
1050 1045
1051 1046 class HasRepoPermissionAny(PermsFunction):
1052 1047 def __call__(self, repo_name=None, check_location='', user=None):
1053 1048 self.repo_name = repo_name
1054 1049 return super(HasRepoPermissionAny, self).__call__(check_location, user)
1055 1050
1056 1051 def check_permissions(self):
1057 1052 if not self.repo_name:
1058 1053 self.repo_name = get_repo_slug(request)
1059 1054
1060 1055 try:
1061 1056 self._user_perms = set(
1062 1057 [self.user_perms['repositories'][self.repo_name]]
1063 1058 )
1064 1059 except KeyError:
1065 1060 return False
1066 1061 if self.required_perms.intersection(self._user_perms):
1067 1062 return True
1068 1063 return False
1069 1064
1070 1065
1071 1066 class HasRepoGroupPermissionAny(PermsFunction):
1072 1067 def __call__(self, group_name=None, check_location='', user=None):
1073 1068 self.group_name = group_name
1074 1069 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
1075 1070
1076 1071 def check_permissions(self):
1077 1072 try:
1078 1073 self._user_perms = set(
1079 1074 [self.user_perms['repositories_groups'][self.group_name]]
1080 1075 )
1081 1076 except KeyError:
1082 1077 return False
1083 1078 if self.required_perms.intersection(self._user_perms):
1084 1079 return True
1085 1080 return False
1086 1081
1087 1082
1088 1083 class HasRepoGroupPermissionAll(PermsFunction):
1089 1084 def __call__(self, group_name=None, check_location='', user=None):
1090 1085 self.group_name = group_name
1091 1086 return super(HasRepoGroupPermissionAll, self).__call__(check_location, user)
1092 1087
1093 1088 def check_permissions(self):
1094 1089 try:
1095 1090 self._user_perms = set(
1096 1091 [self.user_perms['repositories_groups'][self.group_name]]
1097 1092 )
1098 1093 except KeyError:
1099 1094 return False
1100 1095 if self.required_perms.issubset(self._user_perms):
1101 1096 return True
1102 1097 return False
1103 1098
1104 1099
1105 1100 class HasUserGroupPermissionAny(PermsFunction):
1106 1101 def __call__(self, user_group_name=None, check_location='', user=None):
1107 1102 self.user_group_name = user_group_name
1108 1103 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
1109 1104
1110 1105 def check_permissions(self):
1111 1106 try:
1112 1107 self._user_perms = set(
1113 1108 [self.user_perms['user_groups'][self.user_group_name]]
1114 1109 )
1115 1110 except KeyError:
1116 1111 return False
1117 1112 if self.required_perms.intersection(self._user_perms):
1118 1113 return True
1119 1114 return False
1120 1115
1121 1116
1122 1117 class HasUserGroupPermissionAll(PermsFunction):
1123 1118 def __call__(self, user_group_name=None, check_location='', user=None):
1124 1119 self.user_group_name = user_group_name
1125 1120 return super(HasUserGroupPermissionAll, self).__call__(check_location, user)
1126 1121
1127 1122 def check_permissions(self):
1128 1123 try:
1129 1124 self._user_perms = set(
1130 1125 [self.user_perms['user_groups'][self.user_group_name]]
1131 1126 )
1132 1127 except KeyError:
1133 1128 return False
1134 1129 if self.required_perms.issubset(self._user_perms):
1135 1130 return True
1136 1131 return False
1137 1132
1138 1133
1139 1134 #==============================================================================
1140 1135 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
1141 1136 #==============================================================================
1142 1137 class HasPermissionAnyMiddleware(object):
1143 1138 def __init__(self, *perms):
1144 1139 self.required_perms = set(perms)
1145 1140
1146 1141 def __call__(self, user, repo_name):
1147 1142 # repo_name MUST be unicode, since we handle keys in permission
1148 1143 # dict by unicode
1149 1144 repo_name = safe_unicode(repo_name)
1150 1145 usr = AuthUser(user.user_id)
1151 1146 self.user_perms = set([usr.permissions['repositories'][repo_name]])
1152 1147 self.username = user.username
1153 1148 self.repo_name = repo_name
1154 1149 return self.check_permissions()
1155 1150
1156 1151 def check_permissions(self):
1157 1152 log.debug('checking VCS protocol '
1158 1153 'permissions %s for user:%s repository:%s', self.user_perms,
1159 1154 self.username, self.repo_name)
1160 1155 if self.required_perms.intersection(self.user_perms):
1161 1156 log.debug('Permission to repo: %s granted for user: %s @ %s'
1162 1157 % (self.repo_name, self.username, 'PermissionMiddleware'))
1163 1158 return True
1164 1159 log.debug('Permission to repo: %s denied for user: %s @ %s'
1165 1160 % (self.repo_name, self.username, 'PermissionMiddleware'))
1166 1161 return False
1167 1162
1168 1163
1169 1164 #==============================================================================
1170 1165 # SPECIAL VERSION TO HANDLE API AUTH
1171 1166 #==============================================================================
1172 1167 class _BaseApiPerm(object):
1173 1168 def __init__(self, *perms):
1174 1169 self.required_perms = set(perms)
1175 1170
1176 1171 def __call__(self, check_location=None, user=None, repo_name=None,
1177 1172 group_name=None):
1178 1173 cls_name = self.__class__.__name__
1179 1174 check_scope = 'user:%s' % (user)
1180 1175 if repo_name:
1181 1176 check_scope += ', repo:%s' % (repo_name)
1182 1177
1183 1178 if group_name:
1184 1179 check_scope += ', repo group:%s' % (group_name)
1185 1180
1186 1181 log.debug('checking cls:%s %s %s @ %s'
1187 1182 % (cls_name, self.required_perms, check_scope, check_location))
1188 1183 if not user:
1189 1184 log.debug('Empty User passed into arguments')
1190 1185 return False
1191 1186
1192 1187 ## process user
1193 1188 if not isinstance(user, AuthUser):
1194 1189 user = AuthUser(user.user_id)
1195 1190 if not check_location:
1196 1191 check_location = 'unspecified'
1197 1192 if self.check_permissions(user.permissions, repo_name, group_name):
1198 1193 log.debug('Permission to %s granted for user: %s @ %s'
1199 1194 % (check_scope, user, check_location))
1200 1195 return True
1201 1196
1202 1197 else:
1203 1198 log.debug('Permission to %s denied for user: %s @ %s'
1204 1199 % (check_scope, user, check_location))
1205 1200 return False
1206 1201
1207 1202 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1208 1203 """
1209 1204 implement in child class should return True if permissions are ok,
1210 1205 False otherwise
1211 1206
1212 1207 :param perm_defs: dict with permission definitions
1213 1208 :param repo_name: repo name
1214 1209 """
1215 1210 raise NotImplementedError()
1216 1211
1217 1212
1218 1213 class HasPermissionAllApi(_BaseApiPerm):
1219 1214 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1220 1215 if self.required_perms.issubset(perm_defs.get('global')):
1221 1216 return True
1222 1217 return False
1223 1218
1224 1219
1225 1220 class HasPermissionAnyApi(_BaseApiPerm):
1226 1221 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1227 1222 if self.required_perms.intersection(perm_defs.get('global')):
1228 1223 return True
1229 1224 return False
1230 1225
1231 1226
1232 1227 class HasRepoPermissionAllApi(_BaseApiPerm):
1233 1228 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1234 1229 try:
1235 1230 _user_perms = set([perm_defs['repositories'][repo_name]])
1236 1231 except KeyError:
1237 1232 log.warning(traceback.format_exc())
1238 1233 return False
1239 1234 if self.required_perms.issubset(_user_perms):
1240 1235 return True
1241 1236 return False
1242 1237
1243 1238
1244 1239 class HasRepoPermissionAnyApi(_BaseApiPerm):
1245 1240 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1246 1241 try:
1247 1242 _user_perms = set([perm_defs['repositories'][repo_name]])
1248 1243 except KeyError:
1249 1244 log.warning(traceback.format_exc())
1250 1245 return False
1251 1246 if self.required_perms.intersection(_user_perms):
1252 1247 return True
1253 1248 return False
1254 1249
1255 1250
1256 1251 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
1257 1252 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1258 1253 try:
1259 1254 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1260 1255 except KeyError:
1261 1256 log.warning(traceback.format_exc())
1262 1257 return False
1263 1258 if self.required_perms.intersection(_user_perms):
1264 1259 return True
1265 1260 return False
1266 1261
1267 1262 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
1268 1263 def check_permissions(self, perm_defs, repo_name=None, group_name=None):
1269 1264 try:
1270 1265 _user_perms = set([perm_defs['repositories_groups'][group_name]])
1271 1266 except KeyError:
1272 1267 log.warning(traceback.format_exc())
1273 1268 return False
1274 1269 if self.required_perms.issubset(_user_perms):
1275 1270 return True
1276 1271 return False
1277 1272
1278 1273 def check_ip_access(source_ip, allowed_ips=None):
1279 1274 """
1280 1275 Checks if source_ip is a subnet of any of allowed_ips.
1281 1276
1282 1277 :param source_ip:
1283 1278 :param allowed_ips: list of allowed ips together with mask
1284 1279 """
1285 1280 from kallithea.lib import ipaddr
1286 1281 log.debug('checking if ip:%s is subnet of %s' % (source_ip, allowed_ips))
1287 1282 if isinstance(allowed_ips, (tuple, list, set)):
1288 1283 for ip in allowed_ips:
1289 1284 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
1290 1285 log.debug('IP %s is network %s' %
1291 1286 (ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip)))
1292 1287 return True
1293 1288 return False
@@ -1,474 +1,473 b''
1 1 # -*- coding: utf-8 -*-
2 2 # This program is free software: you can redistribute it and/or modify
3 3 # it under the terms of the GNU General Public License as published by
4 4 # the Free Software Foundation, either version 3 of the License, or
5 5 # (at your option) any later version.
6 6 #
7 7 # This program is distributed in the hope that it will be useful,
8 8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 # GNU General Public License for more details.
11 11 #
12 12 # You should have received a copy of the GNU General Public License
13 13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14
15 15 """
16 16 kallithea.lib.base
17 17 ~~~~~~~~~~~~~~~~~~
18 18
19 19 The base Controller API
20 20 Provides the BaseController class for subclassing. And usage in different
21 21 controllers
22 22
23 23 This file was forked by the Kallithea project in July 2014.
24 24 Original author and date, and relevant copyright and licensing information is below:
25 25 :created_on: Oct 06, 2010
26 26 :author: marcink
27 27 :copyright: (c) 2013 RhodeCode GmbH, and others.
28 28 :license: GPLv3, see LICENSE.md for more details.
29 29 """
30 30
31 31 import logging
32 32 import time
33 33 import traceback
34 34
35 35 import webob.exc
36 36 import paste.httpexceptions
37 37 import paste.auth.basic
38 38 import paste.httpheaders
39 39
40 40 from pylons import config, tmpl_context as c, request, session, url
41 41 from pylons.controllers import WSGIController
42 42 from pylons.controllers.util import redirect
43 43 from pylons.templating import render_mako as render # don't remove this import
44 44 from pylons.i18n.translation import _
45 45
46 46 from kallithea import __version__, BACKENDS
47 47
48 48 from kallithea.lib.utils2 import str2bool, safe_unicode, AttributeDict,\
49 49 safe_str, safe_int
50 50 from kallithea.lib import auth_modules
51 51 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware, CookieStoreWrapper
52 52 from kallithea.lib.utils import get_repo_slug
53 53 from kallithea.lib.exceptions import UserCreationError
54 54 from kallithea.lib.vcs.exceptions import RepositoryError, EmptyRepositoryError, ChangesetDoesNotExistError
55 55 from kallithea.model import meta
56 56
57 57 from kallithea.model.db import Repository, Ui, User, Setting
58 58 from kallithea.model.notification import NotificationModel
59 59 from kallithea.model.scm import ScmModel
60 60 from kallithea.model.pull_request import PullRequestModel
61 61
62 62 log = logging.getLogger(__name__)
63 63
64 64
65 65 def _filter_proxy(ip):
66 66 """
67 67 HEADERS can have multiple ips inside the left-most being the original
68 68 client, and each successive proxy that passed the request adding the IP
69 69 address where it received the request from.
70 70
71 71 :param ip:
72 72 """
73 73 if ',' in ip:
74 74 _ips = ip.split(',')
75 75 _first_ip = _ips[0].strip()
76 76 log.debug('Got multiple IPs %s, using %s' % (','.join(_ips), _first_ip))
77 77 return _first_ip
78 78 return ip
79 79
80 80
81 81 def _get_ip_addr(environ):
82 82 proxy_key = 'HTTP_X_REAL_IP'
83 83 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
84 84 def_key = 'REMOTE_ADDR'
85 85
86 86 ip = environ.get(proxy_key)
87 87 if ip:
88 88 return _filter_proxy(ip)
89 89
90 90 ip = environ.get(proxy_key2)
91 91 if ip:
92 92 return _filter_proxy(ip)
93 93
94 94 ip = environ.get(def_key, '0.0.0.0')
95 95 return _filter_proxy(ip)
96 96
97 97
98 98 def _get_access_path(environ):
99 99 path = environ.get('PATH_INFO')
100 100 org_req = environ.get('pylons.original_request')
101 101 if org_req:
102 102 path = org_req.environ.get('PATH_INFO')
103 103 return path
104 104
105 105
106 106 class BasicAuth(paste.auth.basic.AuthBasicAuthenticator):
107 107
108 108 def __init__(self, realm, authfunc, auth_http_code=None):
109 109 self.realm = realm
110 110 self.authfunc = authfunc
111 111 self._rc_auth_http_code = auth_http_code
112 112
113 113 def build_authentication(self):
114 114 head = paste.httpheaders.WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
115 115 if self._rc_auth_http_code and self._rc_auth_http_code == '403':
116 116 # return 403 if alternative http return code is specified in
117 117 # Kallithea config
118 118 return paste.httpexceptions.HTTPForbidden(headers=head)
119 119 return paste.httpexceptions.HTTPUnauthorized(headers=head)
120 120
121 121 def authenticate(self, environ):
122 122 authorization = paste.httpheaders.AUTHORIZATION(environ)
123 123 if not authorization:
124 124 return self.build_authentication()
125 125 (authmeth, auth) = authorization.split(' ', 1)
126 126 if 'basic' != authmeth.lower():
127 127 return self.build_authentication()
128 128 auth = auth.strip().decode('base64')
129 129 _parts = auth.split(':', 1)
130 130 if len(_parts) == 2:
131 131 username, password = _parts
132 132 if self.authfunc(username, password, environ):
133 133 return username
134 134 return self.build_authentication()
135 135
136 136 __call__ = authenticate
137 137
138 138
139 139 class BaseVCSController(object):
140 140
141 141 def __init__(self, application, config):
142 142 self.application = application
143 143 self.config = config
144 144 # base path of repo locations
145 145 self.basepath = self.config['base_path']
146 146 #authenticate this VCS request using authfunc
147 147 self.authenticate = BasicAuth('', auth_modules.authenticate,
148 148 config.get('auth_ret_code'))
149 149 self.ip_addr = '0.0.0.0'
150 150
151 151 def _handle_request(self, environ, start_response):
152 152 raise NotImplementedError()
153 153
154 154 def _get_by_id(self, repo_name):
155 155 """
156 156 Gets a special pattern _<ID> from clone url and tries to replace it
157 157 with a repository_name for support of _<ID> permanent URLs
158 158
159 159 :param repo_name:
160 160 """
161 161
162 162 data = repo_name.split('/')
163 163 if len(data) >= 2:
164 164 from kallithea.lib.utils import get_repo_by_id
165 165 by_id_match = get_repo_by_id(repo_name)
166 166 if by_id_match:
167 167 data[1] = by_id_match
168 168
169 169 return '/'.join(data)
170 170
171 171 def _invalidate_cache(self, repo_name):
172 172 """
173 173 Sets cache for this repository for invalidation on next access
174 174
175 175 :param repo_name: full repo name, also a cache key
176 176 """
177 177 ScmModel().mark_for_invalidation(repo_name)
178 178
179 179 def _check_permission(self, action, user, repo_name, ip_addr=None):
180 180 """
181 181 Checks permissions using action (push/pull) user and repository
182 182 name
183 183
184 184 :param action: push or pull action
185 185 :param user: user instance
186 186 :param repo_name: repository name
187 187 """
188 188 # check IP
189 189 inherit = user.inherit_default_permissions
190 190 ip_allowed = AuthUser.check_ip_allowed(user.user_id, ip_addr,
191 191 inherit_from_default=inherit)
192 192 if ip_allowed:
193 193 log.info('Access for IP:%s allowed' % (ip_addr,))
194 194 else:
195 195 return False
196 196
197 197 if action == 'push':
198 198 if not HasPermissionAnyMiddleware('repository.write',
199 199 'repository.admin')(user,
200 200 repo_name):
201 201 return False
202 202
203 203 else:
204 204 #any other action need at least read permission
205 205 if not HasPermissionAnyMiddleware('repository.read',
206 206 'repository.write',
207 207 'repository.admin')(user,
208 208 repo_name):
209 209 return False
210 210
211 211 return True
212 212
213 213 def _get_ip_addr(self, environ):
214 214 return _get_ip_addr(environ)
215 215
216 216 def _check_ssl(self, environ):
217 217 """
218 218 Checks the SSL check flag and returns False if SSL is not present
219 219 and required True otherwise
220 220 """
221 221 #check if we have SSL required ! if not it's a bad request !
222 222 if str2bool(Ui.get_by_key('push_ssl').ui_value):
223 223 org_proto = environ.get('wsgi._org_proto', environ['wsgi.url_scheme'])
224 224 if org_proto != 'https':
225 225 log.debug('proto is %s and SSL is required BAD REQUEST !'
226 226 % org_proto)
227 227 return False
228 228 return True
229 229
230 230 def _check_locking_state(self, environ, action, repo, user_id):
231 231 """
232 232 Checks locking on this repository, if locking is enabled and lock is
233 233 present returns a tuple of make_lock, locked, locked_by.
234 234 make_lock can have 3 states None (do nothing) True, make lock
235 235 False release lock, This value is later propagated to hooks, which
236 236 do the locking. Think about this as signals passed to hooks what to do.
237 237
238 238 """
239 239 locked = False # defines that locked error should be thrown to user
240 240 make_lock = None
241 241 repo = Repository.get_by_repo_name(repo)
242 242 user = User.get(user_id)
243 243
244 244 # this is kind of hacky, but due to how mercurial handles client-server
245 245 # server see all operation on changeset; bookmarks, phases and
246 246 # obsolescence marker in different transaction, we don't want to check
247 247 # locking on those
248 248 obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',]
249 249 locked_by = repo.locked
250 250 if repo and repo.enable_locking and not obsolete_call:
251 251 if action == 'push':
252 252 #check if it's already locked !, if it is compare users
253 253 user_id, _date = repo.locked
254 254 if user.user_id == user_id:
255 255 log.debug('Got push from user %s, now unlocking' % (user))
256 256 # unlock if we have push from user who locked
257 257 make_lock = False
258 258 else:
259 259 # we're not the same user who locked, ban with 423 !
260 260 locked = True
261 261 if action == 'pull':
262 262 if repo.locked[0] and repo.locked[1]:
263 263 locked = True
264 264 else:
265 265 log.debug('Setting lock on repo %s by %s' % (repo, user))
266 266 make_lock = True
267 267
268 268 else:
269 269 log.debug('Repository %s do not have locking enabled' % (repo))
270 270 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s'
271 271 % (make_lock, locked, locked_by))
272 272 return make_lock, locked, locked_by
273 273
274 274 def __call__(self, environ, start_response):
275 275 start = time.time()
276 276 try:
277 277 return self._handle_request(environ, start_response)
278 278 finally:
279 279 log = logging.getLogger('kallithea.' + self.__class__.__name__)
280 280 log.debug('Request time: %.3fs' % (time.time() - start))
281 281 meta.Session.remove()
282 282
283 283
284 284 class BaseController(WSGIController):
285 285
286 286 def __before__(self):
287 287 """
288 288 __before__ is called before controller methods and after __call__
289 289 """
290 290 c.kallithea_version = __version__
291 291 rc_config = Setting.get_app_settings()
292 292
293 293 # Visual options
294 294 c.visual = AttributeDict({})
295 295
296 296 ## DB stored
297 297 c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
298 298 c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
299 299 c.visual.stylify_metatags = str2bool(rc_config.get('stylify_metatags'))
300 300 c.visual.dashboard_items = safe_int(rc_config.get('dashboard_items', 100))
301 301 c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
302 302 c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
303 303 c.visual.show_version = str2bool(rc_config.get('show_version'))
304 304 c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
305 305 c.visual.gravatar_url = rc_config.get('gravatar_url')
306 306
307 307 c.ga_code = rc_config.get('ga_code')
308 308 # TODO: replace undocumented backwards compatibility hack with db upgrade and rename ga_code
309 309 if c.ga_code and '<' not in c.ga_code:
310 310 c.ga_code = '''<script type="text/javascript">
311 311 var _gaq = _gaq || [];
312 312 _gaq.push(['_setAccount', '%s']);
313 313 _gaq.push(['_trackPageview']);
314 314
315 315 (function() {
316 316 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
317 317 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
318 318 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
319 319 })();
320 320 </script>''' % c.ga_code
321 321 c.site_name = rc_config.get('title')
322 322 c.clone_uri_tmpl = rc_config.get('clone_uri_tmpl')
323 323
324 324 ## INI stored
325 325 c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
326 326 c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
327 327
328 328 c.instance_id = config.get('instance_id')
329 329 c.issues_url = config.get('bugtracker', url('issues_url'))
330 330 # END CONFIG VARS
331 331
332 332 c.repo_name = get_repo_slug(request) # can be empty
333 333 c.backends = BACKENDS.keys()
334 334 c.unread_notifications = NotificationModel()\
335 335 .get_unread_cnt_for_user(c.authuser.user_id)
336 336
337 337 self.cut_off_limit = safe_int(config.get('cut_off_limit'))
338 338
339 339 c.my_pr_count = PullRequestModel().get_pullrequest_cnt_for_user(c.authuser.user_id)
340 340
341 341 self.sa = meta.Session
342 342 self.scm_model = ScmModel(self.sa)
343 343
344 344 @staticmethod
345 def _determine_auth_user(ip_addr, api_key, session_authuser):
345 def _determine_auth_user(api_key, session_authuser):
346 346 """
347 347 Create an `AuthUser` object given the IP address of the request, the
348 348 API key (if any), and the authuser from the session.
349 349 """
350 350
351 351 if api_key:
352 352 # when using API_KEY we are sure user exists.
353 auth_user = AuthUser(api_key=api_key, ip_addr=ip_addr)
353 auth_user = AuthUser(api_key=api_key)
354 354 authenticated = False
355 355 else:
356 356 cookie_store = CookieStoreWrapper(session_authuser)
357 357 user_id = cookie_store.get('user_id')
358 358 try:
359 auth_user = AuthUser(user_id=user_id, ip_addr=ip_addr)
359 auth_user = AuthUser(user_id=user_id)
360 360 except UserCreationError as e:
361 361 # container auth or other auth functions that create users on
362 362 # the fly can throw UserCreationError to signal issues with
363 363 # user creation. Explanation should be provided in the
364 364 # exception object.
365 365 from kallithea.lib import helpers as h
366 366 h.flash(e, 'error')
367 auth_user = AuthUser(ip_addr=ip_addr)
367 auth_user = AuthUser()
368 368
369 369 authenticated = cookie_store.get('is_authenticated')
370 370
371 371 if not auth_user.is_authenticated and auth_user.user_id is not None:
372 372 # user is not authenticated and not empty
373 373 auth_user.set_authenticated(authenticated)
374 374
375 375 return auth_user
376 376
377 377 def __call__(self, environ, start_response):
378 378 """Invoke the Controller"""
379 379
380 380 # WSGIController.__call__ dispatches to the Controller method
381 381 # the request is routed to. This routing information is
382 382 # available in environ['pylons.routes_dict']
383 383 try:
384 384 self.ip_addr = _get_ip_addr(environ)
385 385 # make sure that we update permissions each time we call controller
386 386
387 387 #set globals for auth user
388 388 self.authuser = c.authuser = request.user = self._determine_auth_user(
389 self.ip_addr,
390 389 request.GET.get('api_key'),
391 390 session.get('authuser'),
392 391 )
393 392
394 393 log.info('IP: %s User: %s accessed %s',
395 394 self.ip_addr, self.authuser,
396 395 safe_unicode(_get_access_path(environ)),
397 396 )
398 397 return WSGIController.__call__(self, environ, start_response)
399 398 finally:
400 399 meta.Session.remove()
401 400
402 401
403 402 class BaseRepoController(BaseController):
404 403 """
405 404 Base class for controllers responsible for loading all needed data for
406 405 repository loaded items are
407 406
408 407 c.db_repo_scm_instance: instance of scm repository
409 408 c.db_repo: instance of db
410 409 c.repository_followers: number of followers
411 410 c.repository_forks: number of forks
412 411 c.repository_following: weather the current user is following the current repo
413 412 """
414 413
415 414 def __before__(self):
416 415 super(BaseRepoController, self).__before__()
417 416 if c.repo_name: # extracted from routes
418 417 _dbr = Repository.get_by_repo_name(c.repo_name)
419 418 if not _dbr:
420 419 return
421 420
422 421 log.debug('Found repository in database %s with state `%s`'
423 422 % (safe_unicode(_dbr), safe_unicode(_dbr.repo_state)))
424 423 route = getattr(request.environ.get('routes.route'), 'name', '')
425 424
426 425 # allow to delete repos that are somehow damages in filesystem
427 426 if route in ['delete_repo']:
428 427 return
429 428
430 429 if _dbr.repo_state in [Repository.STATE_PENDING]:
431 430 if route in ['repo_creating_home']:
432 431 return
433 432 check_url = url('repo_creating_home', repo_name=c.repo_name)
434 433 return redirect(check_url)
435 434
436 435 dbr = c.db_repo = _dbr
437 436 c.db_repo_scm_instance = c.db_repo.scm_instance
438 437 if c.db_repo_scm_instance is None:
439 438 log.error('%s this repository is present in database but it '
440 439 'cannot be created as an scm instance', c.repo_name)
441 440 from kallithea.lib import helpers as h
442 441 h.flash(h.literal(_('Repository not found in the filesystem')),
443 442 category='error')
444 443 raise paste.httpexceptions.HTTPNotFound()
445 444
446 445 # some globals counter for menu
447 446 c.repository_followers = self.scm_model.get_followers(dbr)
448 447 c.repository_forks = self.scm_model.get_forks(dbr)
449 448 c.repository_pull_requests = self.scm_model.get_pull_requests(dbr)
450 449 c.repository_following = self.scm_model.is_following_repo(
451 450 c.repo_name, self.authuser.user_id)
452 451
453 452 @staticmethod
454 453 def _get_ref_rev(repo, ref_type, ref_name, returnempty=False):
455 454 """
456 455 Safe way to get changeset. If error occurs show error.
457 456 """
458 457 from kallithea.lib import helpers as h
459 458 try:
460 459 return repo.scm_instance.get_ref_revision(ref_type, ref_name)
461 460 except EmptyRepositoryError as e:
462 461 if returnempty:
463 462 return repo.scm_instance.EMPTY_CHANGESET
464 463 h.flash(h.literal(_('There are no changesets yet')),
465 464 category='error')
466 465 raise webob.exc.HTTPNotFound()
467 466 except ChangesetDoesNotExistError as e:
468 467 h.flash(h.literal(_('Changeset not found')),
469 468 category='error')
470 469 raise webob.exc.HTTPNotFound()
471 470 except RepositoryError as e:
472 471 log.error(traceback.format_exc())
473 472 h.flash(safe_str(e), category='error')
474 473 raise webob.exc.HTTPBadRequest()
@@ -1,75 +1,75 b''
1 1 ${h.form(url('my_account'), method='post')}
2 2 <div class="form">
3 3
4 4 <div class="field">
5 5 <div class="gravatar_box">
6 6 <div class="gravatar">
7 7 ${h.gravatar(c.user.email)}
8 8 </div>
9 9 <p>
10 10 %if c.visual.use_gravatar:
11 11 <strong>${_('Change your avatar at')} <a href="http://gravatar.com">gravatar.com</a></strong>
12 12 <br/>${_('Using')} ${c.user.email}
13 13 %else:
14 14 <strong>${_('Avatars are disabled')}</strong>
15 15 <br/>${c.user.email or _('Missing email, please update your user email address.')}
16 [${_('Current IP')}: ${c.perm_user.ip_addr or "?"}]
16 [${_('Current IP')}: ${c.ip_addr}]
17 17 %endif
18 18 </p>
19 19 </div>
20 20 </div>
21 21
22 22 <% readonly = None %>
23 23 <% disabled = "" %>
24 24 <div class="fields">
25 25 %if c.extern_type != c.EXTERN_TYPE_INTERNAL:
26 26 <% readonly = "readonly" %>
27 27 <% disabled = " disabled" %>
28 28 <strong>${_('Your user is in an external Source of Record; some details cannot be managed here')}.</strong>
29 29 %endif
30 30 <div class="field">
31 31 <div class="label">
32 32 <label for="username">${_('Username')}:</label>
33 33 </div>
34 34 <div class="input">
35 35 ${h.text('username',class_='medium%s' % disabled, readonly=readonly)}
36 36 ${h.hidden('extern_name', c.extern_name)}
37 37 ${h.hidden('extern_type', c.extern_type)}
38 38 </div>
39 39 </div>
40 40
41 41 <div class="field">
42 42 <div class="label">
43 43 <label for="name">${_('First Name')}:</label>
44 44 </div>
45 45 <div class="input">
46 46 ${h.text('firstname',class_="medium")}
47 47 </div>
48 48 </div>
49 49
50 50 <div class="field">
51 51 <div class="label">
52 52 <label for="lastname">${_('Last Name')}:</label>
53 53 </div>
54 54 <div class="input">
55 55 ${h.text('lastname',class_="medium")}
56 56 </div>
57 57 </div>
58 58
59 59 <div class="field">
60 60 <div class="label">
61 61 <label for="email">${_('Email')}:</label>
62 62 </div>
63 63 <div class="input">
64 64 ## we should be able to edit email !
65 65 ${h.text('email',class_="medium")}
66 66 </div>
67 67 </div>
68 68
69 69 <div class="buttons">
70 70 ${h.submit('save',_('Save'),class_="btn")}
71 71 ${h.reset('reset',_('Reset'),class_="btn")}
72 72 </div>
73 73 </div>
74 74 </div>
75 75 ${h.end_form()}
@@ -1,127 +1,127 b''
1 1 ${h.form(url('update_user', id=c.user.user_id),method='put')}
2 2 <div class="form">
3 3 <div class="field">
4 4 <div class="gravatar_box">
5 5 <div class="gravatar">${h.gravatar(c.user.email)}</div>
6 6 <p>
7 7 %if c.visual.use_gravatar:
8 8 <strong>${_('Change avatar at')} <a href="http://gravatar.com">gravatar.com</a></strong>
9 9 <br/>${_('Using')} ${c.user.email}
10 10 %else:
11 11 <strong>${_('Avatars are disabled')}</strong>
12 12 <br/>${c.user.email or _('Missing email, please update this user email address.')}
13 13 ##show current ip just if we show ourself
14 14 %if c.authuser.username == c.user.username:
15 [${_('Current IP')}: ${c.perm_user.ip_addr or "?"}]
15 [${_('Current IP')}: ${c.ip_addr}]
16 16 %endif
17 17 %endif
18 18 </div>
19 19 </div>
20 20 <% readonly = None %>
21 21 <% disabled = "" %>
22 22 <div class="fields">
23 23 %if c.extern_type != c.EXTERN_TYPE_INTERNAL:
24 24 <div class="field">
25 25 <% readonly = "readonly" %>
26 26 <% disabled = " disabled" %>
27 27 <strong>${_('This user is in an external Source of Record (%s); some details cannot be managed here.' % c.extern_type)}.</strong>
28 28 </div>
29 29 %endif
30 30
31 31 <div class="field">
32 32 <div class="label">
33 33 <label for="username">${_('Username')}:</label>
34 34 </div>
35 35 <div class="input">
36 36 ${h.text('username',class_='medium%s' % disabled, readonly=readonly)}
37 37 </div>
38 38 </div>
39 39
40 40 <div class="field">
41 41 <div class="label">
42 42 <label for="email">${_('Email')}:</label>
43 43 </div>
44 44 <div class="input">
45 45 ${h.text('email',class_='medium')}
46 46 </div>
47 47 </div>
48 48
49 49 <div class="field">
50 50 <div class="label">
51 51 <label for="extern_type">${_('Source of Record')}:</label>
52 52 </div>
53 53 <div class="input">
54 54 ${h.text('extern_type',class_='medium disabled',readonly="readonly")}
55 55 </div>
56 56 </div>
57 57
58 58 <div class="field">
59 59 <div class="label">
60 60 <label for="extern_name">${_('Name in Source of Record')}:</label>
61 61 </div>
62 62 <div class="input">
63 63 ${h.text('extern_name',class_='medium disabled',readonly="readonly")}
64 64 </div>
65 65 </div>
66 66
67 67 <div class="field">
68 68 <div class="label">
69 69 <label for="new_password">${_('New password')}:</label>
70 70 </div>
71 71 <div class="input">
72 72 ${h.password('new_password',class_='medium%s' % disabled,readonly=readonly)}
73 73 </div>
74 74 </div>
75 75
76 76 <div class="field">
77 77 <div class="label">
78 78 <label for="password_confirmation">${_('New password confirmation')}:</label>
79 79 </div>
80 80 <div class="input">
81 81 ${h.password('password_confirmation',class_="medium%s" % disabled,readonly=readonly)}
82 82 </div>
83 83 </div>
84 84
85 85 <div class="field">
86 86 <div class="label">
87 87 <label for="firstname">${_('First Name')}:</label>
88 88 </div>
89 89 <div class="input">
90 90 ${h.text('firstname',class_='medium')}
91 91 </div>
92 92 </div>
93 93
94 94 <div class="field">
95 95 <div class="label">
96 96 <label for="lastname">${_('Last Name')}:</label>
97 97 </div>
98 98 <div class="input">
99 99 ${h.text('lastname',class_='medium')}
100 100 </div>
101 101 </div>
102 102
103 103 <div class="field">
104 104 <div class="label label-checkbox">
105 105 <label for="active">${_('Active')}:</label>
106 106 </div>
107 107 <div class="checkboxes">
108 108 ${h.checkbox('active',value=True)}
109 109 </div>
110 110 </div>
111 111
112 112 <div class="field">
113 113 <div class="label label-checkbox">
114 114 <label for="admin">${_('Admin')}:</label>
115 115 </div>
116 116 <div class="checkboxes">
117 117 ${h.checkbox('admin',value=True)}
118 118 </div>
119 119 </div>
120 120
121 121 <div class="buttons">
122 122 ${h.submit('save',_('Save'),class_="btn")}
123 123 ${h.reset('reset',_('Reset'),class_="btn")}
124 124 </div>
125 125 </div>
126 126 </div>
127 127 ${h.end_form()}
General Comments 0
You need to be logged in to leave comments. Login now