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') }} +
+