##// END OF EJS Templates
repositories: allow updating repository settings for users without store-in-root permissions...
repositories: allow updating repository settings for users without store-in-root permissions in case repository name didn't change. - when an user owns repository in root location, and isn't allow to create repositories in root before we failed to allow this user to update such repository settings due to this validation. We'll now check if name didn't change and in this case allow to update since this doesn't store any new data in root location.

File last commit:

r4306:09801de9 default
r4415:fc1f6c1b default
Show More
views.py
489 lines | 19.3 KiB | text/x-python | PythonLexer
login: moved to apps module
r1501 # -*- coding: utf-8 -*-
code: update copyrights to 2020
r4306 # Copyright (C) 2016-2020 RhodeCode GmbH
login: moved to apps module
r1501 #
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License, version 3
# (only), as published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import time
import collections
import datetime
import formencode
apps: cleanup imports
r2080 import formencode.htmlfill
login: moved to apps module
r1501 import logging
import urlparse
re-captcha: adjust for v2 that is the only left one supported since 1st of May.
r2731 import requests
login: moved to apps module
r1501
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
login: switch to re-use the baseApp pyramid view.
r1536 from rhodecode.apps._base import BaseAppView
login: moved to apps module
r1501 from rhodecode.authentication.base import authenticate, HTTP_TYPE
auth/security: enforce that external users cannot reset their password.
r3258 from rhodecode.authentication.plugins import auth_rhodecode
pylons: fixed code and test suite after removal of pylons.
r2358 from rhodecode.events import UserRegistered, trigger
login: moved to apps module
r1501 from rhodecode.lib import helpers as h
audit-logs: use new v2 api on login/logout/password reset views.
r1697 from rhodecode.lib import audit_logger
login: moved to apps module
r1501 from rhodecode.lib.auth import (
AuthUser, HasPermissionAnyDecorator, CSRFRequired)
from rhodecode.lib.base import get_ip_addr
from rhodecode.lib.exceptions import UserCreationError
from rhodecode.lib.utils2 import safe_str
from rhodecode.model.db import User, UserApiKeys
from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
from rhodecode.model.meta import Session
from rhodecode.model.auth_token import AuthTokenModel
from rhodecode.model.settings import SettingsModel
from rhodecode.model.user import UserModel
from rhodecode.translation import _
log = logging.getLogger(__name__)
CaptchaData = collections.namedtuple(
'CaptchaData', 'active, private_key, public_key')
auth: make the store_user_in_session public, as it's used in external auth plugins.
r3250 def store_user_in_session(session, username, remember=False):
login: moved to apps module
r1501 user = User.get_by_username(username, case_insensitive=True)
auth_user = AuthUser(user.user_id)
auth_user.set_authenticated()
cs = auth_user.get_cookie_store()
session['rhodecode_user'] = cs
user.update_lastlogin()
Session().commit()
# If they want to be remembered, update the cookie
if remember:
_year = (datetime.datetime.now() +
datetime.timedelta(seconds=60 * 60 * 24 * 365))
session._set_cookie_expires(_year)
session.save()
safe_cs = cs.copy()
safe_cs['password'] = '****'
log.info('user %s is now authenticated and stored in '
'session, session attrs %s', username, safe_cs)
# dumps session attrs back to cookie
session._update_cookie_out()
# we set new cookie
headers = None
if session.request['set_cookie']:
# send set-cookie headers back to response to update cookie
headers = [('Set-Cookie', session.request['cookie_out'])]
return headers
def get_came_from(request):
came_from = safe_str(request.GET.get('came_from', ''))
parsed = urlparse.urlparse(came_from)
allowed_schemes = ['http', 'https']
home: moved home and repo group views into pyramid....
r1774 default_came_from = h.route_path('home')
login: moved to apps module
r1501 if parsed.scheme and parsed.scheme not in allowed_schemes:
logging: use lazy parameter evaluation in log calls.
r3061 log.error('Suspicious URL scheme detected %s for url %s',
parsed.scheme, parsed)
home: moved home and repo group views into pyramid....
r1774 came_from = default_came_from
login: moved to apps module
r1501 elif parsed.netloc and request.host != parsed.netloc:
log.error('Suspicious NETLOC detected %s for url %s server url '
logging: use lazy parameter evaluation in log calls.
r3061 'is: %s', parsed.netloc, parsed, request.host)
home: moved home and repo group views into pyramid....
r1774 came_from = default_came_from
login: moved to apps module
r1501 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
logging: use lazy parameter evaluation in log calls.
r3061 log.error('Header injection detected `%s` for url %s server url ',
parsed.path, parsed)
home: moved home and repo group views into pyramid....
r1774 came_from = default_came_from
login: moved to apps module
r1501
home: moved home and repo group views into pyramid....
r1774 return came_from or default_came_from
login: moved to apps module
r1501
login: switch to re-use the baseApp pyramid view.
r1536 class LoginView(BaseAppView):
login: moved to apps module
r1501
login: switch to re-use the baseApp pyramid view.
r1536 def load_default_context(self):
c = self._get_local_tmpl_context()
c.came_from = get_came_from(self.request)
pylons: remove pylons as dependency...
r2351
login: switch to re-use the baseApp pyramid view.
r1536 return c
login: moved to apps module
r1501
def _get_captcha_data(self):
settings = SettingsModel().get_all_settings()
private_key = settings.get('rhodecode_captcha_private_key')
public_key = settings.get('rhodecode_captcha_public_key')
active = bool(private_key)
return CaptchaData(
active=active, private_key=private_key, public_key=public_key)
re-captcha: adjust for v2 that is the only left one supported since 1st of May.
r2731 def validate_captcha(self, private_key):
captcha_rs = self.request.POST.get('g-recaptcha-response')
url = "https://www.google.com/recaptcha/api/siteverify"
params = {
'secret': private_key,
'response': captcha_rs,
'remoteip': get_ip_addr(self.request.environ)
}
requests: added a default timeout for operation calling the endpoint url....
r2950 verify_rs = requests.get(url, params=params, verify=True, timeout=60)
re-captcha: adjust for v2 that is the only left one supported since 1st of May.
r2731 verify_rs = verify_rs.json()
captcha_status = verify_rs.get('success', False)
captcha_errors = verify_rs.get('error-codes', [])
if not isinstance(captcha_errors, list):
captcha_errors = [captcha_errors]
captcha_errors = ', '.join(captcha_errors)
captcha_message = ''
if captcha_status is False:
captcha_message = "Bad captcha. Errors: {}".format(
captcha_errors)
return captcha_status, captcha_message
login: moved to apps module
r1501 @view_config(
route_name='login', request_method='GET',
renderer='rhodecode:templates/login.mako')
def login(self):
login: switch to re-use the baseApp pyramid view.
r1536 c = self.load_default_context()
auth_user = self._rhodecode_user
login: moved to apps module
r1501
# redirect if already logged in
login: switch to re-use the baseApp pyramid view.
r1536 if (auth_user.is_authenticated and
not auth_user.is_default and auth_user.ip_allowed):
raise HTTPFound(c.came_from)
login: moved to apps module
r1501
# check if we use headers plugin, and try to login using it.
try:
log.debug('Running PRE-AUTH for headers based authentication')
auth_info = authenticate(
'', '', self.request.environ, HTTP_TYPE, skip_missing=True)
if auth_info:
auth: make the store_user_in_session public, as it's used in external auth plugins.
r3250 headers = store_user_in_session(
login: moved to apps module
r1501 self.session, auth_info.get('username'))
login: switch to re-use the baseApp pyramid view.
r1536 raise HTTPFound(c.came_from, headers=headers)
login: moved to apps module
r1501 except UserCreationError as e:
log.error(e)
pylons: fixed code and test suite after removal of pylons.
r2358 h.flash(e, category='error')
login: moved to apps module
r1501
login: switch to re-use the baseApp pyramid view.
r1536 return self._get_template_context(c)
login: moved to apps module
r1501
@view_config(
route_name='login', request_method='POST',
renderer='rhodecode:templates/login.mako')
def login_post(self):
login: switch to re-use the baseApp pyramid view.
r1536 c = self.load_default_context()
login: moved to apps module
r1501
pylons: remove pylons as dependency...
r2351 login_form = LoginForm(self.request.translate)()
login: moved to apps module
r1501
try:
self.session.invalidate()
login: don't use request.params because it allows to passing multiple...
r2149 form_result = login_form.to_python(self.request.POST)
login: moved to apps module
r1501 # form checks for username/password, now we're authenticated
auth: make the store_user_in_session public, as it's used in external auth plugins.
r3250 headers = store_user_in_session(
login: moved to apps module
r1501 self.session,
username=form_result['username'],
remember=form_result['remember'])
login: switch to re-use the baseApp pyramid view.
r1536 log.debug('Redirecting to "%s" after login.', c.came_from)
audit-logs: use new v2 api on login/logout/password reset views.
r1697
audit_user = audit_logger.UserWrap(
login: don't use request.params because it allows to passing multiple...
r2149 username=self.request.POST.get('username'),
audit-logs: use new v2 api on login/logout/password reset views.
r1697 ip_addr=self.request.remote_addr)
audit-logs: store user agent for login/logout actions.
r1702 action_data = {'user_agent': self.request.user_agent}
audit-logs: use specific web/api calls....
r1806 audit_logger.store_web(
audit-logs: implemented full audit logs across application....
r1829 'user.login.success', action_data=action_data,
audit-logs: store user agent for login/logout actions.
r1702 user=audit_user, commit=True)
audit-logs: use new v2 api on login/logout/password reset views.
r1697
login: switch to re-use the baseApp pyramid view.
r1536 raise HTTPFound(c.came_from, headers=headers)
login: moved to apps module
r1501 except formencode.Invalid as errors:
defaults = errors.value
# remove password from filling in form again
defaults.pop('password', None)
pylons: remove pylons as dependency...
r2351 render_ctx = {
login: moved to apps module
r1501 'errors': errors.error_dict,
'defaults': defaults,
pylons: remove pylons as dependency...
r2351 }
audit-logs: use new v2 api on login/logout/password reset views.
r1697
audit_user = audit_logger.UserWrap(
login: don't use request.params because it allows to passing multiple...
r2149 username=self.request.POST.get('username'),
audit-logs: use new v2 api on login/logout/password reset views.
r1697 ip_addr=self.request.remote_addr)
audit-logs: store user agent for login/logout actions.
r1702 action_data = {'user_agent': self.request.user_agent}
audit-logs: use specific web/api calls....
r1806 audit_logger.store_web(
audit-logs: implemented full audit logs across application....
r1829 'user.login.failure', action_data=action_data,
audit-logs: store user agent for login/logout actions.
r1702 user=audit_user, commit=True)
pylons: remove pylons as dependency...
r2351 return self._get_template_context(c, **render_ctx)
login: moved to apps module
r1501
except UserCreationError as e:
# headers auth or other auth functions that create users on
# the fly can throw this exception signaling that there's issue
# with user creation, explanation should be provided in
# Exception itself
pylons: fixed code and test suite after removal of pylons.
r2358 h.flash(e, category='error')
login: switch to re-use the baseApp pyramid view.
r1536 return self._get_template_context(c)
login: moved to apps module
r1501
@CSRFRequired()
@view_config(route_name='logout', request_method='POST')
def logout(self):
login: switch to re-use the baseApp pyramid view.
r1536 auth_user = self._rhodecode_user
log.info('Deleting session for user: `%s`', auth_user)
audit-logs: store user agent for login/logout actions.
r1702
action_data = {'user_agent': self.request.user_agent}
audit-logs: use specific web/api calls....
r1806 audit_logger.store_web(
audit-logs: implemented full audit logs across application....
r1829 'user.logout', action_data=action_data,
audit-logs: store user agent for login/logout actions.
r1702 user=auth_user, commit=True)
login: moved to apps module
r1501 self.session.delete()
home: moved home and repo group views into pyramid....
r1774 return HTTPFound(h.route_path('home'))
login: moved to apps module
r1501
@HasPermissionAnyDecorator(
'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
@view_config(
route_name='register', request_method='GET',
renderer='rhodecode:templates/register.mako',)
def register(self, defaults=None, errors=None):
login: switch to re-use the baseApp pyramid view.
r1536 c = self.load_default_context()
login: moved to apps module
r1501 defaults = defaults or {}
errors = errors or {}
settings = SettingsModel().get_all_settings()
register_message = settings.get('rhodecode_register_message') or ''
captcha = self._get_captcha_data()
auto_active = 'hg.register.auto_activate' in User.get_default_user()\
users: make AuthUser propert a method, and allow override of params.
r1997 .AuthUser().permissions['global']
login: moved to apps module
r1501
login: switch to re-use the baseApp pyramid view.
r1536 render_ctx = self._get_template_context(c)
login: moved to apps module
r1501 render_ctx.update({
'defaults': defaults,
'errors': errors,
'auto_active': auto_active,
'captcha_active': captcha.active,
'captcha_public_key': captcha.public_key,
'register_message': register_message,
})
return render_ctx
@HasPermissionAnyDecorator(
'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
@view_config(
route_name='register', request_method='POST',
renderer='rhodecode:templates/register.mako')
def register_post(self):
authentication: allow setting extern type with registration....
r3255 from rhodecode.authentication.plugins import auth_rhodecode
pylons: fixed code and test suite after removal of pylons.
r2358 self.load_default_context()
login: moved to apps module
r1501 captcha = self._get_captcha_data()
auto_active = 'hg.register.auto_activate' in User.get_default_user()\
users: make AuthUser propert a method, and allow override of params.
r1997 .AuthUser().permissions['global']
login: moved to apps module
r1501
authentication: allow setting extern type with registration....
r3255 extern_name = auth_rhodecode.RhodeCodeAuthPlugin.uid
extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
pylons: remove pylons as dependency...
r2351 register_form = RegisterForm(self.request.translate)()
login: moved to apps module
r1501 try:
login: don't use request.params because it allows to passing multiple...
r2149
form_result = register_form.to_python(self.request.POST)
login: moved to apps module
r1501 form_result['active'] = auto_active
authentication: allow setting extern type with registration....
r3255 external_identity = self.request.POST.get('external_identity')
if external_identity:
extern_name = external_identity
extern_type = external_identity
login: moved to apps module
r1501
if captcha.active:
re-captcha: adjust for v2 that is the only left one supported since 1st of May.
r2731 captcha_status, captcha_message = self.validate_captcha(
captcha.private_key)
if not captcha_status:
login: moved to apps module
r1501 _value = form_result
_msg = _('Bad captcha')
re-captcha: adjust for v2 that is the only left one supported since 1st of May.
r2731 error_dict = {'recaptcha_field': captcha_message}
raise formencode.Invalid(
_msg, _value, None, error_dict=error_dict)
login: moved to apps module
r1501
authentication: allow setting extern type with registration....
r3255 new_user = UserModel().create_registration(
form_result, extern_name=extern_name, extern_type=extern_type)
audit-logs: added user.register audit log entry.
r2384
action_data = {'data': new_user.get_api_data(),
'user_agent': self.request.user_agent}
audit: properly store IP address for user registration
r2732
authentication: allow setting extern type with registration....
r3255 if external_identity:
action_data['external_identity'] = external_identity
audit: properly store IP address for user registration
r2732 audit_user = audit_logger.UserWrap(
username=new_user.username,
user_id=new_user.user_id,
ip_addr=self.request.remote_addr)
audit-logs: added user.register audit log entry.
r2384 audit_logger.store_web(
'user.register', action_data=action_data,
audit: properly store IP address for user registration
r2732 user=audit_user)
audit-logs: added user.register audit log entry.
r2384
login: moved to apps module
r1501 event = UserRegistered(user=new_user, session=self.session)
pylons: fixed code and test suite after removal of pylons.
r2358 trigger(event)
h.flash(
registration: updated flash with more information after user registered.
r4058 _('You have successfully registered with RhodeCode. You can log-in now.'),
pylons: fixed code and test suite after removal of pylons.
r2358 category='success')
registration: updated flash with more information after user registered.
r4058 if external_identity:
h.flash(
_('Please use the {identity} button to log-in').format(
identity=external_identity),
category='success')
login: moved to apps module
r1501 Session().commit()
redirect_ro = self.request.route_path('login')
raise HTTPFound(redirect_ro)
except formencode.Invalid as errors:
errors.value.pop('password', None)
errors.value.pop('password_confirmation', None)
return self.register(
defaults=errors.value, errors=errors.error_dict)
except UserCreationError as e:
# container auth or other auth functions that create users on
# the fly can throw this exception signaling that there's issue
# with user creation, explanation should be provided in
# Exception itself
pylons: fixed code and test suite after removal of pylons.
r2358 h.flash(e, category='error')
login: moved to apps module
r1501 return self.register()
@view_config(
route_name='reset_password', request_method=('GET', 'POST'),
renderer='rhodecode:templates/password_reset.mako')
def password_reset(self):
pylons: fixed code and test suite after removal of pylons.
r2358 c = self.load_default_context()
login: moved to apps module
r1501 captcha = self._get_captcha_data()
pylons: fixed code and test suite after removal of pylons.
r2358 template_context = {
login: moved to apps module
r1501 'captcha_active': captcha.active,
'captcha_public_key': captcha.public_key,
'defaults': {},
'errors': {},
}
# always send implicit message to prevent from discovery of
# matching emails
msg = _('If such email exists, a password reset link was sent to it.')
auth/security: enforce that external users cannot reset their password.
r3258 def default_response():
log.debug('faking response on invalid password reset')
# make this take 2s, to prevent brute forcing.
time.sleep(2)
h.flash(msg, category='success')
return HTTPFound(self.request.route_path('reset_password'))
login: moved to apps module
r1501 if self.request.POST:
if h.HasPermissionAny('hg.password_reset.disabled')():
_email = self.request.POST.get('email', '')
log.error('Failed attempt to reset password for `%s`.', _email)
auth/security: enforce that external users cannot reset their password.
r3258 h.flash(_('Password reset has been disabled.'), category='error')
login: moved to apps module
r1501 return HTTPFound(self.request.route_path('reset_password'))
pylons: remove pylons as dependency...
r2351 password_reset_form = PasswordResetForm(self.request.translate)()
auth/security: enforce that external users cannot reset their password.
r3258 description = u'Generated token for password reset from {}'.format(
datetime.datetime.now().isoformat())
login: moved to apps module
r1501 try:
form_result = password_reset_form.to_python(
login: don't use request.params because it allows to passing multiple...
r2149 self.request.POST)
login: moved to apps module
r1501 user_email = form_result['email']
if captcha.active:
re-captcha: adjust for v2 that is the only left one supported since 1st of May.
r2731 captcha_status, captcha_message = self.validate_captcha(
captcha.private_key)
if not captcha_status:
login: moved to apps module
r1501 _value = form_result
_msg = _('Bad captcha')
re-captcha: adjust for v2 that is the only left one supported since 1st of May.
r2731 error_dict = {'recaptcha_field': captcha_message}
login: moved to apps module
r1501 raise formencode.Invalid(
_msg, _value, None, error_dict=error_dict)
# Generate reset URL and send mail.
user = User.get_by_email(user_email)
auth/security: enforce that external users cannot reset their password.
r3258 # only allow rhodecode based users to reset their password
# external auth shouldn't allow password reset
if user and user.extern_type != auth_rhodecode.RhodeCodeAuthPlugin.uid:
log.warning('User %s with external type `%s` tried a password reset. '
'This try was rejected', user, user.extern_type)
return default_response()
auth-tokens: abstracted adding token for users into UserModel method for easier usage in scripts, and in future in API.
r2951 # generate password reset token that expires in 10 minutes
reset_token = UserModel().add_auth_token(
user=user, lifetime_minutes=10,
role=UserModel.auth_token_role.ROLE_PASSWORD_RESET,
description=description)
login: moved to apps module
r1501 Session().commit()
log.debug('Successfully created password recovery token')
password_reset_url = self.request.route_url(
'reset_password_confirmation',
_query={'key': reset_token.api_key})
UserModel().reset_password_link(
form_result, password_reset_url)
audit-logs: use new v2 api on login/logout/password reset views.
r1697
audit-logs: store user agent for login/logout actions.
r1702 action_data = {'email': user_email,
'user_agent': self.request.user_agent}
audit-logs: use specific web/api calls....
r1806 audit_logger.store_web(
audit-logs: implemented full audit logs across application....
r1829 'user.password.reset_request', action_data=action_data,
audit-logs: use specific web/api calls....
r1806 user=self._rhodecode_user, commit=True)
auth/security: enforce that external users cannot reset their password.
r3258
return default_response()
login: moved to apps module
r1501
except formencode.Invalid as errors:
pylons: fixed code and test suite after removal of pylons.
r2358 template_context.update({
login: moved to apps module
r1501 'defaults': errors.value,
'errors': errors.error_dict,
})
login: don't use request.params because it allows to passing multiple...
r2149 if not self.request.POST.get('email'):
login: moved to apps module
r1501 # case of empty email, we want to report that
pylons: fixed code and test suite after removal of pylons.
r2358 return self._get_template_context(c, **template_context)
login: moved to apps module
r1501
if 'recaptcha_field' in errors.error_dict:
# case of failed captcha
pylons: fixed code and test suite after removal of pylons.
r2358 return self._get_template_context(c, **template_context)
login: moved to apps module
r1501
auth/security: enforce that external users cannot reset their password.
r3258 return default_response()
login: moved to apps module
r1501
pylons: fixed code and test suite after removal of pylons.
r2358 return self._get_template_context(c, **template_context)
login: moved to apps module
r1501
@view_config(route_name='reset_password_confirmation',
request_method='GET')
def password_reset_confirmation(self):
pylons: fixed code and test suite after removal of pylons.
r2358 self.load_default_context()
login: moved to apps module
r1501 if self.request.GET and self.request.GET.get('key'):
# make this take 2s, to prevent brute forcing.
time.sleep(2)
token = AuthTokenModel().get_auth_token(
self.request.GET.get('key'))
# verify token is the correct role
if token is None or token.role != UserApiKeys.ROLE_PASSWORD_RESET:
log.debug('Got token with role:%s expected is %s',
getattr(token, 'role', 'EMPTY_TOKEN'),
UserApiKeys.ROLE_PASSWORD_RESET)
pylons: fixed code and test suite after removal of pylons.
r2358 h.flash(
_('Given reset token is invalid'), category='error')
login: moved to apps module
r1501 return HTTPFound(self.request.route_path('reset_password'))
try:
owner = token.user
data = {'email': owner.email, 'token': token.api_key}
UserModel().reset_password(data)
pylons: fixed code and test suite after removal of pylons.
r2358 h.flash(
login: moved to apps module
r1501 _('Your password reset was successful, '
'a new password has been sent to your email'),
pylons: fixed code and test suite after removal of pylons.
r2358 category='success')
login: moved to apps module
r1501 except Exception as e:
log.error(e)
return HTTPFound(self.request.route_path('reset_password'))
return HTTPFound(self.request.route_path('login'))