user.py
1055 lines
| 39.4 KiB
| text/x-python
|
PythonLexer
r5088 | # Copyright (C) 2010-2023 RhodeCode GmbH | |||
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 | ||||
r2358 | import datetime | |||
import ipaddress | ||||
r1 | ||||
r2358 | from pyramid.threadlocal import get_current_request | |||
r1 | from sqlalchemy.exc import DatabaseError | |||
r375 | from rhodecode import events | |||
r1559 | from rhodecode.lib.user_log_filter import user_log_filter | |||
r1 | from rhodecode.lib.utils2 import ( | |||
r5070 | get_current_rhodecode_user, action_logger_generic, | |||
r1094 | AttributeDict, str2bool) | |||
r5070 | from rhodecode.lib.str_utils import safe_str | |||
r1780 | from rhodecode.lib.exceptions import ( | |||
DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, | ||||
r4351 | UserOwnsUserGroupsException, NotAllowedToCreateUserError, | |||
r5352 | UserOwnsPullRequestsException, UserOwnsArtifactsException, DuplicateUpdateUserError) | |||
r1 | from rhodecode.lib.caching_query import FromCache | |||
from rhodecode.model import BaseModel | ||||
r1780 | from rhodecode.model.db import ( | |||
r4518 | _hash_key, func, true, false, or_, joinedload, User, UserToPerm, | |||
r1780 | UserEmailMap, UserIpMap, UserLog) | |||
r1 | from rhodecode.model.meta import Session | |||
r4351 | from rhodecode.model.auth_token import AuthTokenModel | |||
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: | ||||
r1749 | user = user.options( | |||
r5009 | FromCache("sql_cache_short", f"get_user_{user_id}")) | |||
r1 | return user.get(user_id) | |||
def get_user(self, user): | ||||
return self._get_user(user) | ||||
r1678 | def _serialize_user(self, user): | |||
import rhodecode.lib.helpers as h | ||||
return { | ||||
'id': user.user_id, | ||||
r1815 | 'first_name': user.first_name, | |||
'last_name': user.last_name, | ||||
r1678 | 'username': user.username, | |||
'email': user.email, | ||||
'icon_link': h.gravatar_url(user.email, 30), | ||||
r2484 | 'profile_link': h.link_to_user(user), | |||
r1780 | 'value_display': h.escape(h.person(user)), | |||
r1678 | 'value': user.username, | |||
'value_type': 'user', | ||||
'active': user.active, | ||||
} | ||||
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: | ||||
r5096 | ilike_expression = f'%{safe_str(name_contains)}%' | |||
r1677 | query = query.filter( | |||
or_( | ||||
User.name.ilike(ilike_expression), | ||||
User.lastname.ilike(ilike_expression), | ||||
User.username.ilike(ilike_expression) | ||||
) | ||||
) | ||||
r4518 | # sort by len to have top most matches first | |||
query = query.order_by(func.length(User.username))\ | ||||
.order_by(User.username) | ||||
r1677 | query = query.limit(limit) | |||
r4518 | ||||
r1677 | users = query.all() | |||
_users = [ | ||||
r1678 | self._serialize_user(user) for user in users | |||
r1677 | ] | |||
return _users | ||||
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) | ||||
r5365 | ||||
r1 | if cache: | |||
r1749 | name_key = _hash_key(username) | |||
user = user.options( | ||||
r5009 | FromCache("sql_cache_short", f"get_user_{name_key}")) | |||
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): | ||||
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() | ||||
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 | ||||
} | ||||
r1094 | if 'create_repo_group' in form_data: | |||
user_data['create_repo_group'] = str2bool( | ||||
form_data.get('create_repo_group')) | ||||
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( | ||||
r2358 | "You can't edit this user (`%(username)s`) since it's " | |||
"crucial for entire application" % { | ||||
'username': user.username}) | ||||
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, | ||||
r4022 | 'description': user.description, | |||
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, | ||||
r1094 | allow_to_create_user=True, create_repo_group=None, | |||
r4024 | updating_user_id=None, language=None, description='', | |||
r4022 | strict_creation_check=True): | |||
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. | ||||
r4024 | :param description: user description | |||
:param strict_creation_check: checks for allowed creation license wise etc. | ||||
r1 | ||||
:returns: new User object with injected `is_new_user` attribute. | ||||
""" | ||||
r2358 | ||||
r1 | if not cur_user: | |||
cur_user = getattr(get_current_rhodecode_user(), 'username', None) | ||||
from rhodecode.lib.auth import ( | ||||
r4351 | get_crypt_password, check_password) | |||
r4445 | from rhodecode.lib import hooks_base | |||
r1 | ||||
def _password_change(new_user, password): | ||||
r2153 | old_password = new_user.password or '' | |||
r1 | # empty password | |||
r2153 | if not old_password: | |||
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: | ||||
r1094 | # first check if we gave crypted password back, and if it | |||
# matches it's not password change | ||||
r1 | if new_user.password == password: | |||
return False | ||||
r2153 | password_match = check_password(password, old_password) | |||
r1 | if not password_match: | |||
return True | ||||
return False | ||||
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 | ||||
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 ' | ||||
r3061 | 'database with user_id `%s` ', updating_user_id) | |||
r1 | user = User.get(updating_user_id) | |||
r5352 | # now also validate if USERNAME belongs to potentially other user | |||
maybe_other_user = User.get_by_username(username, case_insensitive=True) | ||||
if maybe_other_user and maybe_other_user.user_id != updating_user_id: | ||||
raise DuplicateUpdateUserError(f'different user exists with the {username} username') | ||||
r1 | else: | |||
log.debug('Checking for existing account in RhodeCode ' | ||||
r3061 | 'database with username `%s` ', username) | |||
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 | ||||
r4445 | hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True) | |||
r375 | events.trigger(events.UserPreCreate(user_data)) | |||
r1 | new_user = User() | |||
edit = False | ||||
else: | ||||
r3944 | log.debug('updating user `%s`', username) | |||
r375 | events.trigger(events.UserPreUpdate(user, user_data)) | |||
r1 | new_user = user | |||
edit = True | ||||
# we're not allowed to edit default user | ||||
if user.username == User.DEFAULT_USER: | ||||
raise DefaultUserException( | ||||
r2358 | "You can't edit this user (`%(username)s`) since it's " | |||
"crucial for entire application" | ||||
% {'username': user.username}) | ||||
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 | ||||
r3255 | extern_name = extern_name or auth_rhodecode.RhodeCodeAuthPlugin.uid | |||
extern_type = extern_type or auth_rhodecode.RhodeCodeAuthPlugin.uid | ||||
r1 | ||||
try: | ||||
new_user.username = username | ||||
new_user.admin = admin | ||||
new_user.email = email | ||||
new_user.active = active | ||||
r5070 | new_user.extern_name = safe_str(extern_name) | |||
new_user.extern_type = safe_str(extern_type) | ||||
r1 | new_user.name = firstname | |||
new_user.lastname = lastname | ||||
r4022 | new_user.description = description | |||
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) | ||||
r734 | new_user.update_userdata(notification_status=True) | |||
r1 | ||||
self.sa.add(new_user) | ||||
if not edit and create_repo_group: | ||||
r1094 | RepoGroupModel().create_personal_repo_group( | |||
new_user, commit_early=False) | ||||
r1 | if not edit: | |||
# add the RSS token | ||||
r2951 | self.add_auth_token( | |||
user=username, lifetime_minutes=-1, | ||||
role=self.auth_token_role.ROLE_FEED, | ||||
r5096 | description='Generated feed token') | |||
r2951 | ||||
r1953 | kwargs = new_user.get_dict() | |||
# backward compat, require api_keys present | ||||
kwargs['api_keys'] = kwargs['auth_tokens'] | ||||
r4445 | hooks_base.create_user(created_by=cur_user, **kwargs) | |||
r1094 | events.trigger(events.UserPostCreate(user_data)) | |||
r1 | return new_user | |||
except (DatabaseError,): | ||||
log.error(traceback.format_exc()) | ||||
raise | ||||
r3255 | def create_registration(self, form_data, | |||
extern_name='rhodecode', extern_type='rhodecode'): | ||||
r1 | from rhodecode.model.notification import NotificationModel | |||
from rhodecode.model.notification import EmailNotificationModel | ||||
try: | ||||
form_data['admin'] = False | ||||
r3255 | form_data['extern_name'] = extern_name | |||
form_data['extern_type'] = extern_type | ||||
r1 | new_user = self.create(form_data) | |||
self.sa.add(new_user) | ||||
self.sa.flush() | ||||
user_data = new_user.get_dict() | ||||
r4057 | user_data.update({ | |||
'first_name': user_data.get('firstname'), | ||||
'last_name': user_data.get('lastname'), | ||||
}) | ||||
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, | ||||
r4560 | notification_subject='', # Filled in based on the notification_type | |||
notification_body='', # Filled in based on the notification_type | ||||
r1 | notification_type=notification_type, | |||
recipients=None, # all admins | ||||
email_kwargs=kwargs, | ||||
) | ||||
return new_user | ||||
except Exception: | ||||
log.error(traceback.format_exc()) | ||||
raise | ||||
r4351 | def _handle_user_repos(self, username, repositories, handle_user, | |||
handle_mode=None): | ||||
r1 | left_overs = True | |||
from rhodecode.model.repo import RepoModel | ||||
if handle_mode == 'detach': | ||||
for obj in repositories: | ||||
r4351 | obj.user = handle_user | |||
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 | ||||
r4351 | def _handle_user_repo_groups(self, username, repository_groups, handle_user, | |||
r1 | handle_mode=None): | |||
r4351 | ||||
r1 | left_overs = True | |||
from rhodecode.model.repo_group import RepoGroupModel | ||||
if handle_mode == 'detach': | ||||
for r in repository_groups: | ||||
r4351 | r.user = handle_user | |||
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,) | ||||
r3038 | r.personal = False | |||
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 | ||||
r4351 | def _handle_user_user_groups(self, username, user_groups, handle_user, | |||
handle_mode=None): | ||||
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: | ||||
r4351 | user_user_group_to_perm.user = handle_user | |||
r.user = handle_user | ||||
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 | ||||
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 | ||||
r5070 | # if nothing is done we have leftovers left | |||
r4351 | return left_overs | |||
def _handle_user_artifacts(self, username, artifacts, handle_user, | ||||
handle_mode=None): | ||||
r4011 | left_overs = True | |||
if handle_mode == 'detach': | ||||
for a in artifacts: | ||||
r4351 | a.upload_user = handle_user | |||
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 | ||||
r4351 | request = get_current_request() | |||
r5516 | f_store = store_utils.get_filestore_backend(request.registry.settings) | |||
r4011 | for a in artifacts: | |||
file_uid = a.file_uid | ||||
r5516 | f_store.delete(file_uid) | |||
r4011 | self.sa.delete(a) | |||
left_overs = False | ||||
# if nothing is done we have left overs left | ||||
return left_overs | ||||
r1 | def delete(self, user, cur_user=None, handle_repos=None, | |||
r4351 | handle_repo_groups=None, handle_user_groups=None, | |||
handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None): | ||||
r4445 | from rhodecode.lib import hooks_base | |||
r3979 | ||||
r1 | if not cur_user: | |||
r3981 | cur_user = getattr(get_current_rhodecode_user(), 'username', None) | |||
r4351 | ||||
r1 | user = self._get_user(user) | |||
try: | ||||
if user.username == User.DEFAULT_USER: | ||||
raise DefaultUserException( | ||||
r5095 | "You can't remove this user since it's" | |||
" crucial for entire application") | ||||
r4351 | handle_user = handle_new_owner or self.cls.get_first_super_admin() | |||
log.debug('New detached objects owner %s', handle_user) | ||||
r1 | ||||
left_overs = self._handle_user_repos( | ||||
r4351 | user.username, user.repositories, handle_user, handle_repos) | |||
r1 | if left_overs and user.repositories: | |||
repos = [x.repo_name for x in user.repositories] | ||||
raise UserOwnsReposException( | ||||
r5095 | 'user "%(username)s" still owns %(len_repos)s repositories and cannot be ' | |||
'removed. Switch owners or remove those repositories:%(list_repos)s' | ||||
r2358 | % {'username': user.username, 'len_repos': len(repos), | |||
'list_repos': ', '.join(repos)}) | ||||
r1 | ||||
left_overs = self._handle_user_repo_groups( | ||||
r4351 | user.username, user.repository_groups, handle_user, handle_repo_groups) | |||
r1 | if left_overs and user.repository_groups: | |||
repo_groups = [x.group_name for x in user.repository_groups] | ||||
raise UserOwnsRepoGroupsException( | ||||
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' | ||||
r2358 | % {'username': user.username, 'len_repo_groups': len(repo_groups), | |||
'list_repo_groups': ', '.join(repo_groups)}) | ||||
r1 | ||||
left_overs = self._handle_user_user_groups( | ||||
r4351 | user.username, user.user_groups, handle_user, handle_user_groups) | |||
r1 | if left_overs and user.user_groups: | |||
user_groups = [x.users_group_name for x in user.user_groups] | ||||
raise UserOwnsUserGroupsException( | ||||
r5095 | 'user "%s" still owns %s user groups and cannot be ' | |||
'removed. Switch owners or remove those user groups:%s' | ||||
r1 | % (user.username, len(user_groups), ', '.join(user_groups))) | |||
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: | ||||
r5095 | pull_requests = [f'!{x.pull_request_id}' for x in user.user_pull_requests] | |||
r4351 | raise UserOwnsPullRequestsException( | |||
r5095 | 'user "%s" still owns %s pull requests and cannot be ' | |||
'removed. Switch owners or remove those pull requests:%s' | ||||
r4351 | % (user.username, len(pull_requests), ', '.join(pull_requests))) | |||
r4011 | left_overs = self._handle_user_artifacts( | |||
r4351 | user.username, user.artifacts, handle_user, handle_artifacts) | |||
r4011 | if left_overs and user.artifacts: | |||
artifacts = [x.file_uid for x in user.artifacts] | ||||
raise UserOwnsArtifactsException( | ||||
r5095 | 'user "%s" still owns %s artifacts and cannot be ' | |||
'removed. Switch owners or remove those artifacts:%s' | ||||
r4011 | % (user.username, len(artifacts), ', '.join(artifacts))) | |||
r3979 | user_data = user.get_dict() # fetch user data before expire | |||
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) | ||||
r3979 | ||||
r4445 | hooks_base.delete_user(deleted_by=cur_user, **user_data) | |||
r1 | except Exception: | |||
log.error(traceback.format_exc()) | ||||
raise | ||||
r37 | def reset_password_link(self, data, pwd_reset_url): | |||
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 = { | ||||
r37 | 'password_reset_url': pwd_reset_url, | |||
r1 | 'user': user, | |||
'email': user_email, | ||||
r4038 | 'date': datetime.datetime.now(), | |||
'first_admin_email': User.get_first_super_admin().email | ||||
r1 | } | |||
r4447 | (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email( | |||
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 | ||||
r1471 | def reset_password(self, data): | |||
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) | ||||
r1471 | ||||
# now delete the token in question | ||||
UserApiKeys = AuthTokenModel.cls | ||||
UserApiKeys().query().filter( | ||||
UserApiKeys.api_key == data['token']).delete() | ||||
r1 | Session().commit() | |||
r1296 | log.info('successfully reset password for `%s`', user_email) | |||
r1471 | ||||
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, | ||||
r4038 | 'date': datetime.datetime.now(), | |||
'first_admin_email': User.get_first_super_admin().email | ||||
r1 | } | |||
r4447 | (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email( | |||
r1471 | EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION, | |||
**email_kwargs) | ||||
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 | ||||
""" | ||||
r2657 | def token_obfuscate(token): | |||
if token: | ||||
return token[:4] + "****" | ||||
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( | ||||
r1955 | 'AuthUser: fill data execution based on: ' | |||
'user_id:%s api_key:%s username:%s', user_id, api_key, username) | ||||
r1 | try: | |||
r5365 | found_with = '' | |||
r1 | dbuser = None | |||
if user_id: | ||||
dbuser = self.get(user_id) | ||||
r5365 | found_with = 'user_id' | |||
r1 | elif api_key: | |||
dbuser = self.get_by_auth_token(api_key) | ||||
r5365 | found_with = 'auth_token' | |||
r1 | elif username: | |||
dbuser = self.get_by_username(username) | ||||
r5365 | found_with = 'username' | |||
r1 | ||||
if not dbuser: | ||||
log.warning( | ||||
r5365 | 'Unable to lookup user by id:%s api_key:%s username:%s, found with: %s', | |||
user_id, token_obfuscate(api_key), username, found_with) | ||||
r1 | return False | |||
if not dbuser.active: | ||||
r1690 | log.debug('User `%s:%s` is inactive, skipping fill data', | |||
username, user_id) | ||||
r1 | return False | |||
r5365 | log.debug('AuthUser: filling found user:%s data, found with: %s', dbuser, found_with) | |||
r1 | ||||
r4018 | attrs = { | |||
'user_id': dbuser.user_id, | ||||
'username': dbuser.username, | ||||
'name': dbuser.name, | ||||
r1815 | 'first_name': dbuser.first_name, | |||
r4018 | 'firstname': dbuser.firstname, | |||
r1815 | 'last_name': dbuser.last_name, | |||
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, | ||||
r1 | ||||
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, | ||||
r1 | ||||
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) | ||||
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: | ||||
""" | ||||
r2351 | ||||
r1 | user = self._get_user(user) | |||
obj = UserEmailMap() | ||||
obj.user = user | ||||
r2351 | obj.email = email | |||
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) | ||||
r1801 | if obj and obj.user_id == user.user_id: | |||
r1 | self.sa.delete(obj) | |||
def parse_ip_range(self, ip_range): | ||||
ip_list = [] | ||||
r1907 | ||||
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) | ||||
r5070 | start_ip = ipaddress.ip_address(safe_str(start_ip.strip())) | |||
end_ip = ipaddress.ip_address(safe_str(end_ip.strip())) | ||||
r1 | parsed_ip_range = [] | |||
r4351 | for index in range(int(start_ip), int(end_ip) + 1): | |||
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: | ||||
""" | ||||
r2351 | ||||
r1 | user = self._get_user(user) | |||
obj = UserIpMap() | ||||
obj.user = user | ||||
r2351 | obj.ip_addr = ip | |||
r1 | obj.description = description | |||
self.sa.add(obj) | ||||
return obj | ||||
r2951 | auth_token_role = AuthTokenModel.cls | |||
r5095 | def add_auth_token(self, user, lifetime_minutes, role, description='', | |||
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 | ||||
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) | ||||
r1801 | if obj and obj.user_id == user.user_id: | |||
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 | ||||
r1946 | def deactivate_last_users(self, expected_users, current_user=None): | |||
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 | ||||
r2964 | the end N amount of users from that list | |||
r1 | """ | |||
r1946 | list_of_accounts = self.get_accounts_in_creation_order( | |||
current_user=current_user) | ||||
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 | ||||
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 | ||||