LCOV - code coverage report
Current view: top level - pkg/users - password-dialogs.js Coverage Total Hit
Test: cockpit Lines: 75.2 % 226 170
Test Date: 2026-06-25 11:17:56

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2020 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6           19 : import cockpit from 'cockpit';
       7           19 : import React from 'react';
       8              : import { superuser } from "superuser";
       9              : import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
      10              : import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
      11              : 
      12              : import { has_errors } from "./dialog-utils.js";
      13              : import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
      14              : import { password_quality, PasswordFormFields } from "cockpit-components-password.jsx";
      15              : import { FormHelper } from "cockpit-components-form-helper";
      16              : 
      17           19 : const _ = cockpit.gettext;
      18              : 
      19            1 : function passwd_self(old_pass, new_pass) {
      20            1 :     const old_exps = [
      21            1 :         /Current password: $/,
      22            1 :         /Current Password: $/,
      23            1 :         /.*\(current\) UNIX password: $/,
      24            1 :     ];
      25            1 :     const new_exps = [
      26            1 :         /.*New password: $/,
      27            1 :         /.*Retype new password: $/,
      28            1 :         /.*Enter new \w*\s?password: $/,
      29            1 :         /.*Retype new \w*\s?password: $/
      30            1 :     ];
      31            1 :     const bad_exps = [
      32            1 :         /.*BAD PASSWORD:.*/
      33            1 :     ];
      34            1 :     const too_new_exps = [
      35            1 :         /.*must wait longer to change.*/
      36            1 :     ];
      37              : 
      38            1 :     return new Promise((resolve, reject) => {
      39            1 :         let buffer = "";
      40            1 :         let sent_new = false;
      41            1 :         let failure = _("Old password not accepted");
      42              : 
      43            0 :         const timeout = window.setTimeout(function() {
      44            0 :             failure = _("Prompting via passwd timed out");
      45            0 :             proc.close("timeout");
      46            0 :         }, 10 * 1000);
      47              : 
      48            1 :         const proc = cockpit.spawn(["passwd"], { pty: true, environ: ["LC_ALL=C"], err: "out" })
      49            1 :                 .always(function() {
      50            1 :                     window.clearInterval(timeout);
      51            1 :                 })
      52            0 :                 .done(function() {
      53            0 :                     resolve();
      54            0 :                 })
      55            1 :                 .fail(function(ex) {
      56            0 :                     if (ex.exit_status || ex.problem == "timeout")
      57            1 :                         ex = new Error(failure);
      58            1 :                     reject(ex);
      59            1 :                 })
      60            1 :                 .stream(function(data) {
      61            1 :                     buffer += data;
      62              : 
      63            1 :                     for (let i = 0; i < too_new_exps.length; i++) {
      64            1 :                         if (too_new_exps[i].test(buffer)) {
      65            1 :                             failure = _("You must wait longer to change your password");
      66            1 :                         }
      67            1 :                     }
      68              : 
      69            0 :                     if (sent_new) {
      70            0 :                         for (let i = 0; i < bad_exps.length; i++) {
      71            0 :                             if (bad_exps[i].test(buffer)) {
      72            0 :                                 failure = _("New password was not accepted");
      73            0 :                             }
      74            0 :                         }
      75            0 :                     }
      76              : 
      77            1 :                     for (let i = 0; i < old_exps.length; i++) {
      78            1 :                         if (old_exps[i].test(buffer)) {
      79            1 :                             buffer = "";
      80            1 :                             this.input(old_pass + "\n", true);
      81            1 :                             return;
      82            1 :                         }
      83            1 :                     }
      84              : 
      85            1 :                     for (let i = 0; i < new_exps.length; i++) {
      86            0 :                         if (new_exps[i].test(buffer)) {
      87            0 :                             buffer = "";
      88            0 :                             this.input(new_pass + "\n", true);
      89            0 :                             failure = _("Failed to change password");
      90            0 :                             sent_new = true;
      91            0 :                             return;
      92            0 :                         }
      93            1 :                     }
      94            1 :                 });
      95            1 :     });
      96            1 : }
      97              : 
      98            4 : export function passwd_change(user, new_pass) {
      99            4 :     return new Promise((resolve, reject) => {
     100            4 :         cockpit.spawn(["chpasswd"], { superuser: "require", err: "out" })
     101            4 :                 .input(user + ":" + new_pass)
     102            4 :                 .done(function() {
     103            4 :                     resolve();
     104            4 :                 })
     105            0 :                 .fail(function(ex, response) {
     106            0 :                     if (ex.exit_status) {
     107            0 :                         console.log(ex);
     108            0 :                         if (response)
     109            0 :                             ex = new Error(response);
     110              :                         else
     111            0 :                             ex = new Error(_("Failed to change password"));
     112            0 :                     }
     113            0 :                     reject(ex);
     114            0 :                 });
     115            4 :     });
     116            4 : }
     117              : 
     118            1 : function SetPasswordDialogBody({ state, errors, change }) {
     119            1 :     const { need_old, password_old, current_user } = state;
     120              : 
     121            1 :     return (
     122            1 :         <Form isHorizontal onSubmit={apply_modal_dialog}>
     123            1 :             { need_old &&
     124            1 :             <>
     125            1 :                 <input hidden disabled value={current_user} />
     126            1 :                 <FormGroup label={_("Old password")}
     127            1 :                            fieldId="account-set-password-old">
     128            1 :                     <TextInput className="check-passwords" type="password" id="account-set-password-old"
     129            1 :                                autoComplete="current-password" value={password_old} onChange={(_event, value) => change("password_old", value)} />
     130            1 :                     <FormHelper helperTextInvalid={errors?.password_old} />
     131            1 :                 </FormGroup>
     132            1 :             </> }
     133            1 :             <PasswordFormFields password_label={_("New password")}
     134            1 :                                 password_confirm_label={_("Confirm new password")}
     135            1 :                                 error_password={errors?.password}
     136            1 :                                 error_password_confirm={errors?.password_confirm}
     137            1 :                                 idPrefix="account-set-password"
     138            1 :                                 change={change} />
     139            1 :         </Form>
     140              :     );
     141            1 : }
     142              : 
     143            1 : export function set_password_dialog(account, current_user) {
     144            1 :     let dlg = null;
     145              : 
     146            1 :     const change_self = (account.name == current_user && !superuser.allowed);
     147              : 
     148            1 :     const state = {
     149            1 :         need_old: change_self,
     150            1 :         current_user,
     151            1 :         password_old: "",
     152            1 :         password: "",
     153            1 :         password_confirm: "",
     154            1 :         confirm_weak: false,
     155            1 :     };
     156              : 
     157            1 :     let errors = { };
     158              : 
     159            1 :     let old_password = null;
     160              : 
     161            1 :     function change(field, value) {
     162            1 :         state[field] = value;
     163              : 
     164            1 :         if (state.password != old_password) {
     165            1 :             state.confirm_weak = false;
     166            1 :             old_password = state.password;
     167            1 :             errors = { };
     168            1 :         }
     169              : 
     170            1 :         update();
     171            1 :     }
     172              : 
     173            1 :     function validate(force, password, password_confirm) {
     174            1 :         const errs = { };
     175              : 
     176            1 :         if (password != password_confirm)
     177            0 :             errs.password_confirm = _("The passwords do not match");
     178              : 
     179            1 :         if (password.length > 256)
     180            0 :             errs.password = _("Password is longer than 256 characters");
     181              : 
     182            1 :         return password_quality(password, force)
     183            0 :                 .catch(ex => {
     184            0 :                     errs.password = (ex.message || ex.toString()).replaceAll("\n", " ");
     185            0 :                 })
     186            1 :                 .then(() => {
     187            1 :                     errors = errs;
     188            1 :                     return !has_errors(errs);
     189            1 :                 });
     190            1 :     }
     191              : 
     192            1 :     function passwd_check(force, password, password_confirm, password_old) {
     193            1 :         return validate(force, password, password_confirm).then(valid => {
     194            1 :             if (valid) {
     195            1 :                 if (change_self)
     196            0 :                     return passwd_self(password_old, password);
     197              :                 else
     198            0 :                     return passwd_change(account.name, password);
     199            0 :             } else {
     200            0 :                 if (!errors.password_confirm && state.password.length <= 256) {
     201            0 :                     state.confirm_weak = true;
     202            0 :                 }
     203            0 :                 update();
     204            0 :                 return Promise.reject();
     205            0 :             }
     206            1 :         });
     207            1 :     }
     208              : 
     209            1 :     function update() {
     210            1 :         const props = {
     211            1 :             id: "account-set-password-dialog",
     212            1 :             title: _("Set password"),
     213            1 :             body: <SetPasswordDialogBody state={state} errors={errors} change={change} />
     214            1 :         };
     215              : 
     216            1 :         const footer = {
     217            1 :             actions: [
     218            1 :                 {
     219            1 :                     caption: _("Set password"),
     220            1 :                     style: "primary",
     221            1 :                     clicked: () => {
     222            1 :                         return passwd_check(false, state.password, state.password_confirm, state.password_old);
     223            1 :                     },
     224            1 :                     disabled: state.confirm_weak
     225            1 :                 }
     226            1 :             ]
     227            1 :         };
     228            0 :         if (state.confirm_weak) {
     229            0 :             footer.actions.push(
     230            0 :                 {
     231            0 :                     caption: _("Set weak password"),
     232            0 :                     style: "warning",
     233            0 :                     clicked: () => {
     234            0 :                         return passwd_check(true, state.password, state.password_confirm, state.password_old);
     235            0 :                     }
     236            0 :                 }
     237            0 :             );
     238            0 :         }
     239              : 
     240            1 :         if (!dlg)
     241            1 :             dlg = show_modal_dialog(props, footer);
     242            1 :         else {
     243            1 :             dlg.setProps(props);
     244            1 :             dlg.setFooterProps(footer);
     245            1 :         }
     246            1 :     }
     247              : 
     248            1 :     update();
     249            1 : }
     250              : 
     251            1 : export function reset_password_dialog(account) {
     252            1 :     const msg = cockpit.format(_("The account '$0' will be forced to change their password on next login"),
     253            1 :                                account.name);
     254              : 
     255            1 :     const props = {
     256            1 :         id: "password-reset",
     257            1 :         title: _("Force password change"),
     258            1 :         body: <p>{msg}</p>
     259            1 :     };
     260              : 
     261            1 :     const footer = {
     262            1 :         actions: [
     263            1 :             {
     264            1 :                 caption: _("Reset password"),
     265            1 :                 style: "primary",
     266            1 :                 clicked: () => {
     267            1 :                     return cockpit.spawn(["passwd", "-e", account.name],
     268            1 :                                          { superuser: "require", err: "message" });
     269            1 :                 }
     270            1 :             }
     271            1 :         ]
     272            1 :     };
     273              : 
     274            1 :     show_modal_dialog(props, footer);
     275            1 : }
        

Generated by: LCOV version 2.0-1