Line data Source code
1 : /*
2 : * Copyright (C) 2020 Red Hat, Inc.
3 : * SPDX-License-Identifier: LGPL-2.1-or-later
4 : */
5 :
6 15 : import cockpit from 'cockpit';
7 15 : 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 15 : 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 : }
|