Block password changes while Guest role is active #65

Merged
sjg merged 1 commits from agent/disable-guest-password-changes into main 2026-08-12 18:49:12 +02:00
4 changed files with 52 additions and 1 deletions
Showing only changes of commit b037225a05 - Show all commits
@@ -6097,6 +6097,14 @@ async function refreshUserManagement() {
password.className = "auth-input";
password.minLength = 8;
password.maxLength = 1024;
const syncPasswordAccess = () => {
const guestActive = guestInput?.checked === true;
password.disabled = guestActive;
password.title = guestActive ? "Password changes are unavailable while the Guest role is active" : "";
if (guestActive) password.value = "";
};
roleInputs.forEach(({ input }) => input.addEventListener("change", syncPasswordAccess));
syncPasswordAccess();
const save = document.createElement("button");
save.type = "button";
save.textContent = "Save";
@@ -5173,6 +5173,14 @@ async function refreshUserManagement() {
}
const password = document.createElement("input");
password.type = "password"; password.placeholder = "New password (8+ characters)"; password.autocomplete = "new-password"; password.className = "auth-input"; password.minLength = 8; password.maxLength = 1024;
const syncPasswordAccess = () => {
const guestActive = guestInput?.checked === true;
password.disabled = guestActive;
password.title = guestActive ? "Password changes are unavailable while the Guest role is active" : "";
if (guestActive) password.value = "";
};
roleInputs.forEach(({ input }) => input.addEventListener("change", syncPasswordAccess));
syncPasswordAccess();
const save = document.createElement("button"); save.type = "button"; save.textContent = "Save";
save.addEventListener("click", async () => {
const changes: { roles?: AuthRole[]; password?: string; enabled?: boolean } = {
@@ -18,6 +18,7 @@ const fixture = await startWebFixture({
},
users: [
{ username: "admin", roles: ALL_ROLES, enabled: true },
{ username: "guest", roles: ["guest"], enabled: true },
{ username: "listener", roles: ["read"], enabled: false },
],
});
@@ -41,6 +42,7 @@ try {
const rows = [...document.querySelectorAll("#user-list > .sch-row")];
const rowFor = (username) => rows.find((row) => row.querySelector("strong")?.textContent.startsWith(username));
const admin = rowFor("admin");
const guest = rowFor("guest");
const listener = rowFor("listener");
const role = (row, value) => row?.querySelector(`input[value="${value}"]`);
return {
@@ -49,6 +51,7 @@ try {
adminRoleLocked: role(admin, "administrator")?.disabled,
adminGuestLocked: role(admin, "guest")?.disabled,
adminRemoveLocked: admin?.querySelector("button.danger")?.disabled,
guestPasswordLocked: guest?.querySelector('input[type="password"]')?.disabled,
listenerEnabled: listener?.querySelector('input[type="checkbox"]')?.checked,
listenerRead: role(listener, "read")?.checked,
};
@@ -58,8 +61,16 @@ try {
assert.equal(state.adminRoleLocked, true);
assert.equal(state.adminGuestLocked, true);
assert.equal(state.adminRemoveLocked, true);
assert.equal(state.guestPasswordLocked, true);
assert.equal(state.listenerEnabled, false);
assert.equal(state.listenerRead, true);
const listenerRow = page.locator("#user-list > .sch-row").filter({ hasText: "listener" });
const listenerPassword = listenerRow.locator('input[type="password"]');
await listenerRow.locator('input[value="guest"]').check();
assert.equal(await listenerPassword.isDisabled(), true);
await listenerRow.locator('input[value="read"]').check();
assert.equal(await listenerPassword.isDisabled(), false);
assert.deepEqual(runtimeErrors, []);
} finally {
await browser.close();
@@ -514,6 +514,10 @@ impl UserStore {
roles: Option<AuthRoles>,
enabled: Option<bool>,
) -> Result<(), UserStoreError> {
let roles = roles
.map(validate_roles)
.transpose()
.map_err(UserStoreError::Invalid)?;
if let Some(value) = password {
validate_password(value).map_err(UserStoreError::Invalid)?;
}
@@ -526,6 +530,16 @@ impl UserStore {
.get(username)
.cloned()
.ok_or(UserStoreError::NotFound)?;
if password.is_some()
&& roles
.as_ref()
.unwrap_or(&old.roles)
.contains(&AuthRole::Guest)
{
return Err(UserStoreError::Invalid(
"password changes are not available while the guest role is active".to_string(),
));
}
let remains_enabled_administrator = enabled.unwrap_or(old.enabled)
&& roles
.as_ref()
@@ -544,7 +558,7 @@ impl UserStore {
record.password_hash = hash;
}
if let Some(value) = roles {
record.roles = validate_roles(value).map_err(UserStoreError::Invalid)?;
record.roles = value;
}
if let Some(value) = enabled {
record.enabled = value;
@@ -1666,6 +1680,16 @@ mod tests {
assert_eq!(response.status(), actix_web::http::StatusCode::OK);
let admin_cookie = response.response().cookies().next().unwrap().to_string();
let request = aw_test::TestRequest::patch()
.uri("/auth/users/guest")
.insert_header((actix_web::http::header::COOKIE, admin_cookie.clone()))
.set_json(serde_json::json!({"password":"changed-guest-password"}))
.to_request();
assert_eq!(
aw_test::call_service(&app, request).await.status(),
actix_web::http::StatusCode::BAD_REQUEST
);
let request = aw_test::TestRequest::post()
.uri("/auth/users")
.insert_header((actix_web::http::header::COOKIE, admin_cookie.clone()))