From 1453076ee3e47fcab2dc73664ec2d61d3ef7fc4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gina=20H=C3=A4u=C3=9Fge?= Date: Thu, 18 Aug 2022 17:23:11 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=92=EF=B8=8F=20Require=20the=20current?= =?UTF-8?q?=20password=20for=20changing=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Admins may still change existing user account passwords without having to enter the current one, however the regular user oriented password change dialog has been adjusted to require entry of the current password. The API has been locked down accordingly and the password change endpoint has seen a small change due to that, please refer to the updated documentation. --- docs/api/access.rst | 14 +++-- src/octoprint/server/api/access.py | 14 ++++- src/octoprint/static/js/app/client/access.js | 11 +++- .../static/js/app/viewmodels/access.js | 27 +++++++--- .../static/js/app/viewmodels/usersettings.js | 54 +++++++++++++------ .../dialogs/usersettings/access.jinja2 | 7 +++ 6 files changed, 97 insertions(+), 30 deletions(-) diff --git a/docs/api/access.rst b/docs/api/access.rst index 9379a27cb7..8380bc9362 100644 --- a/docs/api/access.rst +++ b/docs/api/access.rst @@ -238,16 +238,22 @@ Change a user's password Changes the password of a user. - Expects a JSON object with a single property ``password`` as request body. + Expects a JSON object with a property ``password`` containing the new password as + request body. Without the ``SETTINGS`` permission, an additional property ``current`` + is also required to be set on the request body, containing the user's current password. - Requires the ``SETTINGS`` permission or to be logged in as the user. + Requires the ``SETTINGS`` permission or to be logged in as the user. Note that ``current`` + will be evaluated even in presence of the ``SETTINGS`` permission, if set. :param username: Name of the user to change the password for :json password: The new password to set + :json current: The current password :status 200: No error - :status 400: If the request doesn't contain a ``password`` property or the request + :status 400: If the request doesn't contain a ``password`` property, doesn't + contain a ``current`` property even though required, or the request is otherwise invalid - :status 403: No admin rights and not logged in as the user + :status 403: No admin rights, not logged in as the user or a current password + mismatch :status 404: The user is unknown .. _sec-api-access-users-settings-get: diff --git a/src/octoprint/server/api/access.py b/src/octoprint/server/api/access.py index a41badce95..28293dd844 100644 --- a/src/octoprint/server/api/access.py +++ b/src/octoprint/server/api/access.py @@ -241,7 +241,10 @@ def change_password_for_user(username): if ( current_user is not None and not current_user.is_anonymous - and (current_user.get_name() == username or current_user.is_admin) + and ( + current_user.get_name() == username + or current_user.has_permission(Permissions.SETTINGS) + ) ): if "application/json" not in request.headers["Content-Type"]: abort(400, description="Expected content-type JSON") @@ -252,7 +255,14 @@ def change_password_for_user(username): abort(400, description="Malformed JSON body in request") if "password" not in data or not data["password"]: - abort(400, description="password is missing") + abort(400, description="new password is missing") + + if not current_user.has_permission(Permissions.SETTINGS) or "current" in data: + if "current" not in data or not data["current"]: + abort(400, description="current password is missing") + + if not userManager.check_password(username, data["current"]): + abort(403, description="Invalid current password") try: userManager.change_user_password(username, data["password"]) diff --git a/src/octoprint/static/js/app/client/access.js b/src/octoprint/static/js/app/client/access.js index 8476b4eeeb..2963cdbbc2 100644 --- a/src/octoprint/static/js/app/client/access.js +++ b/src/octoprint/static/js/app/client/access.js @@ -176,17 +176,26 @@ OctoPrintAccessUsersClient.prototype.changePassword = function ( name, password, + oldpw, opts ) { + if (_.isObject(oldpw)) { + opts = oldpw; + oldpw = undefined; + } + if (!name || !password) { throw new OctoPrintClient.InvalidArgumentError( - "user name and password must be set" + "user name and new password must be set" ); } var data = { password: password }; + if (oldpw) { + data["current"] = oldpw; + } return this.base.putJson(this.url(name, "password"), data, opts); }; diff --git a/src/octoprint/static/js/app/viewmodels/access.js b/src/octoprint/static/js/app/viewmodels/access.js index 0c9c842097..f773f1f36a 100644 --- a/src/octoprint/static/js/app/viewmodels/access.js +++ b/src/octoprint/static/js/app/viewmodels/access.js @@ -40,10 +40,12 @@ $(function () { groups: ko.observableArray([]), permissions: ko.observableArray([]), password: ko.observable(undefined), + currentPassword: ko.observable(undefined), repeatedPassword: ko.observable(undefined), passwordMismatch: ko.pureComputed(function () { return self.editor.password() !== self.editor.repeatedPassword(); }), + currentPasswordMismatch: ko.observable(false), apikey: ko.observable(undefined), active: ko.observable(undefined), permissionSelectable: function (permission) { @@ -128,6 +130,11 @@ $(function () { } self.editor.password(undefined); self.editor.repeatedPassword(undefined); + self.editor.currentPassword(undefined); + self.editor.currentPasswordMismatch(false); + }); + self.editor.currentPassword.subscribe(function () { + self.editor.currentPasswordMismatch(false); }); self.requestData = function () { @@ -244,13 +251,21 @@ $(function () { self.confirmChangePassword = function () { if (!CONFIG_ACCESS_CONTROL) return; - self.updatePassword(self.currentUser().name, self.editor.password()).done( - function () { + self.updatePassword( + self.currentUser().name, + self.editor.password(), + self.editor.currentPassword() + ) + .done(function () { // close dialog self.currentUser(undefined); self.changePasswordDialog.modal("hide"); - } - ); + }) + .fail(function (xhr) { + if (xhr.status === 403) { + self.currentPasswordMismatch(true); + } + }); }; self.confirmGenerateApikey = function () { @@ -349,8 +364,8 @@ $(function () { .done(self.fromResponse); }; - self.updatePassword = function (username, password) { - return OctoPrint.access.users.changePassword(username, password); + self.updatePassword = function (username, password, current) { + return OctoPrint.access.users.changePassword(username, password, current); }; self.generateApikey = function (username) { diff --git a/src/octoprint/static/js/app/viewmodels/usersettings.js b/src/octoprint/static/js/app/viewmodels/usersettings.js index 39828c365e..4588241b6d 100644 --- a/src/octoprint/static/js/app/viewmodels/usersettings.js +++ b/src/octoprint/static/js/app/viewmodels/usersettings.js @@ -25,6 +25,8 @@ $(function () { self.access_password = ko.observable(undefined); self.access_repeatedPassword = ko.observable(undefined); + self.access_currentPassword = ko.observable(undefined); + self.access_currentPasswordMismatch = ko.observable(false); self.access_apikey = ko.observable(undefined); self.interface_language = ko.observable(undefined); @@ -32,6 +34,8 @@ $(function () { self.currentUser.subscribe(function (newUser) { self.access_password(undefined); self.access_repeatedPassword(undefined); + self.access_currentPassword(undefined); + self.access_currentPasswordMismatch(false); self.access_apikey(undefined); self.interface_language("_default"); @@ -45,6 +49,9 @@ $(function () { } } }); + self.access_currentPassword.subscribe(function () { + self.access_currentPasswordMismatch(false); + }); self.passwordMismatch = ko.pureComputed(function () { return self.access_password() !== self.access_repeatedPassword(); @@ -81,25 +88,38 @@ $(function () { self.userSettingsDialog.trigger("beforeSave"); - if (self.access_password() && !self.passwordMismatch()) { - self.users.updatePassword( - self.currentUser().name, - self.access_password(), - function () {} - ); + function process() { + var settings = { + interface: { + language: self.interface_language() + } + }; + self.updateSettings(self.currentUser().name, settings).done(function () { + // close dialog + self.currentUser(undefined); + self.userSettingsDialog.modal("hide"); + self.loginState.reloadUser(); + }); } - var settings = { - interface: { - language: self.interface_language() - } - }; - self.updateSettings(self.currentUser().name, settings).done(function () { - // close dialog - self.currentUser(undefined); - self.userSettingsDialog.modal("hide"); - self.loginState.reloadUser(); - }); + if (self.access_password() && !self.passwordMismatch()) { + self.users + .updatePassword( + self.currentUser().name, + self.access_password(), + self.access_currentPassword() + ) + .done(function () { + process(); + }) + .fail(function (xhr) { + if (xhr.status === 403) { + self.access_currentPasswordMismatch(true); + } + }); + } else { + process(); + } }; self.copyApikey = function () { diff --git a/src/octoprint/templates/dialogs/usersettings/access.jinja2 b/src/octoprint/templates/dialogs/usersettings/access.jinja2 index 50f4ba45f8..9cd750b6ff 100644 --- a/src/octoprint/templates/dialogs/usersettings/access.jinja2 +++ b/src/octoprint/templates/dialogs/usersettings/access.jinja2 @@ -4,6 +4,13 @@

{{ _('If you do not wish to change your password, just leave the following fields empty.') }}

+
+ +
+ + {{ _('Passwords do not match') }} +
+