##// END OF EJS Templates
exc-tracking: fixed API calls with new exceptions store engine
exc-tracking: fixed API calls with new exceptions store engine

File last commit:

r5096:a0018795 default
r5147:e3745ca4 default
Show More
user.py
1046 lines | 38.8 KiB | text/x-python | PythonLexer
copyrights: updated for 2023
r5088 # Copyright (C) 2010-2023 RhodeCode GmbH
project: added all source files and assets
r1 #
# 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/
"""
users model for RhodeCode
"""
import logging
import traceback
pylons: fixed code and test suite after removal of pylons.
r2358 import datetime
import ipaddress
project: added all source files and assets
r1
pylons: fixed code and test suite after removal of pylons.
r2358 from pyramid.threadlocal import get_current_request
project: added all source files and assets
r1 from sqlalchemy.exc import DatabaseError
dan
events: add event system for RepoEvents
r375 from rhodecode import events
admin-users: add audit page to allow showing user actions in RhodeCode....
r1559 from rhodecode.lib.user_log_filter import user_log_filter
project: added all source files and assets
r1 from rhodecode.lib.utils2 import (
models: major update for python3,...
r5070 get_current_rhodecode_user, action_logger_generic,
repo-groups: implemented default personal repo groups logic....
r1094 AttributeDict, str2bool)
models: major update for python3,...
r5070 from rhodecode.lib.str_utils import safe_str
security: escape always the provided user data like firstname/lastname.
r1780 from rhodecode.lib.exceptions import (
DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
dan
users: added option to detach pull requests for users which we delete....
r4351 UserOwnsUserGroupsException, NotAllowedToCreateUserError,
UserOwnsPullRequestsException, UserOwnsArtifactsException)
project: added all source files and assets
r1 from rhodecode.lib.caching_query import FromCache
from rhodecode.model import BaseModel
security: escape always the provided user data like firstname/lastname.
r1780 from rhodecode.model.db import (
users: autocomplete now sorts by matched username to show best matches first.
r4518 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
security: escape always the provided user data like firstname/lastname.
r1780 UserEmailMap, UserIpMap, UserLog)
project: added all source files and assets
r1 from rhodecode.model.meta import Session
dan
users: added option to detach pull requests for users which we delete....
r4351 from rhodecode.model.auth_token import AuthTokenModel
project: added all source files and assets
r1 from rhodecode.model.repo_group import RepoGroupModel
log = logging.getLogger(__name__)
class UserModel(BaseModel):
cls = User
def get(self, user_id, cache=False):
user = self.sa.query(User)
if cache:
caches: ensure we don't use non-ascii characters in cache keys....
r1749 user = user.options(
caches: cleanup code...
r5009 FromCache("sql_cache_short", f"get_user_{user_id}"))
project: added all source files and assets
r1 return user.get(user_id)
def get_user(self, user):
return self._get_user(user)
pull-request-reviewers: added option to add reviewers by picking an user group for pull requests....
r1678 def _serialize_user(self, user):
import rhodecode.lib.helpers as h
return {
'id': user.user_id,
security: use new safe escaped user attributes across the application....
r1815 'first_name': user.first_name,
'last_name': user.last_name,
pull-request-reviewers: added option to add reviewers by picking an user group for pull requests....
r1678 'username': user.username,
'email': user.email,
'icon_link': h.gravatar_url(user.email, 30),
default-reviewers: introduce new voting rule logic that allows...
r2484 'profile_link': h.link_to_user(user),
security: escape always the provided user data like firstname/lastname.
r1780 'value_display': h.escape(h.person(user)),
pull-request-reviewers: added option to add reviewers by picking an user group for pull requests....
r1678 'value': user.username,
'value_type': 'user',
'active': user.active,
}
users: moved get_users from RepoModel to UserModel.
r1677 def get_users(self, name_contains=None, limit=20, only_active=True):
query = self.sa.query(User)
if only_active:
query = query.filter(User.active == true())
if name_contains:
modernize: python3 updates
r5096 ilike_expression = f'%{safe_str(name_contains)}%'
users: moved get_users from RepoModel to UserModel.
r1677 query = query.filter(
or_(
User.name.ilike(ilike_expression),
User.lastname.ilike(ilike_expression),
User.username.ilike(ilike_expression)
)
)
users: autocomplete now sorts by matched username to show best matches first.
r4518 # sort by len to have top most matches first
query = query.order_by(func.length(User.username))\
.order_by(User.username)
users: moved get_users from RepoModel to UserModel.
r1677 query = query.limit(limit)
users: autocomplete now sorts by matched username to show best matches first.
r4518
users: moved get_users from RepoModel to UserModel.
r1677 users = query.all()
_users = [
pull-request-reviewers: added option to add reviewers by picking an user group for pull requests....
r1678 self._serialize_user(user) for user in users
users: moved get_users from RepoModel to UserModel.
r1677 ]
return _users
project: added all source files and assets
r1 def get_by_username(self, username, cache=False, case_insensitive=False):
if case_insensitive:
user = self.sa.query(User).filter(User.username.ilike(username))
else:
user = self.sa.query(User)\
.filter(User.username == username)
if cache:
caches: ensure we don't use non-ascii characters in cache keys....
r1749 name_key = _hash_key(username)
user = user.options(
caches: cleanup code...
r5009 FromCache("sql_cache_short", f"get_user_{name_key}"))
project: added all source files and assets
r1 return user.scalar()
def get_by_email(self, email, cache=False, case_insensitive=False):
return User.get_by_email(email, case_insensitive, cache)
def get_by_auth_token(self, auth_token, cache=False):
return User.get_by_auth_token(auth_token, cache)
def get_active_user_count(self, cache=False):
users: allow caching of active users count.
r2427 qry = User.query().filter(
User.active == true()).filter(
User.username != User.DEFAULT_USER)
if cache:
qry = qry.options(
FromCache("sql_cache_short", "get_active_users"))
return qry.count()
project: added all source files and assets
r1
def create(self, form_data, cur_user=None):
if not cur_user:
cur_user = getattr(get_current_rhodecode_user(), 'username', None)
user_data = {
'username': form_data['username'],
'password': form_data['password'],
'email': form_data['email'],
'firstname': form_data['firstname'],
'lastname': form_data['lastname'],
'active': form_data['active'],
'extern_type': form_data['extern_type'],
'extern_name': form_data['extern_name'],
'admin': False,
'cur_user': cur_user
}
repo-groups: implemented default personal repo groups logic....
r1094 if 'create_repo_group' in form_data:
user_data['create_repo_group'] = str2bool(
form_data.get('create_repo_group'))
project: added all source files and assets
r1 try:
if form_data.get('password_change'):
user_data['force_password_change'] = True
return UserModel().create_or_update(**user_data)
except Exception:
log.error(traceback.format_exc())
raise
def update_user(self, user, skip_attrs=None, **kwargs):
from rhodecode.lib.auth import get_crypt_password
user = self._get_user(user)
if user.username == User.DEFAULT_USER:
raise DefaultUserException(
pylons: fixed code and test suite after removal of pylons.
r2358 "You can't edit this user (`%(username)s`) since it's "
"crucial for entire application" % {
'username': user.username})
project: added all source files and assets
r1
# first store only defaults
user_attrs = {
'updating_user_id': user.user_id,
'username': user.username,
'password': user.password,
'email': user.email,
'firstname': user.name,
'lastname': user.lastname,
users: add edition of description in admin view for users
r4022 'description': user.description,
project: added all source files and assets
r1 'active': user.active,
'admin': user.admin,
'extern_name': user.extern_name,
'extern_type': user.extern_type,
'language': user.user_data.get('language')
}
# in case there's new_password, that comes from form, use it to
# store password
if kwargs.get('new_password'):
kwargs['password'] = kwargs['new_password']
# cleanups, my_account password change form
kwargs.pop('current_password', None)
kwargs.pop('new_password', None)
# cleanups, user edit password change form
kwargs.pop('password_confirmation', None)
kwargs.pop('password_change', None)
# create repo group on user creation
kwargs.pop('create_repo_group', None)
# legacy forms send name, which is the firstname
firstname = kwargs.pop('name', None)
if firstname:
kwargs['firstname'] = firstname
for k, v in kwargs.items():
# skip if we don't want to update this
if skip_attrs and k in skip_attrs:
continue
user_attrs[k] = v
try:
return self.create_or_update(**user_attrs)
except Exception:
log.error(traceback.format_exc())
raise
def create_or_update(
self, username, password, email, firstname='', lastname='',
active=True, admin=False, extern_type=None, extern_name=None,
cur_user=None, plugin=None, force_password_change=False,
repo-groups: implemented default personal repo groups logic....
r1094 allow_to_create_user=True, create_repo_group=None,
users: description edit fixes...
r4024 updating_user_id=None, language=None, description='',
users: add edition of description in admin view for users
r4022 strict_creation_check=True):
project: added all source files and assets
r1 """
Creates a new instance if not found, or updates current one
:param username:
:param password:
:param email:
:param firstname:
:param lastname:
:param active:
:param admin:
:param extern_type:
:param extern_name:
:param cur_user:
:param plugin: optional plugin this method was called from
:param force_password_change: toggles new or existing user flag
for password change
:param allow_to_create_user: Defines if the method can actually create
new users
:param create_repo_group: Defines if the method should also
create an repo group with user name, and owner
:param updating_user_id: if we set it up this is the user we want to
update this allows to editing username.
:param language: language of user from interface.
users: description edit fixes...
r4024 :param description: user description
:param strict_creation_check: checks for allowed creation license wise etc.
project: added all source files and assets
r1
:returns: new User object with injected `is_new_user` attribute.
"""
pylons: fixed code and test suite after removal of pylons.
r2358
project: added all source files and assets
r1 if not cur_user:
cur_user = getattr(get_current_rhodecode_user(), 'username', None)
from rhodecode.lib.auth import (
dan
users: added option to detach pull requests for users which we delete....
r4351 get_crypt_password, check_password)
comments: added rcextensions hoooks for comment editing, and renamed methods to remove odd log_ prefix which...
r4445 from rhodecode.lib import hooks_base
project: added all source files and assets
r1
def _password_change(new_user, password):
auth-rhodecode: don't fail on bcrypt if user password is set to None....
r2153 old_password = new_user.password or ''
project: added all source files and assets
r1 # empty password
auth-rhodecode: don't fail on bcrypt if user password is set to None....
r2153 if not old_password:
project: added all source files and assets
r1 return False
# password check is only needed for RhodeCode internal auth calls
# in case it's a plugin we don't care
if not plugin:
repo-groups: implemented default personal repo groups logic....
r1094 # first check if we gave crypted password back, and if it
# matches it's not password change
project: added all source files and assets
r1 if new_user.password == password:
return False
auth-rhodecode: don't fail on bcrypt if user password is set to None....
r2153 password_match = check_password(password, old_password)
project: added all source files and assets
r1 if not password_match:
return True
return False
repo-groups: implemented default personal repo groups logic....
r1094 # read settings on default personal repo group creation
if create_repo_group is None:
default_create_repo_group = RepoGroupModel()\
.get_default_create_personal_repo_group()
create_repo_group = default_create_repo_group
project: added all source files and assets
r1 user_data = {
'username': username,
'password': password,
'email': email,
'firstname': firstname,
'lastname': lastname,
'active': active,
'admin': admin
}
if updating_user_id:
log.debug('Checking for existing account in RhodeCode '
logging: use lazy parameter evaluation in log calls.
r3061 'database with user_id `%s` ', updating_user_id)
project: added all source files and assets
r1 user = User.get(updating_user_id)
else:
log.debug('Checking for existing account in RhodeCode '
logging: use lazy parameter evaluation in log calls.
r3061 'database with username `%s` ', username)
project: added all source files and assets
r1 user = User.get_by_username(username, case_insensitive=True)
if user is None:
# we check internal flag if this method is actually allowed to
# create new user
if not allow_to_create_user:
msg = ('Method wants to create new user, but it is not '
'allowed to do so')
log.warning(msg)
raise NotAllowedToCreateUserError(msg)
log.debug('Creating new user %s', username)
# only if we create user that is active
new_active_user = active
if new_active_user and strict_creation_check:
# raises UserCreationError if it's not allowed for any reason to
# create new active user, this also executes pre-create hooks
comments: added rcextensions hoooks for comment editing, and renamed methods to remove odd log_ prefix which...
r4445 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
dan
events: add event system for RepoEvents
r375 events.trigger(events.UserPreCreate(user_data))
project: added all source files and assets
r1 new_user = User()
edit = False
else:
dan
logging: small updates for logging.
r3944 log.debug('updating user `%s`', username)
dan
events: add event system for RepoEvents
r375 events.trigger(events.UserPreUpdate(user, user_data))
project: added all source files and assets
r1 new_user = user
edit = True
# we're not allowed to edit default user
if user.username == User.DEFAULT_USER:
raise DefaultUserException(
pylons: fixed code and test suite after removal of pylons.
r2358 "You can't edit this user (`%(username)s`) since it's "
"crucial for entire application"
% {'username': user.username})
project: added all source files and assets
r1
# inject special attribute that will tell us if User is new or old
new_user.is_new_user = not edit
# for users that didn's specify auth type, we use RhodeCode built in
from rhodecode.authentication.plugins import auth_rhodecode
authentication: allow setting extern type with registration....
r3255 extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid
extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid
project: added all source files and assets
r1
try:
new_user.username = username
new_user.admin = admin
new_user.email = email
new_user.active = active
models: major update for python3,...
r5070 new_user.extern_name = safe_str(extern_name)
new_user.extern_type = safe_str(extern_type)
project: added all source files and assets
r1 new_user.name = firstname
new_user.lastname = lastname
users: add edition of description in admin view for users
r4022 new_user.description = description
project: added all source files and assets
r1
# set password only if creating an user or password is changed
if not edit or _password_change(new_user, password):
reason = 'new password' if edit else 'new user'
log.debug('Updating password reason=>%s', reason)
new_user.password = get_crypt_password(password) if password else None
if force_password_change:
new_user.update_userdata(force_password_change=True)
if language:
new_user.update_userdata(language=language)
notifications: store notification status in channelstream
r734 new_user.update_userdata(notification_status=True)
project: added all source files and assets
r1
self.sa.add(new_user)
if not edit and create_repo_group:
repo-groups: implemented default personal repo groups logic....
r1094 RepoGroupModel().create_personal_repo_group(
new_user, commit_early=False)
project: added all source files and assets
r1 if not edit:
# add the RSS token
auth-tokens: abstracted adding token for users into UserModel method for easier usage in scripts, and in future in API.
r2951 self.add_auth_token(
user=username, lifetime_minutes=-1,
role=self.auth_token_role.ROLE_FEED,
modernize: python3 updates
r5096 description='Generated feed token')
auth-tokens: abstracted adding token for users into UserModel method for easier usage in scripts, and in future in API.
r2951
user: deprecated usage of api_keys....
r1953 kwargs = new_user.get_dict()
# backward compat, require api_keys present
kwargs['api_keys'] = kwargs['auth_tokens']
comments: added rcextensions hoooks for comment editing, and renamed methods to remove odd log_ prefix which...
r4445 hooks_base.create_user(created_by=cur_user, **kwargs)
repo-groups: implemented default personal repo groups logic....
r1094 events.trigger(events.UserPostCreate(user_data))
project: added all source files and assets
r1 return new_user
except (DatabaseError,):
log.error(traceback.format_exc())
raise
authentication: allow setting extern type with registration....
r3255 def create_registration(self, form_data,
extern_name='rhodecode', extern_type='rhodecode'):
project: added all source files and assets
r1 from rhodecode.model.notification import NotificationModel
from rhodecode.model.notification import EmailNotificationModel
try:
form_data['admin'] = False
authentication: allow setting extern type with registration....
r3255 form_data['extern_name'] = extern_name
form_data['extern_type'] = extern_type
project: added all source files and assets
r1 new_user = self.create(form_data)
self.sa.add(new_user)
self.sa.flush()
user_data = new_user.get_dict()
registration: properly expose first_name/last_name into email fake object.
r4057 user_data.update({
'first_name': user_data.get('firstname'),
'last_name': user_data.get('lastname'),
})
project: added all source files and assets
r1 kwargs = {
# use SQLALCHEMY safe dump of user data
'user': AttributeDict(user_data),
'date': datetime.datetime.now()
}
notification_type = EmailNotificationModel.TYPE_REGISTRATION
# create notification objects, and emails
NotificationModel().create(
created_by=new_user,
notifications: skip double rendering just to generate email title/desc
r4560 notification_subject='', # Filled in based on the notification_type
notification_body='', # Filled in based on the notification_type
project: added all source files and assets
r1 notification_type=notification_type,
recipients=None, # all admins
email_kwargs=kwargs,
)
return new_user
except Exception:
log.error(traceback.format_exc())
raise
dan
users: added option to detach pull requests for users which we delete....
r4351 def _handle_user_repos(self, username, repositories, handle_user,
handle_mode=None):
project: added all source files and assets
r1 left_overs = True
from rhodecode.model.repo import RepoModel
if handle_mode == 'detach':
for obj in repositories:
dan
users: added option to detach pull requests for users which we delete....
r4351 obj.user = handle_user
project: added all source files and assets
r1 # set description we know why we super admin now owns
# additional repositories that were orphaned !
obj.description += ' \n::detached repository from deleted user: %s' % (username,)
self.sa.add(obj)
left_overs = False
elif handle_mode == 'delete':
for obj in repositories:
RepoModel().delete(obj, forks='detach')
left_overs = False
# if nothing is done we have left overs left
return left_overs
dan
users: added option to detach pull requests for users which we delete....
r4351 def _handle_user_repo_groups(self, username, repository_groups, handle_user,
project: added all source files and assets
r1 handle_mode=None):
dan
users: added option to detach pull requests for users which we delete....
r4351
project: added all source files and assets
r1 left_overs = True
from rhodecode.model.repo_group import RepoGroupModel
if handle_mode == 'detach':
for r in repository_groups:
dan
users: added option to detach pull requests for users which we delete....
r4351 r.user = handle_user
project: added all source files and assets
r1 # set description we know why we super admin now owns
# additional repositories that were orphaned !
r.group_description += ' \n::detached repository group from deleted user: %s' % (username,)
users: when deleting users ensure we also clear personal flag so we don't have multiple personal groups...
r3038 r.personal = False
project: added all source files and assets
r1 self.sa.add(r)
left_overs = False
elif handle_mode == 'delete':
for r in repository_groups:
RepoGroupModel().delete(r)
left_overs = False
# if nothing is done we have left overs left
return left_overs
dan
users: added option to detach pull requests for users which we delete....
r4351 def _handle_user_user_groups(self, username, user_groups, handle_user,
handle_mode=None):
project: added all source files and assets
r1 left_overs = True
from rhodecode.model.user_group import UserGroupModel
if handle_mode == 'detach':
for r in user_groups:
for user_user_group_to_perm in r.user_user_group_to_perm:
if user_user_group_to_perm.user.username == username:
dan
users: added option to detach pull requests for users which we delete....
r4351 user_user_group_to_perm.user = handle_user
r.user = handle_user
project: added all source files and assets
r1 # set description we know why we super admin now owns
# additional repositories that were orphaned !
r.user_group_description += ' \n::detached user group from deleted user: %s' % (username,)
self.sa.add(r)
left_overs = False
elif handle_mode == 'delete':
for r in user_groups:
UserGroupModel().delete(r)
left_overs = False
# if nothing is done we have left overs left
return left_overs
dan
users: added option to detach pull requests for users which we delete....
r4351 def _handle_user_pull_requests(self, username, pull_requests, handle_user,
handle_mode=None):
left_overs = True
from rhodecode.model.pull_request import PullRequestModel
if handle_mode == 'detach':
for pr in pull_requests:
pr.user_id = handle_user.user_id
# set description we know why we super admin now owns
# additional repositories that were orphaned !
pr.description += ' \n::detached pull requests from deleted user: %s' % (username,)
self.sa.add(pr)
left_overs = False
elif handle_mode == 'delete':
for pr in pull_requests:
PullRequestModel().delete(pr)
left_overs = False
models: major update for python3,...
r5070 # if nothing is done we have leftovers left
dan
users: added option to detach pull requests for users which we delete....
r4351 return left_overs
def _handle_user_artifacts(self, username, artifacts, handle_user,
handle_mode=None):
artifacts: handle detach/delete of artifacts for users who own them and are to be deleted....
r4011 left_overs = True
if handle_mode == 'detach':
for a in artifacts:
dan
users: added option to detach pull requests for users which we delete....
r4351 a.upload_user = handle_user
artifacts: handle detach/delete of artifacts for users who own them and are to be deleted....
r4011 # set description we know why we super admin now owns
# additional artifacts that were orphaned !
a.file_description += ' \n::detached artifact from deleted user: %s' % (username,)
self.sa.add(a)
left_overs = False
elif handle_mode == 'delete':
from rhodecode.apps.file_store import utils as store_utils
dan
users: added option to detach pull requests for users which we delete....
r4351 request = get_current_request()
storage = store_utils.get_file_storage(request.registry.settings)
artifacts: handle detach/delete of artifacts for users who own them and are to be deleted....
r4011 for a in artifacts:
file_uid = a.file_uid
storage.delete(file_uid)
self.sa.delete(a)
left_overs = False
# if nothing is done we have left overs left
return left_overs
project: added all source files and assets
r1 def delete(self, user, cur_user=None, handle_repos=None,
dan
users: added option to detach pull requests for users which we delete....
r4351 handle_repo_groups=None, handle_user_groups=None,
handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
comments: added rcextensions hoooks for comment editing, and renamed methods to remove odd log_ prefix which...
r4445 from rhodecode.lib import hooks_base
users: fetch user data for user removal hook, *before* actually marking object for deletion.
r3979
project: added all source files and assets
r1 if not cur_user:
tests: stabilize tests for mysql/postgres.
r3981 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
dan
users: added option to detach pull requests for users which we delete....
r4351
project: added all source files and assets
r1 user = self._get_user(user)
try:
if user.username == User.DEFAULT_USER:
raise DefaultUserException(
modernize: updates for python3
r5095 "You can't remove this user since it's"
" crucial for entire application")
dan
users: added option to detach pull requests for users which we delete....
r4351 handle_user = handle_new_owner or self.cls.get_first_super_admin()
log.debug('New detached objects owner %s', handle_user)
project: added all source files and assets
r1
left_overs = self._handle_user_repos(
dan
users: added option to detach pull requests for users which we delete....
r4351 user.username, user.repositories, handle_user, handle_repos)
project: added all source files and assets
r1 if left_overs and user.repositories:
repos = [x.repo_name for x in user.repositories]
raise UserOwnsReposException(
modernize: updates for python3
r5095 'user "%(username)s" still owns %(len_repos)s repositories and cannot be '
'removed. Switch owners or remove those repositories:%(list_repos)s'
pylons: fixed code and test suite after removal of pylons.
r2358 % {'username': user.username, 'len_repos': len(repos),
'list_repos': ', '.join(repos)})
project: added all source files and assets
r1
left_overs = self._handle_user_repo_groups(
dan
users: added option to detach pull requests for users which we delete....
r4351 user.username, user.repository_groups, handle_user, handle_repo_groups)
project: added all source files and assets
r1 if left_overs and user.repository_groups:
repo_groups = [x.group_name for x in user.repository_groups]
raise UserOwnsRepoGroupsException(
modernize: updates for python3
r5095 'user "%(username)s" still owns %(len_repo_groups)s repository groups and cannot be '
'removed. Switch owners or remove those repository groups:%(list_repo_groups)s'
pylons: fixed code and test suite after removal of pylons.
r2358 % {'username': user.username, 'len_repo_groups': len(repo_groups),
'list_repo_groups': ', '.join(repo_groups)})
project: added all source files and assets
r1
left_overs = self._handle_user_user_groups(
dan
users: added option to detach pull requests for users which we delete....
r4351 user.username, user.user_groups, handle_user, handle_user_groups)
project: added all source files and assets
r1 if left_overs and user.user_groups:
user_groups = [x.users_group_name for x in user.user_groups]
raise UserOwnsUserGroupsException(
modernize: updates for python3
r5095 'user "%s" still owns %s user groups and cannot be '
'removed. Switch owners or remove those user groups:%s'
project: added all source files and assets
r1 % (user.username, len(user_groups), ', '.join(user_groups)))
dan
users: added option to detach pull requests for users which we delete....
r4351 left_overs = self._handle_user_pull_requests(
user.username, user.user_pull_requests, handle_user, handle_pull_requests)
if left_overs and user.user_pull_requests:
modernize: updates for python3
r5095 pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests]
dan
users: added option to detach pull requests for users which we delete....
r4351 raise UserOwnsPullRequestsException(
modernize: updates for python3
r5095 'user "%s" still owns %s pull requests and cannot be '
'removed. Switch owners or remove those pull requests:%s'
dan
users: added option to detach pull requests for users which we delete....
r4351 % (user.username, len(pull_requests), ', '.join(pull_requests)))
artifacts: handle detach/delete of artifacts for users who own them and are to be deleted....
r4011 left_overs = self._handle_user_artifacts(
dan
users: added option to detach pull requests for users which we delete....
r4351 user.username, user.artifacts, handle_user, handle_artifacts)
artifacts: handle detach/delete of artifacts for users who own them and are to be deleted....
r4011 if left_overs and user.artifacts:
artifacts = [x.file_uid for x in user.artifacts]
raise UserOwnsArtifactsException(
modernize: updates for python3
r5095 'user "%s" still owns %s artifacts and cannot be '
'removed. Switch owners or remove those artifacts:%s'
artifacts: handle detach/delete of artifacts for users who own them and are to be deleted....
r4011 % (user.username, len(artifacts), ', '.join(artifacts)))
users: fetch user data for user removal hook, *before* actually marking object for deletion.
r3979 user_data = user.get_dict() # fetch user data before expire
project: added all source files and assets
r1 # we might change the user data with detach/delete, make sure
# the object is marked as expired before actually deleting !
self.sa.expire(user)
self.sa.delete(user)
users: fetch user data for user removal hook, *before* actually marking object for deletion.
r3979
comments: added rcextensions hoooks for comment editing, and renamed methods to remove odd log_ prefix which...
r4445 hooks_base.delete_user(deleted_by=cur_user, **user_data)
project: added all source files and assets
r1 except Exception:
log.error(traceback.format_exc())
raise
login: Fix password reset mail link....
r37 def reset_password_link(self, data, pwd_reset_url):
project: added all source files and assets
r1 from rhodecode.lib.celerylib import tasks, run_task
from rhodecode.model.notification import EmailNotificationModel
user_email = data['email']
try:
user = User.get_by_email(user_email)
if user:
log.debug('password reset user found %s', user)
email_kwargs = {
login: Fix password reset mail link....
r37 'password_reset_url': pwd_reset_url,
project: added all source files and assets
r1 'user': user,
'email': user_email,
dan
emails: updated emails design and data structure they provide....
r4038 'date': datetime.datetime.now(),
'first_admin_email': User.get_first_super_admin().email
project: added all source files and assets
r1 }
emails: set References header for threading in mail user agents even with different subjects...
r4447 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
project: added all source files and assets
r1 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
recipients = [user_email]
action_logger_generic(
'sending password reset email to user: {}'.format(
user), namespace='security.password_reset')
run_task(tasks.send_email, recipients, subject,
email_body_plaintext, email_body)
else:
log.debug("password reset email %s not found", user_email)
except Exception:
log.error(traceback.format_exc())
return False
return True
password-reset: strengthten security on password reset logic....
r1471 def reset_password(self, data):
project: added all source files and assets
r1 from rhodecode.lib.celerylib import tasks, run_task
from rhodecode.model.notification import EmailNotificationModel
from rhodecode.lib import auth
user_email = data['email']
pre_db = True
try:
user = User.get_by_email(user_email)
new_passwd = auth.PasswordGenerator().gen_password(
12, auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
if user:
user.password = auth.get_crypt_password(new_passwd)
# also force this user to reset his password !
user.update_userdata(force_password_change=True)
Session().add(user)
password-reset: strengthten security on password reset logic....
r1471
# now delete the token in question
UserApiKeys = AuthTokenModel.cls
UserApiKeys().query().filter(
UserApiKeys.api_key == data['token']).delete()
project: added all source files and assets
r1 Session().commit()
logging: improve slightly password reset logs.
r1296 log.info('successfully reset password for `%s`', user_email)
password-reset: strengthten security on password reset logic....
r1471
project: added all source files and assets
r1 if new_passwd is None:
raise Exception('unable to generate new password')
pre_db = False
email_kwargs = {
'new_password': new_passwd,
'user': user,
'email': user_email,
dan
emails: updated emails design and data structure they provide....
r4038 'date': datetime.datetime.now(),
'first_admin_email': User.get_first_super_admin().email
project: added all source files and assets
r1 }
emails: set References header for threading in mail user agents even with different subjects...
r4447 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
password-reset: strengthten security on password reset logic....
r1471 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
**email_kwargs)
project: added all source files and assets
r1
recipients = [user_email]
action_logger_generic(
'sent new password to user: {} with email: {}'.format(
user, user_email), namespace='security.password_reset')
run_task(tasks.send_email, recipients, subject,
email_body_plaintext, email_body)
except Exception:
log.error('Failed to update user password')
log.error(traceback.format_exc())
if pre_db:
# we rollback only if local db stuff fails. If it goes into
# run_task, we're pass rollback state this wouldn't work then
Session().rollback()
return True
def fill_data(self, auth_user, user_id=None, api_key=None, username=None):
"""
Fetches auth_user by user_id,or api_key if present.
Fills auth_user attributes with those taken from database.
Additionally set's is_authenitated if lookup fails
present in database
:param auth_user: instance of user to set attributes
:param user_id: user id to fetch by
:param api_key: api key to fetch by
:param username: username to fetch by
"""
logs: fix leaking of tokens to logging. Fixes #5452
r2657 def token_obfuscate(token):
if token:
return token[:4] + "****"
project: added all source files and assets
r1 if user_id is None and api_key is None and username is None:
raise Exception('You need to pass user_id, api_key or username')
log.debug(
auth: improve logging
r1955 'AuthUser: fill data execution based on: '
'user_id:%s api_key:%s username:%s', user_id, api_key, username)
project: added all source files and assets
r1 try:
dbuser = None
if user_id:
dbuser = self.get(user_id)
elif api_key:
dbuser = self.get_by_auth_token(api_key)
elif username:
dbuser = self.get_by_username(username)
if not dbuser:
log.warning(
'Unable to lookup user by id:%s api_key:%s username:%s',
logs: fix leaking of tokens to logging. Fixes #5452
r2657 user_id, token_obfuscate(api_key), username)
project: added all source files and assets
r1 return False
if not dbuser.active:
users: personal repo-group shouldn't be available for default user.
r1690 log.debug('User `%s:%s` is inactive, skipping fill data',
username, user_id)
project: added all source files and assets
r1 return False
auth: improve logging
r1955 log.debug('AuthUser: filling found user:%s data', dbuser)
project: added all source files and assets
r1
user: speed up data propagatation for auth users by pre-filling only selected variables...
r4018 attrs = {
'user_id': dbuser.user_id,
'username': dbuser.username,
'name': dbuser.name,
security: use new safe escaped user attributes across the application....
r1815 'first_name': dbuser.first_name,
user: speed up data propagatation for auth users by pre-filling only selected variables...
r4018 'firstname': dbuser.firstname,
security: use new safe escaped user attributes across the application....
r1815 'last_name': dbuser.last_name,
user: speed up data propagatation for auth users by pre-filling only selected variables...
r4018 'lastname': dbuser.lastname,
'admin': dbuser.admin,
'active': dbuser.active,
'email': dbuser.email,
'emails': dbuser.emails_cached(),
'short_contact': dbuser.short_contact,
'full_contact': dbuser.full_contact,
'full_name': dbuser.full_name,
'full_name_or_username': dbuser.full_name_or_username,
project: added all source files and assets
r1
user: speed up data propagatation for auth users by pre-filling only selected variables...
r4018 '_api_key': dbuser._api_key,
'_user_data': dbuser._user_data,
'created_on': dbuser.created_on,
'extern_name': dbuser.extern_name,
'extern_type': dbuser.extern_type,
project: added all source files and assets
r1
user: speed up data propagatation for auth users by pre-filling only selected variables...
r4018 'inherit_default_permissions': dbuser.inherit_default_permissions,
'language': dbuser.language,
'last_activity': dbuser.last_activity,
'last_login': dbuser.last_login,
'password': dbuser.password,
}
auth_user.__dict__.update(attrs)
project: added all source files and assets
r1 except Exception:
log.error(traceback.format_exc())
auth_user.is_authenticated = False
return False
return True
def has_perm(self, user, perm):
perm = self._get_perm(perm)
user = self._get_user(user)
return UserToPerm.query().filter(UserToPerm.user == user)\
.filter(UserToPerm.permission == perm).scalar() is not None
def grant_perm(self, user, perm):
"""
Grant user global permissions
:param user:
:param perm:
"""
user = self._get_user(user)
perm = self._get_perm(perm)
# if this permission is already granted skip it
_perm = UserToPerm.query()\
.filter(UserToPerm.user == user)\
.filter(UserToPerm.permission == perm)\
.scalar()
if _perm:
return
new = UserToPerm()
new.user = user
new.permission = perm
self.sa.add(new)
return new
def revoke_perm(self, user, perm):
"""
Revoke users global permissions
:param user:
:param perm:
"""
user = self._get_user(user)
perm = self._get_perm(perm)
obj = UserToPerm.query()\
.filter(UserToPerm.user == user)\
.filter(UserToPerm.permission == perm)\
.scalar()
if obj:
self.sa.delete(obj)
def add_extra_email(self, user, email):
"""
Adds email address to UserEmailMap
:param user:
:param email:
"""
pylons: remove pylons as dependency...
r2351
project: added all source files and assets
r1 user = self._get_user(user)
obj = UserEmailMap()
obj.user = user
pylons: remove pylons as dependency...
r2351 obj.email = email
project: added all source files and assets
r1 self.sa.add(obj)
return obj
def delete_extra_email(self, user, email_id):
"""
Removes email address from UserEmailMap
:param user:
:param email_id:
"""
user = self._get_user(user)
obj = UserEmailMap.query().get(email_id)
audit-logs: added audit-logs on user actions....
r1801 if obj and obj.user_id == user.user_id:
project: added all source files and assets
r1 self.sa.delete(obj)
def parse_ip_range(self, ip_range):
ip_list = []
dependencies: bumped pyramid-debugtoolbar to 4.3.1
r1907
project: added all source files and assets
r1 def make_unique(value):
seen = []
return [c for c in value if not (c in seen or seen.append(c))]
# firsts split by commas
for ip_range in ip_range.split(','):
if not ip_range:
continue
ip_range = ip_range.strip()
if '-' in ip_range:
start_ip, end_ip = ip_range.split('-', 1)
models: major update for python3,...
r5070 start_ip = ipaddress.ip_address(safe_str(start_ip.strip()))
end_ip = ipaddress.ip_address(safe_str(end_ip.strip()))
project: added all source files and assets
r1 parsed_ip_range = []
dan
users: added option to detach pull requests for users which we delete....
r4351 for index in range(int(start_ip), int(end_ip) + 1):
project: added all source files and assets
r1 new_ip = ipaddress.ip_address(index)
parsed_ip_range.append(str(new_ip))
ip_list.extend(parsed_ip_range)
else:
ip_list.append(ip_range)
return make_unique(ip_list)
def add_extra_ip(self, user, ip, description=None):
"""
Adds ip address to UserIpMap
:param user:
:param ip:
"""
pylons: remove pylons as dependency...
r2351
project: added all source files and assets
r1 user = self._get_user(user)
obj = UserIpMap()
obj.user = user
pylons: remove pylons as dependency...
r2351 obj.ip_addr = ip
project: added all source files and assets
r1 obj.description = description
self.sa.add(obj)
return obj
auth-tokens: abstracted adding token for users into UserModel method for easier usage in scripts, and in future in API.
r2951 auth_token_role = AuthTokenModel.cls
modernize: updates for python3
r5095 def add_auth_token(self, user, lifetime_minutes, role, description='',
auth-tokens: abstracted adding token for users into UserModel method for easier usage in scripts, and in future in API.
r2951 scope_callback=None):
"""
Add AuthToken for user.
:param user: username/user_id
:param lifetime_minutes: in minutes the lifetime for token, -1 equals no limit
:param role: one of AuthTokenModel.cls.ROLE_*
:param description: optional string description
"""
token = AuthTokenModel().create(
user, description, lifetime_minutes, role)
if scope_callback and callable(scope_callback):
# call the callback if we provide, used to attach scope for EE edition
scope_callback(token)
return token
project: added all source files and assets
r1 def delete_extra_ip(self, user, ip_id):
"""
Removes ip address from UserIpMap
:param user:
:param ip_id:
"""
user = self._get_user(user)
obj = UserIpMap.query().get(ip_id)
audit-logs: added audit-logs on user actions....
r1801 if obj and obj.user_id == user.user_id:
project: added all source files and assets
r1 self.sa.delete(obj)
def get_accounts_in_creation_order(self, current_user=None):
"""
Get accounts in order of creation for deactivation for license limits
pick currently logged in user, and append to the list in position 0
pick all super-admins in order of creation date and add it to the list
pick all other accounts in order of creation and add it to the list.
Based on that list, the last accounts can be disabled as they are
created at the end and don't include any of the super admins as well
as the current user.
:param current_user: optionally current user running this operation
"""
if not current_user:
current_user = get_current_rhodecode_user()
active_super_admins = [
x.user_id for x in User.query()
.filter(User.user_id != current_user.user_id)
.filter(User.active == true())
.filter(User.admin == true())
.order_by(User.created_on.asc())]
active_regular_users = [
x.user_id for x in User.query()
.filter(User.user_id != current_user.user_id)
.filter(User.active == true())
.filter(User.admin == false())
.order_by(User.created_on.asc())]
list_of_accounts = [current_user.user_id]
list_of_accounts += active_super_admins
list_of_accounts += active_regular_users
return list_of_accounts
users: made the function for account deactivation easier to use inside ishell.
r1946 def deactivate_last_users(self, expected_users, current_user=None):
project: added all source files and assets
r1 """
Deactivate accounts that are over the license limits.
Algorithm of which accounts to disabled is based on the formula:
Get current user, then super admins in creation order, then regular
active users in creation order.
Using that list we mark all accounts from the end of it as inactive.
This way we block only latest created accounts.
:param expected_users: list of users in special order, we deactivate
models: fixed spelling.
r2964 the end N amount of users from that list
project: added all source files and assets
r1 """
users: made the function for account deactivation easier to use inside ishell.
r1946 list_of_accounts = self.get_accounts_in_creation_order(
current_user=current_user)
project: added all source files and assets
r1
for acc_id in list_of_accounts[expected_users + 1:]:
user = User.get(acc_id)
log.info('Deactivating account %s for license unlock', user)
user.active = False
Session().add(user)
Session().commit()
return
admin-users: add audit page to allow showing user actions in RhodeCode....
r1559
def get_user_log(self, user, filter_term):
user_log = UserLog.query()\
.filter(or_(UserLog.user_id == user.user_id,
UserLog.username == user.username))\
.options(joinedload(UserLog.user))\
.options(joinedload(UserLog.repository))\
.order_by(UserLog.action_date.desc())
user_log = user_log_filter(user_log, filter_term)
return user_log