Skip to content

Commit 1453076

Browse files
committed
🔒️ Require the current password for changing it
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.
1 parent 9836850 commit 1453076

File tree

6 files changed

+97
-30
lines changed

6 files changed

+97
-30
lines changed

docs/api/access.rst

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -238,16 +238,22 @@ Change a user's password
238238
239239
Changes the password of a user.
240240

241-
Expects a JSON object with a single property ``password`` as request body.
241+
Expects a JSON object with a property ``password`` containing the new password as
242+
request body. Without the ``SETTINGS`` permission, an additional property ``current``
243+
is also required to be set on the request body, containing the user's current password.
242244

243-
Requires the ``SETTINGS`` permission or to be logged in as the user.
245+
Requires the ``SETTINGS`` permission or to be logged in as the user. Note that ``current``
246+
will be evaluated even in presence of the ``SETTINGS`` permission, if set.
244247

245248
:param username: Name of the user to change the password for
246249
:json password: The new password to set
250+
:json current: The current password
247251
:status 200: No error
248-
:status 400: If the request doesn't contain a ``password`` property or the request
252+
:status 400: If the request doesn't contain a ``password`` property, doesn't
253+
contain a ``current`` property even though required, or the request
249254
is otherwise invalid
250-
:status 403: No admin rights and not logged in as the user
255+
:status 403: No admin rights, not logged in as the user or a current password
256+
mismatch
251257
:status 404: The user is unknown
252258

253259
.. _sec-api-access-users-settings-get:

src/octoprint/server/api/access.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,10 @@ def change_password_for_user(username):
241241
if (
242242
current_user is not None
243243
and not current_user.is_anonymous
244-
and (current_user.get_name() == username or current_user.is_admin)
244+
and (
245+
current_user.get_name() == username
246+
or current_user.has_permission(Permissions.SETTINGS)
247+
)
245248
):
246249
if "application/json" not in request.headers["Content-Type"]:
247250
abort(400, description="Expected content-type JSON")
@@ -252,7 +255,14 @@ def change_password_for_user(username):
252255
abort(400, description="Malformed JSON body in request")
253256

254257
if "password" not in data or not data["password"]:
255-
abort(400, description="password is missing")
258+
abort(400, description="new password is missing")
259+
260+
if not current_user.has_permission(Permissions.SETTINGS) or "current" in data:
261+
if "current" not in data or not data["current"]:
262+
abort(400, description="current password is missing")
263+
264+
if not userManager.check_password(username, data["current"]):
265+
abort(403, description="Invalid current password")
256266

257267
try:
258268
userManager.change_user_password(username, data["password"])

src/octoprint/static/js/app/client/access.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,17 +176,26 @@
176176
OctoPrintAccessUsersClient.prototype.changePassword = function (
177177
name,
178178
password,
179+
oldpw,
179180
opts
180181
) {
182+
if (_.isObject(oldpw)) {
183+
opts = oldpw;
184+
oldpw = undefined;
185+
}
186+
181187
if (!name || !password) {
182188
throw new OctoPrintClient.InvalidArgumentError(
183-
"user name and password must be set"
189+
"user name and new password must be set"
184190
);
185191
}
186192

187193
var data = {
188194
password: password
189195
};
196+
if (oldpw) {
197+
data["current"] = oldpw;
198+
}
190199
return this.base.putJson(this.url(name, "password"), data, opts);
191200
};
192201

