Show More
user_api.py
570 lines
| 18.7 KiB
| text/x-python
|
PythonLexer
r5088 | # Copyright (C) 2011-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/ | ||||
import logging | ||||
r1832 | from rhodecode.api import ( | |||
jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError) | ||||
r1 | from rhodecode.api.utils import ( | |||
Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update) | ||||
r1829 | from rhodecode.lib import audit_logger | |||
r1 | from rhodecode.lib.auth import AuthUser, PasswordGenerator | |||
from rhodecode.lib.exceptions import DefaultUserException | ||||
r1094 | from rhodecode.lib.utils2 import safe_int, str2bool | |||
r1 | from rhodecode.model.db import Session, User, Repository | |||
from rhodecode.model.user import UserModel | ||||
r1832 | from rhodecode.model import validation_schema | |||
from rhodecode.model.validation_schema.schemas import user_schema | ||||
r1 | ||||
log = logging.getLogger(__name__) | ||||
@jsonrpc_method() | ||||
def get_user(request, apiuser, userid=Optional(OAttr('apiuser'))): | ||||
""" | ||||
Returns the information associated with a username or userid. | ||||
* If the ``userid`` is not set, this command returns the information | ||||
for the ``userid`` calling the method. | ||||
.. note:: | ||||
Normal users may only run this command against their ``userid``. For | ||||
full privileges you must run this command using an |authtoken| with | ||||
admin rights. | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param userid: Sets the userid for which data will be returned. | ||||
:type userid: Optional(str or int) | ||||
Example output: | ||||
.. code-block:: bash | ||||
{ | ||||
"error": null, | ||||
"id": <id>, | ||||
"result": { | ||||
"active": true, | ||||
"admin": false, | ||||
"api_keys": [ list of keys ], | ||||
r1522 | "auth_tokens": [ list of tokens with details ], | |||
r1 | "email": "user@example.com", | |||
"emails": [ | ||||
"user@example.com" | ||||
], | ||||
"extern_name": "rhodecode", | ||||
"extern_type": "rhodecode", | ||||
"firstname": "username", | ||||
r4024 | "description": "user description", | |||
r1 | "ip_addresses": [], | |||
"language": null, | ||||
"last_login": "Timestamp", | ||||
r1558 | "last_activity": "Timestamp", | |||
r1 | "lastname": "surnae", | |||
r2438 | "permissions": <deprecated>, | |||
"permissions_summary": { | ||||
r1 | "global": [ | |||
"hg.inherit_default_perms.true", | ||||
"usergroup.read", | ||||
"hg.repogroup.create.false", | ||||
"hg.create.none", | ||||
r1034 | "hg.password_reset.enabled", | |||
r1 | "hg.extern_activate.manual", | |||
"hg.create.write_on_repogroup.false", | ||||
"hg.usergroup.create.false", | ||||
"group.none", | ||||
"repository.none", | ||||
"hg.register.none", | ||||
"hg.fork.repository" | ||||
], | ||||
"repositories": { "username/example": "repository.write"}, | ||||
"repositories_groups": { "user-group/repo": "group.none" }, | ||||
"user_groups": { "user_group_name": "usergroup.read" } | ||||
r2438 | } | |||
r1 | "user_id": 32, | |||
"username": "username" | ||||
} | ||||
} | ||||
""" | ||||
if not has_superadmin_permission(apiuser): | ||||
# make sure normal user does not pass someone else userid, | ||||
# he is not allowed to do that | ||||
if not isinstance(userid, Optional) and userid != apiuser.user_id: | ||||
raise JSONRPCError('userid is not the same as your user') | ||||
userid = Optional.extract(userid, evaluate_locals=locals()) | ||||
userid = getattr(userid, 'user_id', userid) | ||||
user = get_user_or_error(userid) | ||||
data = user.get_api_data(include_secrets=True) | ||||
r2437 | permissions = AuthUser(user_id=user.user_id).permissions | |||
data['permissions'] = permissions # TODO(marcink): should be deprecated | ||||
data['permissions_summary'] = permissions | ||||
r1 | return data | |||
@jsonrpc_method() | ||||
def get_users(request, apiuser): | ||||
""" | ||||
Lists all users in the |RCE| user database. | ||||
This command can only be run using an |authtoken| with admin rights to | ||||
the specified repository. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
r1603 | result: [<user_object>, ...] | |||
r1 | error: null | |||
""" | ||||
if not has_superadmin_permission(apiuser): | ||||
raise JSONRPCForbidden() | ||||
result = [] | ||||
users_list = User.query().order_by(User.username) \ | ||||
.filter(User.username != User.DEFAULT_USER) \ | ||||
.all() | ||||
for user in users_list: | ||||
result.append(user.get_api_data(include_secrets=True)) | ||||
return result | ||||
@jsonrpc_method() | ||||
def create_user(request, apiuser, username, email, password=Optional(''), | ||||
r4024 | firstname=Optional(''), lastname=Optional(''), description=Optional(''), | |||
r1 | active=Optional(True), admin=Optional(False), | |||
extern_name=Optional('rhodecode'), | ||||
extern_type=Optional('rhodecode'), | ||||
r1094 | force_password_change=Optional(False), | |||
create_personal_repo_group=Optional(None)): | ||||
r1 | """ | |||
Creates a new user and returns the new user object. | ||||
This command can only be run using an |authtoken| with admin rights to | ||||
the specified repository. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param username: Set the new username. | ||||
:type username: str or int | ||||
:param email: Set the user email address. | ||||
:type email: str | ||||
:param password: Set the new user password. | ||||
:type password: Optional(str) | ||||
:param firstname: Set the new user firstname. | ||||
:type firstname: Optional(str) | ||||
:param lastname: Set the new user surname. | ||||
:type lastname: Optional(str) | ||||
r4024 | :param description: Set user description, or short bio. Metatags are allowed. | |||
:type description: Optional(str) | ||||
r1 | :param active: Set the user as active. | |||
:type active: Optional(``True`` | ``False``) | ||||
:param admin: Give the new user admin rights. | ||||
:type admin: Optional(``True`` | ``False``) | ||||
:param extern_name: Set the authentication plugin name. | ||||
Using LDAP this is filled with LDAP UID. | ||||
:type extern_name: Optional(str) | ||||
:param extern_type: Set the new user authentication plugin. | ||||
:type extern_type: Optional(str) | ||||
:param force_password_change: Force the new user to change password | ||||
on next login. | ||||
:type force_password_change: Optional(``True`` | ``False``) | ||||
r1094 | :param create_personal_repo_group: Create personal repo group for this user | |||
:type create_personal_repo_group: Optional(``True`` | ``False``) | ||||
r1603 | ||||
r1 | Example output: | |||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
r1603 | "msg" : "created new user `<username>`", | |||
"user": <user_obj> | ||||
} | ||||
r1 | error: null | |||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
"user `<username>` already exist" | ||||
or | ||||
"email `<email>` already exist" | ||||
or | ||||
"failed to create user `<username>`" | ||||
} | ||||
""" | ||||
if not has_superadmin_permission(apiuser): | ||||
raise JSONRPCForbidden() | ||||
if UserModel().get_by_username(username): | ||||
r5095 | raise JSONRPCError(f"user `{username}` already exist") | |||
r1 | ||||
if UserModel().get_by_email(email, case_insensitive=True): | ||||
r5095 | raise JSONRPCError(f"email `{email}` already exist") | |||
r1 | ||||
# generate random password if we actually given the | ||||
# extern_name and it's not rhodecode | ||||
if (not isinstance(extern_name, Optional) and | ||||
Optional.extract(extern_name) != 'rhodecode'): | ||||
# generate temporary password if user is external | ||||
password = PasswordGenerator().gen_password(length=16) | ||||
r1094 | create_repo_group = Optional.extract(create_personal_repo_group) | |||
r4908 | if isinstance(create_repo_group, str): | |||
r1094 | create_repo_group = str2bool(create_repo_group) | |||
r1 | ||||
r1832 | username = Optional.extract(username) | |||
password = Optional.extract(password) | ||||
email = Optional.extract(email) | ||||
first_name = Optional.extract(firstname) | ||||
last_name = Optional.extract(lastname) | ||||
r4024 | description = Optional.extract(description) | |||
r1832 | active = Optional.extract(active) | |||
admin = Optional.extract(admin) | ||||
extern_type = Optional.extract(extern_type) | ||||
extern_name = Optional.extract(extern_name) | ||||
schema = user_schema.UserSchema().bind( | ||||
# user caller | ||||
user=apiuser) | ||||
try: | ||||
schema_data = schema.deserialize(dict( | ||||
username=username, | ||||
email=email, | ||||
password=password, | ||||
first_name=first_name, | ||||
last_name=last_name, | ||||
active=active, | ||||
admin=admin, | ||||
r4024 | description=description, | |||
r1832 | extern_type=extern_type, | |||
extern_name=extern_name, | ||||
)) | ||||
except validation_schema.Invalid as err: | ||||
raise JSONRPCValidationError(colander_exc=err) | ||||
r1 | try: | |||
user = UserModel().create_or_update( | ||||
r1832 | username=schema_data['username'], | |||
password=schema_data['password'], | ||||
email=schema_data['email'], | ||||
firstname=schema_data['first_name'], | ||||
lastname=schema_data['last_name'], | ||||
r4024 | description=schema_data['description'], | |||
r1832 | active=schema_data['active'], | |||
admin=schema_data['admin'], | ||||
extern_type=schema_data['extern_type'], | ||||
extern_name=schema_data['extern_name'], | ||||
r1 | force_password_change=Optional.extract(force_password_change), | |||
r1094 | create_repo_group=create_repo_group | |||
r1 | ) | |||
r1829 | Session().flush() | |||
creation_data = user.get_api_data() | ||||
audit_logger.store_api( | ||||
'user.create', action_data={'data': creation_data}, | ||||
user=apiuser) | ||||
r1 | Session().commit() | |||
return { | ||||
'msg': 'created new user `%s`' % username, | ||||
'user': user.get_api_data(include_secrets=True) | ||||
} | ||||
except Exception: | ||||
log.exception('Error occurred during creation of user') | ||||
r5095 | raise JSONRPCError(f'failed to create user `{username}`') | |||
r1 | ||||
@jsonrpc_method() | ||||
def update_user(request, apiuser, userid, username=Optional(None), | ||||
email=Optional(None), password=Optional(None), | ||||
firstname=Optional(None), lastname=Optional(None), | ||||
r4024 | description=Optional(None), active=Optional(None), admin=Optional(None), | |||
r1 | extern_type=Optional(None), extern_name=Optional(None), ): | |||
""" | ||||
Updates the details for the specified user, if that user exists. | ||||
This command can only be run using an |authtoken| with admin rights to | ||||
the specified repository. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param userid: Set the ``userid`` to update. | ||||
:type userid: str or int | ||||
:param username: Set the new username. | ||||
:type username: str or int | ||||
:param email: Set the new email. | ||||
:type email: str | ||||
:param password: Set the new password. | ||||
:type password: Optional(str) | ||||
:param firstname: Set the new first name. | ||||
:type firstname: Optional(str) | ||||
:param lastname: Set the new surname. | ||||
:type lastname: Optional(str) | ||||
r4024 | :param description: Set user description, or short bio. Metatags are allowed. | |||
:type description: Optional(str) | ||||
r1 | :param active: Set the new user as active. | |||
:type active: Optional(``True`` | ``False``) | ||||
:param admin: Give the user admin rights. | ||||
:type admin: Optional(``True`` | ``False``) | ||||
:param extern_name: Set the authentication plugin user name. | ||||
Using LDAP this is filled with LDAP UID. | ||||
:type extern_name: Optional(str) | ||||
:param extern_type: Set the authentication plugin type. | ||||
:type extern_type: Optional(str) | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
r1603 | "msg" : "updated user ID:<userid> <username>", | |||
"user": <user_object>, | ||||
} | ||||
r1 | error: null | |||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
"failed to update user `<username>`" | ||||
} | ||||
""" | ||||
if not has_superadmin_permission(apiuser): | ||||
raise JSONRPCForbidden() | ||||
user = get_user_or_error(userid) | ||||
r1829 | old_data = user.get_api_data() | |||
r1 | # only non optional arguments will be stored in updates | |||
updates = {} | ||||
try: | ||||
store_update(updates, username, 'username') | ||||
store_update(updates, password, 'password') | ||||
store_update(updates, email, 'email') | ||||
store_update(updates, firstname, 'name') | ||||
store_update(updates, lastname, 'lastname') | ||||
r4024 | store_update(updates, description, 'description') | |||
r1 | store_update(updates, active, 'active') | |||
store_update(updates, admin, 'admin') | ||||
store_update(updates, extern_name, 'extern_name') | ||||
store_update(updates, extern_type, 'extern_type') | ||||
user = UserModel().update_user(user, **updates) | ||||
r1829 | audit_logger.store_api( | |||
'user.edit', action_data={'old_data': old_data}, | ||||
user=apiuser) | ||||
r1 | Session().commit() | |||
return { | ||||
r5095 | 'msg': f'updated user ID:{user.user_id} {user.username}', | |||
r1 | 'user': user.get_api_data(include_secrets=True) | |||
} | ||||
except DefaultUserException: | ||||
log.exception("Default user edit exception") | ||||
raise JSONRPCError('editing default user is forbidden') | ||||
except Exception: | ||||
log.exception("Error occurred during update of user") | ||||
r5095 | raise JSONRPCError(f'failed to update user `{userid}`') | |||
r1 | ||||
@jsonrpc_method() | ||||
def delete_user(request, apiuser, userid): | ||||
""" | ||||
Deletes the specified user from the |RCE| user database. | ||||
This command can only be run using an |authtoken| with admin rights to | ||||
the specified repository. | ||||
.. important:: | ||||
Ensure all open pull requests and open code review | ||||
requests to this user are close. | ||||
Also ensure all repositories, or repository groups owned by this | ||||
user are reassigned before deletion. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param userid: Set the user to delete. | ||||
:type userid: str or int | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result: { | ||||
r1603 | "msg" : "deleted user ID:<userid> <username>", | |||
"user": null | ||||
} | ||||
r1 | error: null | |||
Example error output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : null | ||||
error : { | ||||
"failed to delete user ID:<userid> <username>" | ||||
} | ||||
""" | ||||
if not has_superadmin_permission(apiuser): | ||||
raise JSONRPCForbidden() | ||||
user = get_user_or_error(userid) | ||||
r1829 | old_data = user.get_api_data() | |||
r1 | try: | |||
UserModel().delete(userid) | ||||
r1829 | audit_logger.store_api( | |||
'user.delete', action_data={'old_data': old_data}, | ||||
user=apiuser) | ||||
r1 | Session().commit() | |||
return { | ||||
r5095 | 'msg': f'deleted user ID:{user.user_id} {user.username}', | |||
r1 | 'user': None | |||
} | ||||
except Exception: | ||||
log.exception("Error occurred during deleting of user") | ||||
raise JSONRPCError( | ||||
r5095 | f'failed to delete user ID:{user.user_id} {user.username}') | |||
r1 | ||||
@jsonrpc_method() | ||||
def get_user_locks(request, apiuser, userid=Optional(OAttr('apiuser'))): | ||||
""" | ||||
Displays all repositories locked by the specified user. | ||||
* If this command is run by a non-admin user, it returns | ||||
a list of |repos| locked by that user. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param userid: Sets the userid whose list of locked |repos| will be | ||||
displayed. | ||||
:type userid: Optional(str or int) | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
[repo_object, repo_object,...] | ||||
} | ||||
error : null | ||||
""" | ||||
include_secrets = False | ||||
if not has_superadmin_permission(apiuser): | ||||
# make sure normal user does not pass someone else userid, | ||||
# he is not allowed to do that | ||||
if not isinstance(userid, Optional) and userid != apiuser.user_id: | ||||
raise JSONRPCError('userid is not the same as your user') | ||||
else: | ||||
include_secrets = True | ||||
userid = Optional.extract(userid, evaluate_locals=locals()) | ||||
userid = getattr(userid, 'user_id', userid) | ||||
user = get_user_or_error(userid) | ||||
ret = [] | ||||
# show all locks | ||||
for r in Repository.getAll(): | ||||
_user_id, _time, _reason = r.locked | ||||
if _user_id and _time: | ||||
_api_data = r.get_api_data(include_secrets=include_secrets) | ||||
# if we use user filter just show the locks for this user | ||||
if safe_int(_user_id) == user.user_id: | ||||
ret.append(_api_data) | ||||
return ret | ||||
r1579 | ||||
@jsonrpc_method() | ||||
def get_user_audit_logs(request, apiuser, userid=Optional(OAttr('apiuser'))): | ||||
""" | ||||
Fetches all action logs made by the specified user. | ||||
This command takes the following options: | ||||
:param apiuser: This is filled automatically from the |authtoken|. | ||||
:type apiuser: AuthUser | ||||
:param userid: Sets the userid whose list of locked |repos| will be | ||||
displayed. | ||||
:type userid: Optional(str or int) | ||||
Example output: | ||||
.. code-block:: bash | ||||
id : <id_given_in_input> | ||||
result : { | ||||
[action, action,...] | ||||
} | ||||
error : null | ||||
""" | ||||
if not has_superadmin_permission(apiuser): | ||||
# make sure normal user does not pass someone else userid, | ||||
# he is not allowed to do that | ||||
if not isinstance(userid, Optional) and userid != apiuser.user_id: | ||||
raise JSONRPCError('userid is not the same as your user') | ||||
userid = Optional.extract(userid, evaluate_locals=locals()) | ||||
userid = getattr(userid, 'user_id', userid) | ||||
user = get_user_or_error(userid) | ||||
ret = [] | ||||
# show all user actions | ||||
for entry in UserModel().get_user_log(user, filter_term=None): | ||||
ret.append(entry) | ||||
return ret | ||||