cockpit/pkg/users/account-create-dialog.js

317 lines
12 KiB
JavaScript

/*
* This file is part of Cockpit.
*
* Copyright (C) 2020 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
*/
import cockpit from 'cockpit';
import React from 'react';
import { Checkbox, Form, FormGroup, TextInput, Popover, Flex, FlexItem, Radio } from '@patternfly/react-core';
import { has_errors } from "./dialog-utils.js";
import { passwd_change } from "./password-dialogs.js";
import { password_quality, PasswordFormFields } from "cockpit-components-password.jsx";
import { show_modal_dialog, apply_modal_dialog } from "cockpit-components-dialog.jsx";
import { HelpIcon } from '@patternfly/react-icons';
const _ = cockpit.gettext;
function AccountCreateBody({ state, errors, change }) {
const {
real_name, user_name,
locked, change_passw_force
} = state;
return (
<Form isHorizontal onSubmit={apply_modal_dialog}>
<FormGroup label={_("Full name")}
helperTextInvalid={errors && errors.real_name}
validated={(errors && errors.real_name) ? "error" : "default"}
fieldId="accounts-create-real-name">
<TextInput id="accounts-create-real-name"
validated={(errors && errors.real_name) ? "error" : "default"}
value={real_name} onChange={value => change("real_name", value)} />
</FormGroup>
<FormGroup label={_("User name")}
helperTextInvalid={errors && errors.user_name}
validated={(errors && errors.user_name) ? "error" : "default"}
fieldId="accounts-create-user-name">
<TextInput id="accounts-create-user-name"
validated={(errors && errors.user_name) ? "error" : "default"}
value={user_name} onChange={value => change("user_name", value)} />
</FormGroup>
<PasswordFormFields password_label={_("Password")}
password_confirm_label={_("Confirm")}
error_password={errors && errors.password}
error_password_confirm={errors && errors.password_confirm}
idPrefix="accounts-create-password"
change={change} />
<FormGroup label={_("Authentication")} fieldId="accounts-create-locked" hasNoPaddingTop>
<Radio id="account-use-password"
label={_("Use password")}
isChecked={!locked} onChange={checked => change("locked", !checked)}
description={
<Checkbox id="accounts-create-force-password-change"
className="pf-u-mb-xs"
label={_("Require password change on first login")}
isChecked={change_passw_force} onChange={checked => change("change_passw_force", checked)} />
} />
<Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
<FlexItem spacer={{ default: 'spacerSm' }}>
<Radio id="accounts-create-locked"
isChecked={locked} onChange={checked => change("locked", checked)}
label={_("Disallow password authentication")} />
</FlexItem>
<FlexItem spacer={{ default: 'spacerLg' }}>
<Popover bodyContent={_("Other authentication methods are still available even when interactive password authentication is not allowed.")}
showClose={false}>
<HelpIcon />
</Popover>
</FlexItem>
</Flex>
</FormGroup>
</Form>
);
}
function is_valid_char_username(c) {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '.' || c == '_' || c == '-';
}
function validate_username(username, accounts) {
if (!username)
return _("No user name specified");
for (let i = 0; i < username.length; i++) {
if (!is_valid_char_username(username[i]))
return _("The user name can only consist of letters from a-z, digits, dots, dashes and underscores.");
}
for (let k = 0; k < accounts.length; k++) {
if (accounts[k].name == username)
return _("This user name already exists");
}
return null;
}
function suggest_username(realname) {
function remove_diacritics(str) {
const translate_table = {
a: '[àáâãäå]',
ae: 'æ',
c: '[čç]',
d: 'ď',
e: '[èéêë]',
i: '[íìïî]',
l: '[ĺľ]',
n: '[ňñ]',
o: '[òóôõö]',
oe: 'œ',
r: '[ŕř]',
s: 'š',
t: 'ť',
u: '[ùúůûűü]',
y: '[ýÿ]',
z: 'ž',
};
for (const i in translate_table)
str = str.replace(new RegExp(translate_table[i], 'g'), i);
for (let k = 0; k < str.length;) {
if (!is_valid_char_username(str[k]))
str = str.substr(0, k) + str.substr(k + 1);
else
k++;
}
return str;
}
let result = "";
const name = realname.split(' ');
if (name.length === 1)
result = name[0].toLowerCase();
else if (name.length > 1)
result = name[0][0].toLowerCase() + name[name.length - 1].toLowerCase();
return remove_diacritics(result);
}
export function account_create_dialog(accounts) {
let dlg = null;
const state = {
real_name: "",
user_name: "",
password: "",
password_confirm: "",
locked: false,
confirm_weak: false,
change_passw_force: false,
};
let errors = { };
let old_password = null;
let user_name_dirty = false;
function change(field, value) {
state[field] = value;
errors = { };
if (field == "user_name")
user_name_dirty = true;
if (!user_name_dirty && field == "real_name")
state.user_name = suggest_username(state.real_name);
if (state.password != old_password) {
state.confirm_weak = false;
old_password = state.password;
}
if (field == "change_passw_force")
state.locked = false;
if (field == "locked")
state.change_passw_force = false;
update();
}
function validate(force, real_name, user_name, password, password_confirm) {
const errs = { };
if (!real_name)
errors.real_name = _("No real name specified");
if (password != password_confirm)
errs.password_confirm = _("The passwords do not match");
if (password.length > 256)
errs.password = _("Password is longer than 256 characters");
errs.user_name = validate_username(user_name, accounts);
return password_quality(password, force)
.catch(ex => {
errs.password = (ex.message || ex.toString()).replace("\n", " ");
})
.then(() => {
errors = errs;
return !has_errors(errs);
});
}
function create(real_name, user_name, password, locked, force_change) {
return cockpit.spawn(["/usr/sbin/useradd", "-D"], { superuser: "require" })
.catch(() => "")
.then(defaults => {
let shell = null;
defaults.split("\n").forEach(item => {
if (item.indexOf("SHELL=") === 0) {
shell = item.split("=")[1] || "";
}
});
const prog = ["/usr/sbin/useradd", "--create-home", "-s", shell || "/bin/bash"];
if (real_name) {
prog.push('-c');
prog.push(real_name);
}
prog.push(user_name);
return cockpit.spawn(prog, { superuser: "require", err: "message" })
.then(() => passwd_change(user_name, password))
.then(() => {
if (locked)
return cockpit.spawn([
"/usr/sbin/usermod",
user_name,
"--lock"
], { superuser: "require", err: "message" });
if (force_change)
return cockpit.spawn([
"/usr/bin/passwd",
"-e",
user_name
], { superuser: "require", err: "message" });
});
});
}
function passwd_check(force_weak, real_name, user_name, password, password_confirm, locked, force_change) {
return validate(force_weak, real_name, user_name, password, password_confirm).then(valid => {
if (valid)
return create(real_name, user_name, password, locked, force_change);
else {
if (!errors.real_name && !errors.user_name && !errors.password_confirm && state.password.length <= 256) {
state.confirm_weak = true;
}
update();
return Promise.reject();
}
});
}
function update() {
const props = {
id: "accounts-create-dialog",
title: _("Create new account"),
body: <AccountCreateBody state={state} errors={errors} change={change} />
};
const footer = {
actions: [
{
caption: _("Create"),
style: "primary",
clicked: () => {
return passwd_check(false, state.real_name, state.user_name, state.password, state.password_confirm, state.locked, state.change_passw_force);
},
disabled: state.confirm_weak
}
]
};
if (state.confirm_weak) {
footer.actions.push(
{
caption: _("Create account with weak password"),
style: "warning",
clicked: () => {
return passwd_check(true, state.real_name, state.user_name, state.password, state.password_confirm, state.locked);
}
}
);
}
if (!dlg)
dlg = show_modal_dialog(props, footer);
else {
dlg.setProps(props);
dlg.setFooterProps(footer);
}
}
update();
}