src/octoprint/static/js/app/viewmodels/access.js

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ $(function () {
4040
groups: ko.observableArray([]),
4141
permissions: ko.observableArray([]),
4242
password: ko.observable(undefined),
43+
currentPassword: ko.observable(undefined),
4344
repeatedPassword: ko.observable(undefined),
4445
passwordMismatch: ko.pureComputed(function () {
4546
return self.editor.password() !== self.editor.repeatedPassword();
4647
}),
48+
currentPasswordMismatch: ko.observable(false),
4749
apikey: ko.observable(undefined),
4850
active: ko.observable(undefined),
4951
permissionSelectable: function (permission) {
@@ -128,6 +130,11 @@ $(function () {
128130
}
129131
self.editor.password(undefined);
130132
self.editor.repeatedPassword(undefined);
133+
self.editor.currentPassword(undefined);
134+
self.editor.currentPasswordMismatch(false);
135+
});
136+
self.editor.currentPassword.subscribe(function () {
137+
self.editor.currentPasswordMismatch(false);
131138
});
132139

133140
self.requestData = function () {
@@ -244,13 +251,21 @@ $(function () {
244251
self.confirmChangePassword = function () {
245252
if (!CONFIG_ACCESS_CONTROL) return;
246253

247-
self.updatePassword(self.currentUser().name, self.editor.password()).done(
248-
function () {
254+
self.updatePassword(
255+
self.currentUser().name,
256+
self.editor.password(),
257+
self.editor.currentPassword()
258+
)
259+
.done(function () {
249260
// close dialog
250261
self.currentUser(undefined);
251262
self.changePasswordDialog.modal("hide");
252-
}
253-
);
263+
})
264+
.fail(function (xhr) {
265+
if (xhr.status === 403) {
266+
self.currentPasswordMismatch(true);
267+
}
268+
});
254269
};
255270

256271
self.confirmGenerateApikey = function () {
@@ -349,8 +364,8 @@ $(function () {
349364
.done(self.fromResponse);
350365
};
351366

352-
self.updatePassword = function (username, password) {
353-
return OctoPrint.access.users.changePassword(username, password);
367+
self.updatePassword = function (username, password, current) {
368+
return OctoPrint.access.users.changePassword(username, password, current);
354369
};
355370

356371
self.generateApikey = function (username) {

src/octoprint/static/js/app/viewmodels/usersettings.js

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,17 @@ $(function () {
2525

2626
self.access_password = ko.observable(undefined);
2727
self.access_repeatedPassword = ko.observable(undefined);
28+
self.access_currentPassword = ko.observable(undefined);
29+
self.access_currentPasswordMismatch = ko.observable(false);
2830
self.access_apikey = ko.observable(undefined);
2931
self.interface_language = ko.observable(undefined);
3032

3133
self.currentUser = ko.observable(undefined);
3234
self.currentUser.subscribe(function (newUser) {
3335
self.access_password(undefined);
3436
self.access_repeatedPassword(undefined);
37+
self.access_currentPassword(undefined);
38+
self.access_currentPasswordMismatch(false);
3539
self.access_apikey(undefined);
3640
self.interface_language("_default");
3741

@@ -45,6 +49,9 @@ $(function () {
4549
}
4650
}
4751
});
52+
self.access_currentPassword.subscribe(function () {
53+
self.access_currentPasswordMismatch(false);
54+
});
4855

4956
self.passwordMismatch = ko.pureComputed(function () {
5057
return self.access_password() !== self.access_repeatedPassword();
@@ -81,25 +88,38 @@ $(function () {
8188

8289
self.userSettingsDialog.trigger("beforeSave");
8390

84-
if (self.access_password() && !self.passwordMismatch()) {
85-
self.users.updatePassword(
86-
self.currentUser().name,
87-
self.access_password(),
88-
function () {}
89-
);
91+
function process() {
92+
var settings = {
93+
interface: {
94+
language: self.interface_language()
95+
}
96+
};
97+
self.updateSettings(self.currentUser().name, settings).done(function () {
98+
// close dialog
99+
self.currentUser(undefined);
100+
self.userSettingsDialog.modal("hide");
101+
self.loginState.reloadUser();
102+
});
90103
}
91104

92-
var settings = {
93-
interface: {
94-
language: self.interface_language()
95-
}
96-
};
97-
self.updateSettings(self.currentUser().name, settings).done(function () {
98-
// close dialog
99-
self.currentUser(undefined);
100-
self.userSettingsDialog.modal("hide");
101-
self.loginState.reloadUser();
102-
});
105+
if (self.access_password() && !self.passwordMismatch()) {
106+
self.users
107+
.updatePassword(
108+
self.currentUser().name,
109+
self.access_password(),
110+
self.access_currentPassword()
111+
)
112+
.done(function () {
113+
process();
114+
})
115+
.fail(function (xhr) {
116+
if (xhr.status === 403) {
117+
self.access_currentPasswordMismatch(true);
118+
}
119+
});
120+
} else {
121+
process();
122+
}
103123
};
104124

105125
self.copyApikey = function () {

src/octoprint/templates/dialogs/usersettings/access.jinja2

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
<p>
55
{{ _('If you do not wish to change your password, just leave the following fields empty.') }}
66
</p>
7+
<div class="control-group" data-bind="css: {error: access_currentPasswordMismatch()}">
8+
<label class="control-label" for="userSettings-access_currentPassword">{{ _('Current Password') }}</label>
9+
<div class="controls">
10+
<input type="password" class="input-block-level" id="userSettings-access_currentPassword" data-bind="value: access_currentPassword, valueUpdate: 'afterkeydown'" required>
11+
<span class="help-inline" data-bind="visible: access_currentPasswordMismatch()">{{ _('Passwords do not match') }}</span>
12+
</div>
13+
</div>
714
<div class="control-group">
815
<label class="control-label" for="userSettings-access_password">{{ _('New Password') }}</label>
916
<div class="controls">

0 commit comments

Comments
 (0)