diff --git a/pillar/api/users/__init__.py b/pillar/api/users/__init__.py index 20329c3a..b2cadbf9 100644 --- a/pillar/api/users/__init__.py +++ b/pillar/api/users/__init__.py @@ -61,6 +61,9 @@ def _update_search_user_changed_role(sender, user: dict): def setup_app(app, api_prefix): from pillar.api import service + from . import patch + + patch.setup_app(app, url_prefix=api_prefix) app.on_pre_GET_users += hooks.check_user_access app.on_post_GET_users += hooks.post_GET_user diff --git a/pillar/api/users/patch.py b/pillar/api/users/patch.py new file mode 100644 index 00000000..df3c4d86 --- /dev/null +++ b/pillar/api/users/patch.py @@ -0,0 +1,45 @@ +"""User patching support.""" + +import logging + +import bson +from flask import Blueprint +import werkzeug.exceptions as wz_exceptions + +from pillar import current_app +from pillar.auth import current_user +from pillar.api.utils import authorization, jsonify, remove_private_keys +from pillar.api import patch_handler + +log = logging.getLogger(__name__) +patch_api_blueprint = Blueprint('users.patch', __name__) + + +class UserPatchHandler(patch_handler.AbstractPatchHandler): + item_name = 'user' + + @authorization.require_login() + def patch_set_username(self, user_id: bson.ObjectId, patch: dict): + """Updates a user's username.""" + if user_id != current_user.user_id: + log.info('User %s tried to change username of user %s', + current_user.user_id, user_id) + raise wz_exceptions.Forbidden('You may only change your own username') + + new_username = patch['username'] + log.info('User %s uses PATCH to set username to %r', current_user.user_id, new_username) + + users_coll = current_app.db('users') + db_user = users_coll.find_one({'_id': user_id}) + db_user['username'] = new_username + + # Save via Eve to check the schema and trigger update hooks. + response, _, _, status = current_app.put_internal( + 'users', remove_private_keys(db_user), _id=user_id) + + return jsonify(response), status + + +def setup_app(app, url_prefix): + UserPatchHandler(patch_api_blueprint) + app.register_api_blueprint(patch_api_blueprint, url_prefix=url_prefix) diff --git a/pillar/web/jinja.py b/pillar/web/jinja.py index 57327cac..71ea8534 100644 --- a/pillar/web/jinja.py +++ b/pillar/web/jinja.py @@ -11,6 +11,8 @@ import flask_login import jinja2.filters import jinja2.utils import werkzeug.exceptions as wz_exceptions +from werkzeug.local import LocalProxy + import pillarsdk import pillar.api.utils @@ -225,6 +227,8 @@ def user_to_dict(user: auth.UserClass) -> dict: def do_json(some_object) -> str: + if isinstance(some_object, LocalProxy): + return do_json(some_object._get_current_object()) if isinstance(some_object, pillarsdk.Resource): some_object = some_object.to_dict() if isinstance(some_object, auth.UserClass): diff --git a/pillar/web/settings/routes.py b/pillar/web/settings/routes.py index 0d50dbb0..d0eb32d4 100644 --- a/pillar/web/settings/routes.py +++ b/pillar/web/settings/routes.py @@ -7,6 +7,7 @@ from flask_login import login_required, current_user from werkzeug.exceptions import abort from pillar import current_app +from pillar.auth import current_user from pillar.web import system_util from pillar.web.users import forms from pillarsdk import User, exceptions as sdk_exceptions @@ -29,11 +30,12 @@ def profile(): if form.validate_on_submit(): try: - user.username = form.username.data - user.update(api=api) + response = user.set_username(form.username.data, api=api) + log.info('updated username of %s: %s', current_user, response) flash("Profile updated", 'success') - except sdk_exceptions.ResourceInvalid as e: - message = json.loads(e.content) + except sdk_exceptions.ResourceInvalid as ex: + log.warning('unable to set username %s to %r: %s', current_user, form.username.data, ex) + message = json.loads(ex.content) flash(message) blender_id_endpoint = current_app.config['BLENDER_ID_ENDPOINT'] diff --git a/tests/test_web/test_user_settings.py b/tests/test_web/test_user_settings.py new file mode 100644 index 00000000..0a659763 --- /dev/null +++ b/tests/test_web/test_user_settings.py @@ -0,0 +1,37 @@ +import flask +import flask_login +from pillar.tests import AbstractPillarTest + + +class UsernameTest(AbstractPillarTest): + def setUp(self, **kwargs) -> None: + super().setUp(**kwargs) + self.user_id = self.create_user() + + def test_update_via_web(self) -> None: + from pillar.auth import current_user + import pillar.web.settings.routes + + with self.app.app_context(): + url = flask.url_for('settings.profile') + + with self.app.test_request_context( + path=url, + data={'username': 'je.moeder'}, + method='POST', + ): + self.login_api_as(self.user_id) + flask_login.login_user(current_user) + pillar.web.settings.routes.profile() + + db_user = self.fetch_user_from_db(self.user_id) + self.assertEqual('je.moeder', db_user['username']) + + def test_update_via_patch(self) -> None: + self.create_valid_auth_token(self.user_id, 'user-token') + self.patch(f'/api/users/{self.user_id}', + json={'op': 'set-username', 'username': 'je.moeder'}, + auth_token='user-token') + + db_user = self.fetch_user_from_db(self.user_id) + self.assertEqual('je.moeder', db_user['username'])